[PATCH] has_many :through scoped association

89 views
Skip to first unread message

Luke Redpath

unread,
Feb 27, 2009, 12:37:30 PM2/27/09
to Ruby on Rails: Core
In my current project, I have an association model that has various
flags available. To keep the implementation of these flags
encapsulated, I created named_scopes for each flag and use these
scopes for finding and creating.

This model sits between two others as the join model in a
has_many :through association. I actually wanted various
has_many :through associations that use the same join model but
different scopes. Unfortunately has_many :through doesn't have
a :scope option; I could use :conditions to get it working but I've
now broken the encapsulation I aimed for when I introduced the named
scopes and created unnecessary duplication.

Hopefully this rather contrived example should make it clear what I'm
aiming for.

http://gist.github.com/71585

And my initial implementation, currently in use in our app (tries to
extend ActiveRecord in the least-intrusive way):

https://gist.github.com/9d7f86e27014ef5df280

And now, my attempt to do it properly as a patch to ActiveRecord, with
a test.

http://gist.github.com/71587

I don't expect this patch to be completely ready for inclusion; there
are probably other things to consider such as, do you just want to
pull the :conditions from the proxy? Is there anything else to pull
in? Could this be written in a better way (probably, my knowledge if
the AR internals is slim).

Any thoughts?

Duncan Beevers

unread,
Feb 27, 2009, 1:03:10 PM2/27/09
to rubyonra...@googlegroups.com
Instead of extracting a specific piece of a named_scope's proxy
options, I would probably elect to use a more flexible,
object-oriented solution using inheritance.

http://gist.github.com/71604

Ryan Bates

unread,
Feb 27, 2009, 1:45:59 PM2/27/09
to Ruby on Rails: Core
-1 on inheritance solution. It is creative, but I imagine that can get
messy very quickly if you have multiple attributes you're trying to do
this with.

As for Luke's solution, I think it is good. But I do wonder if the
interface should be different. Before named scopes it was often
necessary to make custom associations with a :conditions hash. Now
named scopes remove this need and offer a much more flexible solution.
Going back to a custom association here with conditions
(important_tags) seems to go against the grain of scopes to me.

What if it were possible to use the :include option to include named
scopes? It might look like this:

class Product < ActiveRecord::Base
named_scope :visible, :conditions => { :visible => true }
named_scope :available, :conditions => { :available =>
true }, :include => :visible
end

This solves the problem of duplication across named scopes in one
model, but how does this address your problem? Here's where it gets
kind of cool. As you know, the :include option is also used to include
associations. So what if you could nest named scope includes through
associations?

http://gist.github.com/71618

I'm not entirely sure how complex the implementation of this would be,
but I would personally love to see this functionality. I know there
was some discussion of this on Lighthouse some time ago, but I don't
know what became of it and I cannot find it at the moment.

What do you think?

Ryan

Chris Cruft

unread,
Feb 28, 2009, 9:19:11 AM2/28/09
to Ruby on Rails: Core
I'm liking all of this goodness. I suspect that the syntax of Ryan's
solution will be more appealing to most.

Chris Cruft

unread,
Mar 27, 2009, 6:16:15 PM3/27/09
to Ruby on Rails: Core
This thread risks getting stale, which would be a shame because the
need is obvious and we've got a couple of good proposals on the
table. I took the time to more carefully review Ryan's proposed
syntax, and I'm loving it. I will try to work up an implementation
but I'll probably need someone else to forward port it from 2.2.2 to
2.3.2.

Incidentally, the LH discussion Ryan alluded to is here:
https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/11

This ticket, in turn, references an old Trac issue.

Erik Andrejko

unread,
Mar 27, 2009, 8:08:22 PM3/27/09
to Ruby on Rails: Core
There is supposed to be a rewrite in the near future of Actiive
Record's with_scope which is the underlying implementation mechanism
for named_scopes. It would probably be a good idea to create a ticket
in Lighthouse with a patch with failing tests so that they can be
addressed when the rewrite happens.

Chris Cruft

unread,
Mar 31, 2009, 12:24:41 PM3/31/09
to Ruby on Rails: Core
The named_scope macro introduces a concise syntax for a complex set of
[:order, :conditions, :joins, :include, :offset, :limit, :readonly]
options for a model class. The options for the association macros use
most of those SAME OPTIONS. My first suggestion is that association
macros accept a :scope option, which is a symbol naming a named_scope:

Replace this:
has_many :important_taggings, :class_name =>
'Tagging', :conditions => {:flag => :important}
With this:
has_many :important_taggings, :class_name => 'Tagging', :scope
=> :important

where the 'important' scope exists on the Tagging model as in Luke's
example. The scope option could also be used to replace many of the
other SQL-related options (e.g. :order, :include, :limit). And
eventually the scope could be extended to include a lambda for dynamic
construction of a scope. The essence of this suggestion is
that :scope can be used to simplify the options for association macros
AND introduce new, regular behavior (see below).

Like Luke's original proposal, I'm suggesting that the macros accept
a :scope option. Unlike Luke, I am suggesting that, in the presence
of the :through option the :scope option NOT be treated as a special
case -let it scope the target, NOT the join model. So in this
example:

