Skip to content

Instantly share code, notes, and snippets.

@rodamn
Forked from runemadsen/description.markdown
Last active September 11, 2016 21:23
Show Gist options
  • Save rodamn/7c03538381e21b37b88e to your computer and use it in GitHub Desktop.
Save rodamn/7c03538381e21b37b88e to your computer and use it in GitHub Desktop.
Reverse polymorphic associations in Rails

Rails 4 Composite Model using Polymorphism

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
WARNING: This is my attempt to brain-storm/puzzle this out. It may not (and likely does not) work out of the box. Feel free to add, patch, and comment. For now, it is untested so use this at your own peril.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

In Rails, a polymorphic association allows a model with a foreign key to switch (select) between various other models that can be its owner through a has_one or has_many relationship. At the database level, this is underpinned by storing a foreign key — an id, the same as any other foreign key — except here, the table for that foreign key is dynamic and is specified in an additional column, type.

So with this in mind, the goal here is to use rails polymorphism to achieve a compositable data model. In this data model, there should be two types of elements:

  • Atomic elements, representing individual pieces of our model, and
  • Composite elements, composed of one or more elements, atomic or composite

This pattern allows us to represent data as a fairly sophisticated tree of composite and atomic elements, where there would be a root composite element that contains other elements, both composite and atomic. The same would apply to consecutive levels of composite elements, like so:

            C  
+-------+---+---+-----+  
A       C       A     C  
  +--+--+---+-+     +-+-+  
  C     C   A A     C   A  
+-+-+ +-+-+     +-+-+-+  
A A A A   C     A A C A  
        +-+-+       |  
        A A A       A

Figure 1. A Composite graph, where C represents a composite node, and A represents some type of many atomic node types.

Achieving such a structure is not as straightforward in Rails since it is not the traditional polymorphism used by object-oriented programming languages. As explained at the start, Rails polymorphism allows a child object to attach to (or, to belong to) one of many other objects. This implementation uses this feature to establish the "owned" part of the "owner-owned" relationship between a composite element (owner) and another element (owned), be it atomic or another composite element.

To achieve this, we need a new model, OwnerOwned, that defines two belongs_to associations: one association will be a polymorphic belongs_to "pointing" to the owned/ownable object, and the other association will be a belongs_to "pointing" to the owner/owning object. Depending on the needs of the project, this might need to be a many-to-one or many-to-many association. In either case, it may be evident to the reader that OwnerOwned is actually a join table. In this case, however, this join table has polymorphic associations instead of the standard non-morphic associations.

To finish off the "owned" part of the association, each of the atomic model classes and the composite class need to set their has_one relationship to OwnerOwned. So, is that all? No.

A further complication is establishing a root node to which our composite tree will be attached. One solution might be to add a belongs_to association (and thus a foreign key) to the composite class (and database table). This is less than ideal as many of our composites will not be a root node, and thus would not assign something to this foreign key column in the database, thereby making it impossible to enforce a non-null constraint on this column. Certainly, we want to enforce a non-null constraint on the root node, but contraints are an all-rows-or-no-rows sort of affair.

The better solution is to allow the "owner" part of OwnerOwned association to also be polymorphic, with Composite being one owner, and Root being the other:

  OWNERS
+-----------+       OWNER_OWNED      OWNED
| ROOT      | <-+   +--------+       +-----------+
+-----------+   |----- owner |   +-> | ATOMIC A  |
| COMPOSITE | <-+   +--------+   |   +-----------+
+-----------+       | owned -----+-> | ATOMIC B  |
                    +--------+   |   +-----------+
                                 +-> | COMPOSITE |
                                     +-----------+

Figure 2. Owner-Owned Relationships

Composite now has two relationships to OwnerOwneds, so we need to need to differentiate these by name. To do so, we change the association name in has_one and has_many, and since the class name can no longer be inferred by convention, we specify it with class_name.

class PolymorphicCompositeMigration < ActiveRecord::Migration
create_table :composites do |t|
t.belongs_to :owned, index: true, polymorphic: true
t.belongs_to :owner, index: true, polymorphic: true
t.timestamps null: false
end
end
end
class Root < ActiveRecord::Base
has_one :owned, as: :owner, class_name: "OwnerOwned"
accepts_nested_attributes_for :owned, allow_destroy: true
end
class Composite < ActiveRecord::Base
# A section collection can be an section element (child) of another section collection
has_one :owner, as: :owned, class_name: "OwnerOwned"
# A section collection contains various section elements
has_many :owned, as: :owner, class_name: "OwnerOwned"
accepts_nested_attributes_for :owned, allow_destroy: true
end
class AtomicA < ActiveRecord::Base
has_one :owner, as: :owned, class_name: "OwnerOwned"
end
class AtomicB < ActiveRecord::Base
has_one :owner, as: :owned, class_name: "OwnerOwned"
end
class OwnerOwned < ActiveRecord::Base
belongs_to :owned, polymorphic: true
belongs_to :owner, polymorphic: true
end
# Snipper for getting strong parameters to accept recursive-tree nested attributes.
# Adapted from http://stackoverflow.com/questions/17657306/recursive-tree-like-strong-parameters
class RootsController < ApplicationController
...
def create
@root = Root.new(root_params)
...
end
...
def update
if @root.update(root_params)
...
end
end
...
private
def nested_attributes_hash(params)
# add attributes from AtomicA, AtomicB, etc... in this array
child_attributes_array = [:id, :_destroy]
nested_attributes_hash = nil
repeat = (params[:root].depth.to_f / 2.0).ceil
(1..repeat) do
attributes_array = child_attributes_array
attributes_array << nested_attributes_hash if !nested_attributes_hash.nil?
# the outer hash whitelists OwnerOwned, while the nested whitelists Owned-level objects (Atomics & Composites)
nested_attributes_hash = { owned_attributes: [:id, :_destroy, owned_attributes: attributes_array] }
end
nested_attribtues_hash
end
def root_params
# Whitelist acceptable Root-level parameters here
params.require(:root).permit(:id, nested_attributes_hash(params))
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment