Storing Arbitrary Model Attributes in Rails
Sometimes you might want to have access to an indefinite number of attributes on an object and persist these attributes to a database. Here’s how I recently implemented this in Rails.
Background
The application I was working on had an Event
class and a related
EventRegistration
class. Events
have many EventRegistrations
, linking
individual users to specific events. The event_registrations
table looked
roughly like this:
id | event_id | user_id | created_at |
---|---|---|---|
1 | 3 | 83723 | 12/26/16 |
A feature was requested by marketing: they wanted to send emails to people
based on when the user planned on arriving at a certain class of events –
Conferences. So, for every user registered for a conference Event
, we
wanted to be able to store the particular date the user planned to arrive.
We could have simply added a column to our registration table, like this:
id | event_id | user_id | arrival_date | created_at |
---|---|---|---|---|
1 | 3 | 83723 | 3/12/17 | 12/26/16 |
But this would have lead to large numbers of null
values in our database. Many
events, after all, don’t really have arrival dates:
- graduations
- birthday parties
- baseball games
- cookouts
- weddings
It also seemed clear that this kind of request could and would be made again: where some department wanted to store some arbitrary information specific to an individual, and only relevant to some events. They might want to store:
- where the individual is staying
- dietary preferences
- travel type (plane, bus, car, etc)
- whether the individual will be bringing a gift
- the name of the person’s “plus one”
- what meal option was selected
- whether and how the person RSVP’ed
And so on.
The problem, then, was how to write the code for this in a way that was both flexible and scalable.
I decided on a combination of an EAV table and Single Table Inheritance. The benefits of this approach were:
- minimal null values stored in the database
- no additional database schema changes would be needed to add attributes in the future
- specialty behavior is isolated and namespaced so that it is easy to find and understand
Now here’s the code.
Entity Attribute Value Modeling
First, the EAV implementation. I created a new table and corresponding
Metadata
model:
Step by step, this is what it is doing.
First, the database schema. It’s very simple:
Note that attribute_id
is being used here, though it is a string. There are
two reasons why:
attribute
is a reserved word in Rails- EAV tables traditionally use an
attribute_id
column to join to anattributes
table
In this case, I felt there was little value to adding an attributes table, and decided instead to just hard-code my attribute names directly into the model. I took this route for a few reasons. First, I didn’t expect for there to ever be an overwhelming number of metadata attributes. Second, I’ve run into scenarios like this before and have been burnt: scenarios in which I’ve made a basic table to store data that I need to reference throughout the code. The basic issue is this:
When you want to reference the data in the code, you only have two options:
- litter your code with the data
- keep an up-to-date, hard-coded registry of the data and use that registry as a wrapper for the data
And neither of these options is good.
An example of the first option is to do something like this:
Here, the data “arrival_date” is used to pull a line in the database. This will work, but it directly couples your implementation to the data. As soon as your data changes – e.g. as soon as you change “arrival_date” to “arrival date” in the DB – your app blows up. Or, worse still, you’ll feel compelled to never change the data in your DB because you know it will break your app. That defeats the entire purpose of having a database!
The other option, then, is to keep a registry of references to the table in your code somewhere. This allows you to do things like this:
This works well, and doesn’t tie your implementation to your data. However, it’s just duplicating effort. What’s the point of having a DB table at all if you just end up having to hard-code the whole thing in your app?
To avoid these kinds of problems, then, I just make a quick hard-coded database of the attributes I will use, which is the next bit of code we see in the model:
This creates an Attributes
class namespaced to the Metadata
model we’re
creating. I like this because it will give us a clear reference throughout our
codebase for the string values we’ll be using for attributes. This will keep our
code DRY, making it
easy to update these values in one fell swoop if need be, while also decoupling
our application logic from the data itself, which should make it more resilient
to changes in that data. It also allows us to do things like this, the next
snippet of code:
Here we are using the ALL
constant defined on our namespaced Attributes
class to dynamically define some scopes on our model. This is just for
convenience, really. But it will allow us to easily define some handy methods on
our EventRegistration
model soon enough.
Speaking of the EventRegistration
model, the next bit of code registers the SQL
join between these two tables with ActiveRecord.
And, in Rails 5+, belongs_to
gives us a presence validation on
event_registration_id
for free. The corresponding code on the
EventRegistration
model is this:
Super simple. The alias simply provides us with the convenience of calling
event_registration.metadata
instead of the more verbose
event_registration.event_registration_metadata
.
Moving on:
This ensures that we never accidentally try to create some metadata for a non-registered Attribute.
And this ensures that any given EventRegistration
never has more than value for
this attribute. (No registration should have more than one arrival date.)
Notice that this validation is conditional. There might be some attributes that we would want to allow multiple values for, e.g. “Guest Name”.
Finally, we have a custom validation:
I honestly could have done this instead:
which would have been 98% identical to what I have, except that the validation error would have been “Attribute id cannot be blank”, which is not very user-friendly. Instead, the validation errors we’ll get will look like “Arrival Date cannot be blank” – which is suitable for users to see.
That’s the EAV portion. Hopefully it’s very straightforward, boring code.
Single Table Inheritance
A new requirement was handed down: we needed to set default arrival dates for all conference attendees that were the start dates of the conferences.
To do this, I decided to make a new model that inherits from the base
EventRegistration
model and uses the same database table. This would allow me
to keep all of the specialized logic associated with conference registrations
separate and out of the way of other registrations. To do this, however, I would
need to use Single Table Inheritance (STI).
Rails makes STI really easy. The first step is just
adding a “type” column on your table, in this case event_registrations
:
Now for the model:
Notice the #arrival_date
method? Here we’re able to make use of the metadata
scopes we dynamically defined earlier to pull out the registration’s associated
arrival date.
Then, with a before_save
callback, we are able to easily add a default arrival
date for each ConferenceRegistration
instance. The benefit of this approach is
that regular EventRegistrations can be created and saved without triggering any
of this logic. We don’t even have to think about it.
Conclusion
Rails makes STI and EAV quite easy, and these two architectural patterns can be combined to powerful effect.