I am Jack's sofa, stereo and wardrobe... I make Jack's life complete. I reside in a ActiveRecord table called "things" and Jack is the only one that has the key. This is Jack's life, and it's ending one minute at a time.
As rails developers, we have done this simple relationship over and over again. I'm sure the has_many association is by far the most common in app/db design. It gives a single resource quick and easy access to others, but as your application grows, and depression sets in, we have to open up.
It's cheaper than a movie, and there's free coffee.
I am talking about groups. Not the underground ones that carve us out of wood, but ones where we share with those around us. This is healing. The time has come to for our objects to do the same, but how?
The problem is that the ActiveRecord has_many association is scoped to an individual. No matter what conditions are tacked on, they are always pwned by the proxy owner. The things they own have ended up owning you! Sure we could model some groupable schema and go through it, ActiveRecord is beautiful that way. But what about our hard work in all those existing has_many associations and named_scopes?
I felt like destroying something beautiful.
The solution is on everyone's face, it is on the tip of everyone's tongue. I just gave it a name. It is called GroupedScope and it will fundamentally change the constructed SQL for the has_many associations you want to share. The best part is that it will leave those associations untouched for continued use. GroupedScope even works with your existing association extensions and any named scopes. Let's have a session.
First we need to install the plugin.
./script/plugin install git://github.com/metaskills/grouped_scope.git
Now let's open up our Person model and schema, so Jack can share.
1 2 3 4 5 6 7 8 9 |
class PeopleCanChooseAGroup < ActiveRecord::Migration def self.up add_column :people, :group_id, :integer end def self.down remove_column :people, :group_id end end |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Person < ActiveRecord::Base has_many :things has_many :acquaintances has_many :problems grouped_scope :things, :problems end @jack = Person.find_by_name('Jack') @bob = Person.find_by_name('Bob') @jack.update_attribute :group_id, 1 @bob.update_attribute :group_id, 1 |
Every Person object in our app is now ready to share their :things and their :problems. I have also just arbitrarily put Jack and Bob into the same group. Declaring grouped_scope in the model generates a new group instance method that will allow us to iterate over its members.
1 2 |
@jack.group # => [#<Person id: 1, name: "Jack", group_id: 1>, #<Person id: 2, name: "Bob", group_id: 1>] |
We all started seeing things differently.
The object returned by the group method, an instance of GroupedScope::SelfGrouping, is far cooler than you think. It looks and acts as an enumerable array, but in reality it is a proxy object that can delegate to generated grouped association reflections which mimic their originals. Essentially giving the group access to all associated objects of its members, in this case their :things and :problems. Did I loose you?
1 2 3 4 5 6 7 |
@jack.problems.size # => 6 @bob.problems.size # => 2 @bob.group.problems # => [#<Problem...>,#<Problem...>,#<Problem...>,#<Problem...>,....] @bob.group.problems.size # => 8 @jack.group.problems.size # => 8 |
Without going into the detail of Jack's and Bob's problems, we can see that within the group, they are all shared. This is what the GroupedScope plugin is really all about. It allows existing has_many associations to be called on the group which changes the SQL generated to be owned by the group, essentially from id = 1 to id IN (1,2).
The way it accomplishes this is pretty sweet. GroupedScope creates an association reflection and accessor that will proxy all critical reflection calls back to the original association reflection. This means that it can be really smart and maintain all the logic in the existing association which could include custom options like :class_name, :foreign_key, :though, and :extend, very handy for legacy/custom associations. It also lets you chain named scopes on the grouped scope. Here is very contrived example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Person < ActiveRecord::Base has_many :mental_issues, :class_name => "MentalState", :foreign_key => :name do def dangerous find :all, :conditions => {:snap_tolerance => 10} end end grouped_scope :mental_issues end class MentalState < ActiveRecord::Base named_scope :treatable_by, lambda { |doctor| {:conditions => {:doctor_id => doctor.id}} } end @jack.group.mental_issues.dangerous.treatable_by(@doctor) # => [#<MentalState...>,#<MentalState...>] |
This is probably one of those "cry for help" things.
The GroupedScope plugin is not done. It is however well tested and can do what I have outlined in both rails 1.2.6 and 2.1.1. I have marked some TODO's in the project README but I thought I would cover a few here in detail.
First, every model that declares grouped_scope also generates a belongs_to :grouping association that can be used to get to the GroupedScope::Grouping object. This object has no underlying schema and is not being used right now. Eventually this will be the place where you write your own evil twin plugin that defines your business logic to what a group (not the selfgrouping proxy) can do. For now, it would be a good idea to generate a "groupings" table and create rows and use those IDs when assigning group_ids.
Also, you may have noticed that GroupedScope focuses on a single group and that no single owner can essentially have many groups. I think at some point I might like to see this and work like all the examples above. Have other ideas? Send me an email. I'm ken at this domain dot com.










