views:

228

answers:

2

Working on a simple example of double sided polymorphic relationships using the has_ many_ polymorphs ActiveRecord plugin. I have two classes, "cats" and "dogs", which can have "friendships" with each other, so three classes in all: "cats", "dogs" and "friendships". A cat can be friends with a dog (yes, it does happen!) and of course also with other cats. The same goes for dogs. Here are my models:

class Cat < ActiveRecord::Base 
  validates_presence_of :name
end

class Dog < ActiveRecord::Base 
  validates_presence_of :name
end

class Friendship < ActiveRecord::Base
  belongs_to :left, :polymorphic => true
  belongs_to :right, :polymorphic => true

  acts_as_double_polymorphic_join(
    :lefts => [:cats, :dogs],
    :rights => [:dogs, :cats]
  )
end

I've been trying for days to get this to work and I must be missing something obvious. When I try to create a new friendship I get:

NameError (wrong constant name cats):
   (eval):7:in `before_save'
app/controllers/friendships_controller.rb:45:in `create'

or

NameError (wrong constant name dogs):
   (eval):7:in `before_save'
app/controllers/friendships_controller.rb:45:in `create'

This is my schema.rb:

ActiveRecord::Schema.define(:version => 20090613051350) do
  create_table "cats", :force => true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end
  create_table "dogs", :force => true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end
  create_table "friendships", :force => true do |t|
    t.integer  "left_id"
    t.string   "left_type"
    t.integer  "right_id"
    t.string   "right_type"
    t.datetime "created_at"
    t.datetime "updated_at"
  end
end

I am trying to create a new "friendship" by making a POST to, for example, /cats/3/relationships with a route that gets left_ type and left_ id from the URL and the right_ type and right_ id from the POST parameters. The routing triggers the #create method of the friendships_ controller correctly and the resulting params look like this:

Parameters: {
  "authenticity_token"=>"gG1eh2dXTuRPfPJ5eNapeDqJu7UJ5mFC/M5gJK23MB4=", 
  "left_type"=>"cats", 
  "right_type"=>"dogs",
  "left_id"=>"3", 
  "right_id"=>"1"
}

For reasons that are outside the scope of this question I want to separate between "lefts" and "rights" in such a way that I can keep track of to whom the friendship was POSTed (hence the "lefts" and "rights"), but perhaps my model is not the right way to accomplish this? Have I misunderstood the purpose of the has_ many_ polymorphs plugin? Here is the #create action from friendships_ controller:

def create
  @friendship= Friendship.new(
    :left_type => params[:left_type],
    :left_id => params[:left_id],
    :right_type => params[:right_type],
    :right_id => params[:right_id]
  )
  respond_to do |format|
    if @friendship.save
      format.xml  { 
        render :xml => @friendship, 
        :status => :created, 
        :location => @friendship
      }
    else
      format.xml  { 
        render :xml => @friendship.errors, 
        :status => :unprocessable_entity 
      }
    end
  end
end

And finally my routes.rb:

map.resources :friendships

map.resources :cats do |cat|
  cat.resources :friendships, :path_prefix => "/:left_type/:left_id"
end

map.resources :dogs do |dog|
  dog.resources :friendships, :path_prefix => "/:left_type/:left_id"
end

Like I said, I've spent a long time on my own to try to figure this out but the documentation that exists is either outdated, too general or too advanced - or, in some cases, all three at the same time. It is not easy to get started on this RoR stuff, I can tell you! I know there are som real benefits of RoR proficiency so I will persist, but some of these headscratchers are so bad I'm starting to get a bald patch. I know there are a lot of experienced Ruby/Rails devs out there and I'm hoping someone can come out of the woodwork and explain this here thingy for us mere mortals.

Thanks in advance,

JS

A: 

I'd probably opt for a Cat and Dog model and then use a has_and_belongs_to_many association?

class Cat < ActiveRecord::Base
  has_and_belongs_to_many :dogs
end

class Dog < ActiveRecord::Base
  has_and_belongs_to_many :cats
end

Then the join table would look like this…

create_table "cats_dogs", :id => false, :force => true do |t|
  t.integer  "cat_id"
  t.integer  "dog_id"
  t.datetime "created_at"
  t.datetime "updated_at"
end

You'd be able to call cat.dogs and dog.cats to find any 'friendships.'

James Conroy-Finn
Thanks James, that sounds like a much simpler solution! But wouldn't it start to become a bit clunky when I have more than two base classes? What I were to add a third class, say Pigs, to the other two? Also, this model would not allow me to see which party was the initiator of the relationship (the "lefts" in my example above), or am I misunderstanding something?
John Schulze
A: 

A polymorphic relationship should do most of what you need although if your cats and dogs are friends with pigs and you want to know who initiated the friendship my previous solution is too simplistic.

class Animal < ActiveRecord::Base
  belongs_to :creatures, :polymorphic => true
  named_scope :cats, :conditions => {:creature_type => 'Cat'}
  named_scope :dogs, :conditions => {:creature_type => 'Dog'}
  named_scope :pigs, :conditions => {:creature_type => 'Pig'}
end

class Cat < ActiveRecord::Base
  has_many :friends, :as => :creatures
end

class Dog < ActiveRecord::Base
  has_many :friends, :as => :creatures
end

class Pig < ActiveRecord::Base
  has_many :friends, :as => :creatures
end

This may not be what you want but it would allow you to call cat.friends and cat.friends.dogs, cat.friends.pigs etc.

You could add has_many associations so you can call cat.dogs and cat.pigs directly.

class Cat < ActiveRecord::Base
  has_many :friends, :as => :creatures
  has_many :dogs, :through => :animal
  has_many :pigs, :through => :animal
end

I'm not sure if this is what you're looking for but I think it's a simpler way to go and simple is always better.

James Conroy-Finn