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):

1
2
3
4
# 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 ActiveRecord::Type::Value: #cast and #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.

1
2
3
4
5
6
7
8
9
10
11
12
# 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

Gotchas

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:

1
2
# 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.