When it comes to associations in Rails we are all familiar with the traditional directives such as has_many, belongs_to etc.
But sometimes creating a whole table for an associated model feels like overkill because that associated model will only be used in the context of its parent. What you would like is: be able to embed that association within the parent the same way it can be done with NoSQL databases.
The above becomes even more relevant when the association is actually an ordered list of objects which is always saved as a whole.
Fortunately Rails supports storing attributes as JSON and offers a convenient attributes API which can be leveraged to create nested associations.
The Rails attributes API
The attributes API was shipped in Rails 5.
It allows you to define custom types for database fields such as:
The example above uses a prebuilt type but custom types can also be defined for more complex, object-oriented serialization/deserialization.
Let's see how to do this with a concrete example.
Book and Chapters (first version)
Let's assume we want to create Book model and associate to each book a list of chapters. I'll be assuming we're using Postgres as a database. The same can be done with MySQL.
The Book model
Let's start by creating the migration:
Then let's setup the Book ActiveRecord model:
The model uses the attribute directive to specify that the chapters will be of type Chapter::ListType.
This is the only thing we need to do in the parent model. All the nesting logic will actually be done in the Chapter model.
The Chapter model
The Chapter model will be implemented as a plain ActiveModel::Model. There is no need for ActiveRecord as the list of chapters will be saved on the parent model.
There are three things we need to define on this model:
- Serializable attributes: required to know what to save in database
- Equality operator: required to detect changes on the list of chapters
- Type interface: how to serialize/deserialize our set of chapters
The Chapter model looks like this:
That's all we need to do to define a simple nested association.
Testing our new association
Let's open a Rails console and test our new association. The chapters attribute behaves exactly like any other ActiveRecord attribute.
Alright it works but this implementation is a bit of a one-off. Let's see how we can generalise the implementation.
Book and Chapters (refactored)
It feels a bit overkill to define a nested class called ListType on all the models we wish to embed. From one model to another the only parameter that will change is the actual model class.
Let's extract that nested type class into a dedicated class and parameterize it:
Now let's remove the old code from the Chapter model:
Finally, let's change the attribute declaration in our Book model:
That's all we need. With that refactored approach you can reuse this nested association pattern on various models within your app. All you need to do on associated models is define the attributes and equality methods.
Keypup is on a mission to help developers and tech leads work better with each others on development projects. Our platform automatically centralizes, prioritizes and assigns people and actions on issues and pull requests to optimize your development flow.
Don't get lost because you have to juggle with twenty pull requests across five development projects. We'll clean and organize that for you to ensure a smooth landing.