views:

2438

answers:

3

I have a data model something like this:

class CollectionItem < ActiveRecord::Base

  # the collection_items table has columns collection_item_id, collection_id, item_id, position, etc
  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.

+3  A: 

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.

serioys sam
Thanks for the useful link. and for pointing out that "item.collection_item.whatever" is obviously wrong. I'll amend my question to reflect that. The gist of my questions is: (how) can I get at the join table attributes via the coll.items array ?
Noel Walters
+2  A: 
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.

Antti Tarvainen
Thanks - If I could accept two answers I would - I did read the documentation for has_many :through and now that I know the answer it seems obvious - too many late nights!
Noel Walters
A: 

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
Tyler Rick