Michael Schuerig
2009-May-12 00:34 UTC
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 -- Michael Schuerig mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org http://www.schuerig.de/michael/
Matt Jones
2009-May-12 16:48 UTC
Re: has_many :through and scopes: how to mutate the set of associated objects?
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 On May 11, 7:34 pm, Michael Schuerig <mich...-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org> wrote:> 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 Schuerig > mailto:mich...-q5aiKMLteq5BV9CJdY2HSA@public.gmane.org://www.schuerig.de/michael/
Michael Schuerig
2009-May-12 17:16 UTC
Re: has_many :through and scopes: how to mutate the set of associated objects?
On Tuesday 12 May 2009, Matt Jones 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> On May 11, 7:34 pm, Michael Schuerig <mich...-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org> wrote: > > 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 Schuerig mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org http://www.schuerig.de/michael/
Matt Jones
2009-May-13 17:27 UTC
Re: has_many :through and scopes: how to mutate the set of associated objects?
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 On May 12, 12:16 pm, Michael Schuerig <mich...-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org> wrote:> On Tuesday 12 May 2009, Matt Jones 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 > > > > > On May 11, 7:34 pm, Michael Schuerig <mich...-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org> wrote: > > > 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 Schuerig > mailto:mich...-q5aiKMLteq5BV9CJdY2HSA@public.gmane.org://www.schuerig.de/michael/
Michael Schuerig
2009-May-13 18:47 UTC
Re: has_many :through and scopes: how to mutate the set of associated objects?
On Wednesday 13 May 2009, Matt Jones 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] } } > endThat 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 Schuerig mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org http://www.schuerig.de/michael/