Embedded associations in Rails using JSON fields

Arnaud Lachaume
Arnaud LachaumeLink to author's LinkedIn profile
June 8, 2021
icon timer

Leverage JSON fields and Rails attributes API to create embedded associations.

Table of Content

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.

Sign-up and accelerate your engineering organization today !

About us

Keypup's SaaS solution allows engineering teams and all software development stakeholders to gain a better understanding of their engineering efforts by combining real-time insights from their development and project management platforms. The solution integrates multiple data sources into a unified database along with a user-friendly dashboard and insights builder interface. Keypup users can customize tried-and-true templates or create their own reports, insights and dashboards to get a full picture of their development operations at a glance, tailored to their specific needs.


Code snippets hosted with ❤ by GitHub
Banner designed by starline / Freepik