has_many :through and scopes: how to mutate the set of associated objects?

I have a model layer containing Movie, Person, Role, and RoleType, making it possible to express facts such as "Clint Easterbunny is director of the movie Gran Milano".

The relevant model and associations look like this

class Movie < ActiveRecord::Base   has_many :roles, :include => :role_type, :dependent => :destroy

  has_many :participants, :through => :roles, :source => :person do     def as(role_name)       self.scoped(         :joins => 'CROSS JOIN role_types',         :conditions => [           "(roles.role_type_id = role_types.id) +           " AND role_types.name = ?",           role_name         ]       )     end   end   ... end

Querying is easy:

m = Movie.find_by_title('Gran Milano') m.participants.as('director')

However, changing relations is painful. It's already bad with has_many :through associations when the intermediate model is not completely dumb, and my scope trickery doesn't make it any better.

Now, let's assume for a moment that participants was a plain has_many association. Then it would be possible to write things like

m.participants.clear m.participants << Person.find_by_name('Steve McKing') m.participant_ids = params[:movie][:participants]

With the given has_many :through, none of these work, as Role object won't validate without a role type. Anyway, what I would like to write is

m.participants.as('actor').clear m.participants.as('actor') << Person.find_by_name('Steve McKing') m.participants.as('actor') = Person.find(params[:movie][:participants])

I'm not sure this is possible with ActiveRecord as it is, but I'm looking forward to suggestions.

Michael

Haven't tried it, but have you considered switching the association extension to a regular named_scope on Person? For simpler cases, I know that the named_scope code is smart enough to use the scope conditions to instantiate objects. Not sure if it will work here...

--Matt Jones

Haven't tried it, but have you considered switching the association extension to a regular named_scope on Person? For simpler cases, I know that the named_scope code is smart enough to use the scope conditions to instantiate objects. Not sure if it will work here...

If I understand you correctly, I've already considered that case.

class Person < ActiveRecord::Base   named_scope :actors, ... # add a condition picking out the actors end

Then, with

movie.participants.actors

I'd get the people who are participating in movie and who are actors. However, what I want are the people participating in movie *as* actors.

It might be possible to get this to work as intended, but I tend to think it's not. Dealing with the joins involved is already tricky. ActiveRecord (almost) doesn't have an abstract model of queries, it more or less concatenates strings. There's no support for expressing, on the one hand, that a specific join is needed (without duplicating it), and on the other, that you want another, independent join.

Michael

Try it in a named_scope, thus:

class Person < AR::Base   named_scope :as, lambda { |role_name| { :joins => 'CROSS JOIN role_types', :conditions => ["(roles.role_type_id = role_types.id) AND role_types.name = ?", role_name] } } end

But I'm *almost* positive that that still won't be able to trigger the named_scope :create_scope magic. The other thought would be to return a custom subclass of AssociationCollection from your 'as' association extension. You'd probably need to override a few methods (<< especially) to take into account the source of the request (the argument passed to as).

--Matt Jones

Try it in a named_scope, thus:

class Person < AR::Base   named_scope :as, lambda { |role_name| { :joins => 'CROSS JOIN role_types', :conditions => ["(roles.role_type_id = role_types.id) AND role_types.name = ?", role_name] } } end

That works for queries, but AFAICT it is equivalent to what I'm already doing. The drawback is that

Person.as('actor') doesn't work because the necessary join with roles is missing. If I add that, movie.participants.as('actor') blows up because there the added join is a duplicate.

But I'm *almost* positive that that still won't be able to trigger the named_scope :create_scope magic.

No, it won't, simply because ActiveRecord has no way to figure out what additional parameters to use to build the through model (Role).

As I'm staring at this stuff for some time now, I'm still surprised that there doesn't seem to be a fairly generic way to add elements to a has_many :through association with a non-trivial through-model. For me, the point of has_many :through, as opposed to habtm, is that the intervening model carries some weight apart from relating two other models with each other.

The other thought would be to return a custom subclass of AssociationCollection from your 'as' association extension. You'd probably need to override a few methods (<< especially) to take into account the source of the request (the argument passed to as).

I was thinking of returning a subclass of AR::NamedScope::Scope. I can't say which would be better.

Michael