has_many :important_tags, :through => :important_taggings, :source
=> :tag, :scope => :g_rated

the :g_rated scope is NOT coming from the JOIN table (Taggings) but
rather the target (Tags).

The best news is that a trivial patch achieves this functionality AND
it provides the stupendously wonderful feature of scoping the creation
of join models. Following my example above:

user.important_tags.create

will create a Tag within the :g_rated scope AND create a Tagging
within the :important scope (:flag => 'important' to use Luke's
model.) No more extensions required to accomplish the obvious and
trivial setting of the :flag attribute.

Here is the patch against 2.2.2: http://gist.github.com/88236

I'll post a monkey patch as well.

One last comment: I started out liking Ryan's syntax. But I lost my
enthusiasm when I realized that the target class would need to be
decorated with a named_scope for each of the paths that associated
with it. It just doesn't seem like good encapsulation. In the
example, is it right that the Tag model need to know about the Tagging
class' scopes? In this example, it's a bit awkward. But in other use
cases, I think the target class will get pretty ugly with scopes
referring to the myriad ways in which it might be associated. And for
the case of a utility model provided by a plugin, the decoration of
the target is relatively expensive.

Chris Cruft

unread,
Mar 31, 2009, 6:37:24 PM3/31/09
to Ruby on Rails: Core
And here is the monkey patch for getting scoped join models (and
targets):

http://gist.github.com/88448

This is a "proof of concept" patch. It only scopes has_many
associations (and has_many/has_one :through => <has_many>
indirectly). Scoping has_one should be a trivial matter of
adding :scope as a valid option. I don't think belongs_to would be
hard either. HABTM, as usual, is a _special_ case and would probably
explode on lift-off.

The second limitation is that "procedural" scopes (those that take
parameters) are not supported. I could not think of a clean syntax to
deal with the
parameters and I wasn't (yet) prepared to support a proc/lambda as the
param. As a side note, the chicken-and-egg problem of specifying
association by having each end refer to the other frustrates clean
syntax...

-Chris

Chris Cruft

unread,
Apr 22, 2009, 11:39:12 AM4/22/09
to Ruby on Rails: Core
I've expanded my monkey patch (see it here: http://gist.github.com/88448)
to the point where I wouldn't call it proof of concept anymore. It
works very nicely in practice as well. The concept of scoped
associations and composing of scopes, regardless of my implementation,
seems strong. Luke, I would love your thoughts on this approach since
it's pretty close to your original syntax.

The implementation has two major flaws/shortcomings:

1. No tests
2. No proper patch (trivial)
3. No support for procedural scopes (important, IMO)

Anybody care to take a stab?

Peter Gumeson

unread,
May 1, 2009, 1:31:03 AM5/1/09
to Ruby on Rails: Core
Beautiful! This is exactly what I was looking for. Just thought I
would give you guys some positive reinforcement about how useful this
is so that it doesn't get lost in the shuffle.

I see this version requires the scopes to be defined in the join
model? I also had the same reaction that they should be in the target
model to ease plugin development, etc. However, keeping it on the join
model may reduce duplication. Possibly it should check both models,
the target first, then the join. Just an idea.

Anyway, thanks again,
Peter

findchris

unread,
Jun 14, 2009, 9:32:04 PM6/14/09
to Ruby on Rails: Core
You might try updating the lighthouse ticket with this patch to try to
drum up support. Seems quite useful.

+1

On Apr 22, 8:39 am, Chris Cruft <c...@hapgoods.com> wrote:

Chris

unread,
Jun 15, 2009, 9:23:17 AM6/15/09
to Ruby on Rails: Core
Peter,
Not sure how I missed your message....

Anyway, my decision to reference a scope on the join model was easy:
if it's referenced on the target their is confusion/ambiguity should
you also desire a scope on the target model. For example...


class User < AR::Base
has_many :contracts
has_many :tasks, :through => :contracts

class Contract < AR::Base
belongs_to :user
has_many :tasks
named_scope :internal, :conditions => {:customer => nil}

class Task < AR:Base
named_scope :disagreeable, :conditions => {:java_content => 'high'}

What if I want to create a has_many relationship for internal-and-
disagreeable tasks? With my current approach, this is doable:

on User:
has_many :thankless_jobs, :class_name => 'Contract', :scope
=> :internal
has_many :intern_tasks, :through => :thankless_jobs, :class =>
'Task, :scope => :disagreeable

I'm not sure how you could elegantly combine the two scopes in one
macro so I decided to keep the scope for the join table 'where it
belongs'.

Chris

unread,
Jun 15, 2009, 9:31:35 AM6/15/09
to Ruby on Rails: Core
findchris,
I'm a bit reluctant to put my patch up for serious consideration
because it currently lacks tests and would rightfully get slapped down
pretty quickly.

Care to try your hand at writing the tests? I'd be willing to
patchify my solution sometime in the next two or three weeks if that
would help.

Anybody on core care to comment on the state of AR for accepting a
patch like this?

-Chris
Reply all
Reply to author
Forward
0 new messages