Adding custom types to your ActiveRecord models with the Attributes API
In a previous blog post I've written about value objects and their benefits. Today I want to see how we can make use of those value objects within our ActiveRecord models.
Since Rails 5, there's the Attribute API, which lets us define - surprise, surprise! - attributes. This quote from the documentation sounds promising:
This will let you use your domain objects across much of Active Record, without having to rely on implementation details or monkey patching.
The public facing part of the API is simple - there's only one method you can call, called
attribute. You give it the attribute's name and the type you want the attribute to be converted to and from (I'll explain why we're instantiating the custom type here further below):
# app/models/book.rb class Book < ActiveRecord::Base attribute :isbn, Isbn::Type.new end
The code for our
Isbn::Type class is simple. We are implementing two methods which are defined in
#serialize. While the first one is used when reading data from the database and mapping it to our ActiveRecord model, the last one works in the opposite direction and converts our value object into the format we want it to store in the database.
# app/models/isbn.rb class Isbn class Type < ActiveRecord::Type::Value def cast(value) Isbn.new(value) end def serialize(value) value.to_s end end end
As mentioned earlier I'm deviating from the documentation when specifying our custom type. The docs suggest to define types in an initializer, like so:
# config/initializers/types.rb ActiveRecord::Type.register(:isbn, Isbn::Type)
While this will work fine in production, you will face a few gotchas during development: When Rails is running in the development environment, every file within your
app directory is reloaded when you change it. This is an issue, because the initializer still references the old definition of your custom type, but will no longer find the implementation, and ActiveRecord will fall over.