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


#1

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


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#2

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 J.


#3

On Tuesday 12 May 2009, Matt J. wrote:

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

has_many :participants, :through => :roles, :source => :person do
end

m.participant_ids = params[:movie][:participants]
I’m not sure this is possible with ActiveRecord as it is, but I’m
looking forward to suggestions.

Michael


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/


#4

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 J.


#5

On Wednesday 13 May 2009, Matt J. wrote:

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


Michael S.
mailto:removed_email_address@domain.invalid
http://www.schuerig.de/michael/