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