Forum: Ruby on Rails has_many :through and scopes: how to mutate the set of associated objects?

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Michael S. (Guest)
on 2009-05-12 04:35
(Received via mailing list)
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/
Matt J. (Guest)
on 2009-05-12 20:49
(Received via mailing list)
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.
Michael S. (Guest)
on 2009-05-12 21:17
(Received via mailing list)
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/
Matt J. (Guest)
on 2009-05-13 21:28
(Received via mailing list)
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.
Michael S. (Guest)
on 2009-05-13 22:48
(Received via mailing list)
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/
This topic is locked and can not be replied to.