Noel Walters
Noel Walters

Reputation: 1853

how to access rails join model attributes when using has_many :through

I have a data model something like this:

# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
  self.primary_key = 'collection_item_id'
  belongs_to :collection
  belongs_to :item
end

class Item < ActiveRecord::Base
  has_many :collection_items
  has_many :collections, :through => :collection_items, :source => :collection
end

class Collection < ActiveRecord::Base
  has_many :collection_items, :order => :position
  has_many :items, :through => :collection_items, :source => :item, :order => :position
end

An Item can appear in multiple collections and also more than once in the same collection at different positions.

I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.

def helper_method( collection_id )
  colls = Collection.find :all
  colls.each do |coll|
    coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
      do_something_with( item.collection_item_id )
    end
  end
end

I tried this as well but it also fails with ( undefined method `collection_item' )

do_something_with( item.collection_item.collection_item_id )

Edit: thanks to serioys sam for pointing out that the above is obviously wrong

I have also tried to access other attributes in the join model, like this:

do_something_with( item.position )

and:

do_something_with( item.collection_item.position )

Edit: thanks to serioys sam for pointing out that the above is obviously wrong

but they also fail.

Can anyone advise me how to proceed with this?

Edit: -------------------->

I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.

Currently I am working on amending my Collection model like this:

class Collection < ActiveRecord::Base
  has_many :collection_items, :order => :position, :include => :item
  ...
end

and changing the helper to use coll.collection_items instead of coll.items

Edit: -------------------->

I've changed my helper to work as above and it works fine - (thankyou sam)

It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.

Upvotes: 5

Views: 13326

Answers (3)

Tyler Rick
Tyler Rick

Reputation: 9491

I was able to get this working for one of my models:

class Group < ActiveRecord::Base
  has_many :users, :through => :memberships, :source => :user do
    def with_join
      proxy_target.map do |user|
        proxy_owner = proxy_owner()
        user.metaclass.send(:define_method, :membership) do
          memberships.detect {|_| _.group == proxy_owner}
        end
        user
      end
    end
  end
end

In your case, something like this should work (haven't tested):

class Collection < ActiveRecord::Base
  has_many :collection_items, :order => :position
  has_many :items, :through => :collection_items, :source => :item, :order => :position do
    def with_join
      proxy_target.map do |items|
        proxy_owner = proxy_owner()
        item.metaclass.send(:define_method, :join) do
          collection_items.detect {|_| _.collection == proxy_owner}
        end
        item
      end
    end
  end
end

Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):

def helper_method( collection_id )
  colls = Collection.find :all
  colls.each do |coll|
    coll.items.with_join.each do |item|
      do_something_with( item.join.collection_item_id )
    end
  end
end

Here is a more general solution that you can use to add this behavior to any has_many :through association: http://github.com/TylerRick/has_many_through_with_join_model

class Collection < ActiveRecord::Base
  has_many :collection_items, :order => :position
  has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end

Upvotes: 0

Antti Tarvainen
Antti Tarvainen

Reputation: 1219

do_something_with( item.collection_item_id )

This fails because item does not have a collection_item_id member.

do_something_with( item.collection_item.collection_item_id )

This fails because item does not have a collection_item member.

Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:

colls = Collection.find :all
colls.each do |coll|
  coll.collection_items.each do |collection_item|
    do_something_with( collection_item.id )
  end
end

A couple of other pieces of advice:

  • Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
  • You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.

I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.

I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.

Upvotes: 3

serioys sam
serioys sam

Reputation: 2051

In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.

Upvotes: 3

Related Questions