has_many :through scoped association

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 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?

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

-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

I'm liking all of this goodness. I suspect that the syntax of Ryan's solution will be more appealing to most.

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.

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.

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

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

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?

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

You might try updating the lighthouse ticket with this patch to try to drum up support. Seems quite useful.

+1

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

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