Alex Wayne
Alex Wayne

Reputation: 187272

Rails polymorphic many to many association

I'm trying setup a generic sort of web of related objects. Let say I have 4 models.

I would like to able to do:

book = Book.find(1)
book.relations << Tag.find(2)
book.relations << Category.find(3)
book.relations #=> [Tag#2, Category#3]

movie = Movie.find(4)
movie.relations << book
movie.relations << Tag.find(5)
movie.relations #=> [Book#1, Tag#5]

Basically I want to be able to take any 2 objects of any model class (or model class that I allow) and declare that they are related.

Obviously I don't want to create a huge mess of join tables. This seems like it's not quite a has many through association, and not quite a polymorphic association.

Is this something that Rails can support via it's association declarations or should I be rolling my own logic here?

Upvotes: 3

Views: 3626

Answers (4)

ErsatzRyan
ErsatzRyan

Reputation: 3043

depending on how closesly related your movies/books db tables are

what if you declared

class Items < ActiveRecord::Base
  has_many :tags
  has_many :categories
  has_and_belongs_to_many :related_items,
       :class => "Items",
       :join_table => :related_items,
       :foreign_key => "item_id",
       :associated_foreign_key => "related_item_id"
end

class Books < Items
class Movies < Items

make sure you put type in your items table

Upvotes: 0

austinfromboston
austinfromboston

Reputation: 3780

Support for polymorphism has improved dramatically since the early days. You should be able to achieve this in Rails 2.3 by using a single join table for all your models -- a Relation model.

class Relation
  belongs_to :owner, :polymorphic => true
  belongs_to :child_item, :polymorphic => true
end

class Book
  has_many :pwned_relations, :as => :owner, :class_name => 'Relation'
  has_many :pwning_relations, :as => :child_item, :class_name => 'Relation'

  # and so on for each type of relation
  has_many :pwned_movies, :through => :pwned_relations, 
           :source => :child_item, :source_type => 'Movie'
  has_many :pwning_movies, :through => :pwning_relations, 
           :source => :owner, :source_type => 'Movie'
end

A drawback of this kind of data structure is that you are forced to create two different roles for what may be an equal pairing. If I want to see all the related movies for my Book, I have to add the sets together:

( pwned_movies + pwning_movies ).uniq

A common example of this problem is the "friend" relationship in social networking apps. One solution used by Insoshi, among others, is to register an after_create callback on the join model ( Relation, in this case ), which creates the inverse relationship. An after_destroy callback would be similarly necessary, but in this way at the cost of some additional DB storage you can be confident that you will get all your related movies in a single DB query.

class Relation
  after_create do 
    unless Relation.first :conditions => 
      [ 'owner_id = ? and owner_type = ? and child_item_id = ? and child_item_type = ?',       child_item_id, child_item_type, owner_id, owner_type ]
      Relation.create :owner => child_item, :child_item => owner
    end
  end
end

Upvotes: 7

Alex Wayne
Alex Wayne

Reputation: 187272

I have come up with a bit of solution. I'm not sure it's the best however. It seems you cannot have a polymorphic has_many through.

So, I fake it a bit. But it means giving up the association proxy magic that I love so much, and that makes me sad. In a basic state, here is how it works.

book = Book.find(1)
book.add_related(Tag.find(2))
book.add_related(Category.find(3))
book.related        #=> [Tag#2, Category#3]
book.related(:tags) #=> [Tag#2]

I wrapped it up in a reusable module, that can be added to any model class with a single has_relations class method.

http://gist.github.com/123966

I really hope I don;t have to completely re-implement the association proxy to work with this though.

Upvotes: 3

user65663
user65663

Reputation:

I think the only way to do it exactly as you described is the join tables. It's not so bad though, just 6, and you can pretty much set-and-forget them.

Upvotes: 0

Related Questions