I merged and released the ActiveRecord compatibility branch today for has_many_polymorphs
, which represents about a 60% rewrite and a 100% code audit. Custom finders! This is huge.
new features overview
- Custom
find
-er support (:conditions
,:order
,:limit
,:offset
, and:group
). - Custom conditions (same options as above) on the
has_many_polymorphs
macro itself, which also get propogated to the individual relationships in a reasonable way. - ActiveRecord-derived
:has_many_polymorphs
reflection type, which appears in.reflect_on_all_associations
, so you can dynamically reason about the polymorphic relationship. - New extension parameters (
:extend
,:join_extend
,:parent_extend
). Toss around modules, procs, and blocks like you just don’t care. - Option support for double polymorphic relationships. Everything from single relationships, with the exception of
:parent_extend
, can be defined on one or both of the sides of the double join. -
:ignore_duplicates
option, which can be set tofalse
if you want to associate more than once (or to handle duplicates with model validations).
Rails 1.2 or greater is now required.
custom finder usage
Some examples are in order. For instance, we can sort the target records by the join record’s creation date:
zoo = Zoo.find(:first)
zoo.animals.find(:all,
:order => "animals_zoos.created_at ASC")
Or perhaps we have a decorated join model, and want to select on that, with a limit:
zoo.animals.find(:all, :limit => 2,
:conditions =>
"animals_zoos.last_cleaned IS NOT NULL")
We can even select on attributes of the targets, rather than the join, even though the targets are different models and don’t have overlapping fields. For example, we can order by the date the animals were born:
zoo.eaters.find(:all,
:order => "IFNULL(monkeys.created_at,
(IFNULL(elephants.created_at,
(IFNULL(snakes.created_at))))) ASC")
How can this work? In the polymorphic query, fields that aren’t present in a particular child table get nulled out, so you can conditionally traverse them for your query. This example is MySQL specific, but every RDBMS has a similar equivalent. You can use any custom logic on any set of fields you want. Perform math, truncate strings, whatever.
explanation of the optional parameters
There are a lot of options now, so I have made a table to explain which relationships will receive which parameters, what they mean, and what values are allowed. The general rule is that the polymorphic association receives everything, while the individual and join associations receive less, depending on how orthogonal they are to the polymorphic association.
Note that when I say “parent” and “child” I am referring to the polymorphic association’s owner class and target classes, and not to some kind of class inheritance. The owner and target could have an inheritance relationship, but it is irrelevant to this discussion.
single polymorphism parameters
key | meaning/value | affected classes |
:from |
an array of symbols for the child classes | the polymorph, and then is split to create the individual collections |
:as |
a symbol for the relationship the parent acts as in the join | the parent model, the join model |
:through |
a symbol for the join model | everything (the polymorph, the individual associations, the join associations, and the parent associations as seen from the children) |
:join_class_ name
|
the name of the join model’s class | the join model |
:foreign_key |
the column for the parent’s id in the join | the join model |
:foreign_type_ key
|
the column for the parent’s class in the join, if the parent itself is polymorphic | the join model, although usually only useful for double polymorphs |
:polymorphic_ key
|
the column for the child’s id in the join | the join model |
:polymorphic_ type_key
|
the column for the child’s class in the join | the join model |
:dependent |
:destroy , :nullify , :delete_all
|
how the join record gets treated on any associate delete (whether from the polymorph or from an individual collection); defaults to :destroy
|
:ignore_ duplicates
|
if true , will silently ignore pushing of already associated records |
the polymorph and individual associations; defaults to true
|
:rename_ individual_ collections
|
if true , all individual collections are prepended with the polymorph name, and the children’s parent collection becomes :as + “_of_ ” + polymorph ; for example, zoos_of_animals
|
the names of the individual collections everywhere |
:extend |
one or an array of mixed modules and procs | the polymorph, the individual collections |
:join_extend |
one or an array of mixed modules and procs | the join associations for both the parent and the children |
:parent_extend |
one or an array of mixed modules and procs | the parent associations as seen from the children |
:table_aliases |
a hash of ambiguous table/column names to disambiguated temporary names | the instantiation of the polymorphic query; never change this |
:select |
a string containing the SELECT portion of the polymorphic SQL query |
the polymorph only; never change this |
:conditions |
an array or string of conditions for the SQL WHERE clause |
everything |
:order |
a string for the SQL ORDER BY clause |
everything |
:group |
an array or string of conditions for the SQL GROUP BY clause |
the polymorph, the individual collections |
:limit |
an integer | the polymorph, the individual collections |
:offset |
an integer | the polymorph only |
:uniq |
if true , the records returned are passed through a pure-Ruby uniq() before they are given to you |
the polymorph; almost never useful (inherited from has_many :through ) |
Additionally, if you pass a block to the macro, it gets converted to a Proc and added to :extend
.
Phew. Actually, the only required parameter, aside from the association name, is :from
.
double polymorphism parameters
Double polymorphism allows the following general keys: :conditions
, :order
, :limit
, :offset
, :extend
, :join_extend
, :dependent
, :rename_individual_collections
. The general keys get applied to both sides of the relationship. Say you have two sides like:
acts_as_double_polymorphic_join :mammals => [...],
:amphibians => [...]
If you want to :extend
just the :amphibians
, use the key :amphibians_extend
. If you have both :extend
and :amphibians_extend
present, the :amphibians
will receive the extensions from both. Technically the general extensions take precedence, but it doesn’t matter unless you are doing something totally bizarre with module inclusion callbacks.
macro conditions devolution
Above, I mentioned that if you set a :conditions
parameter on the has_many_polymorphs
macro, those same conditions are used on the individual class collections. However, the individual collections are regular has_many :through
’s (more or less) and don’t query the superset of fields that a polymorphic SELECT
has. How does that not explode?
The plugin is your friend, here. When it builds the conditions for the individual collections, it checks which tables and columns will be still available. If your :conditions
or :order
or :group
contains fields from irrelevant target tables, the plugin will devolve them to nulls. Take this single polymorphism example:
has_many_polymorphs :animals,
:from => [:monkeys, :elephants],
:conditions => "monkeys.volume > 1 OR elephants.weight > 10"
The :conditions
string will become "NULL > 1 OR elephants.weight > 10"
when you are dealing with zoo.elephants
, but "monkeys.volume > 1 OR NULL > 10"
when you are dealing with zoo.monkeys
.
new polymorphic reflection type
Just an example:
>> Tag.reflect_on_association(:taggables)
=> #<ActiveRecord::Reflection::PolymorphicReflection:0x2
@active_record=Tag, @macro=:has_many_polymorphs,
@join_class=Tagging, @options={:extend=>[],
:join_class_name=>"Tagging", :dependent=>:destroy,
:from=>[:categories, :recipes, :tags],
:foreign_key=>"tagger_id", :join_extend=>[],
:select=>"...", :as=>:tagger, :table_aliases=>{...},
name:taggables
wishlist
Wait, what?
A few people asked if I had any wishlist or donation box set up so they could show their appreciation. So, I made an Amazon list and put some games and books on it:
Small price to pay :).
looking forward
We are moving closer and closer to supporting every possible polymorphic operation in O(1) time. The only big things left to implement are with_scope
and infinite meta-:include
(:include
-ing polymorphs that :include
other polymorphs). Also, I might roll in a tagging system generator, since people have shown so much interest in the flexible tagging that has_many_polymorphs
supports.
Exciting times.
Hi
I might miss something here but could you outline an example on how to find, lets say tagged posts of a certain tag collection.
Lets say we would like the posts tagged with tags 1 and 4.
Or is there a another way you would recommend doing this?
Cheers Mam
The way you proposed is fastest, at least until I get arbitrary
:include
up and running.Ok – thanks for the swift response. Your syntax is a bit more clean though. ;-)
You mention “
:include
-ing polymorphs that:include
other polymorphs”.I’m am trying to include just one polymorph. Is this currently working, or is that part of the
:include
feature as well?I have:
And when I do this:
I get this error:
Thanks for any help any this great plugin!
Do you have a recommended way to apply this new custom finder functionality to the tag cloud case you mentioned here?
Thanks for this great plugin!
Forest: yes, explicit use of
:include
in any way is currently not implemented.Bernie: unfortunately the way on that page is still the best. Check back in a few weeks…
Pretty neat stuff. You mentioned a decorated join model. Can you elaborate on how you would go about using a rich join model with the plugin? Thanks.
:include
doesn’t work; is there some alternative I can use until it’s fully implemented to eager load associations within the controller? The number of queries I got running during rendering is insane.First, this plugin rocks. Thank you.
Second, a question. Say we have:
In this case it looks like
has_many_polymorphs
overwrites the defaultuser.comments
finder with one that ignores the:order
parameter.Is this a bug or intended behavior? And is there an easy way to get the default
user.comments
behavior while still using HMP?Does it work with
:counter_cache
?It should, if
has_many :through
does.Macro devolution seems to take effect if :conditions refers to the parent table, though I don’t see why this should explode. The use case is a typical tagging setup where tags have an extra field language_code, and children can be tagged with separate tag lists depending on the current locale setting. Should this be handled by the polymorph conditions or extensions to the child associations? Thanks for this awesome plugin.