directed double-polymorphic associations

In a frenzy of Bluetooth-enabled coding from the local diner, I have implemented double polymorphic associations in the has_many_polymorphs plugin. This lets you create a multi-class directed graph in ActiveRecord with great ease.

show, don’t tell

For example, say we wanted to model glue. We want to be able to glue PopsicleSticks and Leaves to themselves, but also to Paper. We will make a join model called Adhesion for this:

class Adhesion < ActiveRecord::Base
  belongs_to :glue_receiver, :polymorphic => true
  belongs_to :glue_applier, :polymorphic => true

  acts_as_double_polymorphic_join :glue_receivers =>
        [:popsicle_sticks, :leaves, :papers],
                                  :glue_appliers =>
        [:popsicle_sticks, :leaves]

This means that a PopsicleStick will have a glue_receivers polymorphic collection that can accept anything in the :glue_receivers list, above. A Paper, though, will not have a .glue_receivers method. It will have a .glue_appliers method, but it is not itself a :glue_applier. This is tricky. Just remember that the lists in the acts_as call are the types of children for that relationship, not the types of parents.

Also, using the individual typed collections (e.g. PopsicleStick#leaves) will always get you the children that belong to the record, instead of the parents that own the record. In this case it will fetch the Leaves from a PopsicleStick object’s .glue_receivers.

warning! class-reloading laser in use!

The API is a little different because there is no parent class that can act as a controller. Things are instead centralized in the join model.

This means that you have to reference the join class (either with load(), by evaluating its bare class name, or another way) before the associations will be injected into the “real” classes. Rails’ selective reloading in the development environment can lead to trouble—a modified class will get reloaded and forget its relationship to the join. Keep this in mind.

crazy clown collections

Combining this with the other plugin features leads to controlled chaos. Self-referential directed double-polymorphic namespaced STI associations are (at least according to my tests) fully supported.

Please try it out and let me know if you have trouble. I really appreciate the filing of bugs.

4 responses

  1. How would you do the following: I started with Categories habtm Products in a catalogue website where Categories acts_as_nested_set (drill down navigation). But then I thought it would be nice if each category and product could also have several Tabs of related information (tabular navigation, moving across instead of drilling down). Hence if a category or product needs more explanation, we can add a Tab to the Category or Product model. The tabs themselves can contain lists of related products or categories, or other nested tabs….

    Is your plugin appropriate for this scenario? Not quite sure how to define the associations yet. Seems to me it would be something along the lines of:

    Category has many children categories (through acts_as_nested_set), has many products, optionally has many tabs, and could belong to many tabs.

    Product has and belongs to many categories, can optionally have and belong to many tabs.

    Tabs can have and belong to Categories, Products or Tabs (in case we want to have nested tabnav inside a tab)

    Is this even possible? Or would it cause some recursive association blow-up?

  2. This is quite possible.

    class Tab < ActiveRecord::Base
      has_many_polymorphs :items,
           :from => [:tabs, :categories, :products],
           :as => :parent_tab
    class ItemsParentTab < ActiveRecord::Base
      belongs_to :parent_tab
      belongs_to :item, :polymorphic => true

    (More or less.)

    This will give your Categories, Products, and Tabs all a collection parent_tabs (for upwards navigation) and a collection tabs (for downwards navigation), and each Tab can contain any number of other items.

    You may want to rethink such a rigid organization strategy though. Why not just relate products and categories to other products and categories, and build the tabs on the fly?