Rails 7 adds attributes_for_database to return attributes as they would be in the database


Rails provides various methods to store objects in a standardized way. One popular method is to use marshal – the marshaling library converts collections of Ruby objects into a byte stream, allowing them to be stored outside the currently active script. This is the general accepted method in the Ruby world.

However it is far less than efficient when storing ActiveRecord objects. It is unable to properly store binary attributes and is sometimes incompatible across Rails versions.

Before

Prior to this update, Rails developers had to use a hacked up solution to accurately serialize ActiveRecord objects. Let’s compare two popular methods used to do this.

First, let’s look at #attributes,

irb(main):020:0> User.first.attributes
=> {
      "id" => 1,
      "email" => "adam@example.com",
      "role" => "admin",
      "name" => "Admin"
    }

The first discrepancy is that though role is an integer column in our database the Rails enum translation is applied and the resultant JSON says role is an admin though in reality it is an integer value.

Rails provides a useful method to extract attributes as they are in the database, attributes_before_type_cast.

irb(main):01:0> User.first.attributes_before_type_cast
=> {
      "id" => 1,
      "email" => "adam@example.com",
      "role" => 2,
      "name" => "Admin"
    }

However even this does not provide a complete solution. If a record is loaded from database, attribute_before_type_cast works – but if the attribute is changed by user, the attribute method will not return mapped enum values.

After

Rails now has two related methods to overcome this. The first is attribute_for_database which consistently returns mapped value for enum. This works per attribute.

With the latest update, Rails 7 ActiveRecord attributes_for_database consistently serializes data. Now, it’s very easy to regenerate the record with instantiate.

user = User.first.attributes_for_database
=> {
      "id" => 1,
      "email" => "adam@example.com",
      "role" => 2,
      "name" => "Admin"
    }

User.instantiate(user.attributes_for_database).attributes == user.attributes
=> true