Value objects in Ruby: Serializing your custom objects with ActiveRecord

Sep 03, 2015 Christopher Moeller

In the last post I discussed the value in moving beyond Ruby's built-in data types like Strings, Arrays, and Hashes in favor of using custom data types (or value objects). There are many benefits in designing a system with custom types, one of the greatest being that your intent is the code itself instead of in documentation or a wiki. Check out part 1 if you haven't had a chance yet. This post will show you how you can use your custom data types with ActiveRecord.

Introduction

Most web applications we work on require storing data in a database. Like many Rails shops our default database of choice at Grok is Postgres. We can store the vast majority of data using its text types, numeric types, dates and times, and booleans. Postgres also provides support for collections with arrays and hstore as well as more specific types like UUIDs and IP addresses, allowing us to store that data in a more structured way instead of as text. This is how Postgres describes using the Network Address Types:

It is better to use these types instead of plain text types to store network addresses, because these types offer input error checking and specialized operators and functions.

So, we get database-level validation simply by using a data type designed for the data we're going to store in it anyway, making it impossible to store the strings like "coffee is delicious" in a column designed for just IP addresses. Brilliant!

But what if the database doesn't have a specific type for the data we need to store? We have ActiveRecord::Validations , of course! Now, how do we validate data if someone opens a database console and inserts records, or someone needs to work quickly and calls model.save(validate: false) ? More recently people have been using constraints to push validations down to the database level, which is very intriguing but seems like it may be duplicating work.

You'd still want application-level validations to avoid a round-trip to the database just for validations. And keeping things like that in sync can be a nightmare. (Please let me know if you have a solution to that problem!) Though they aren't the preferred way to interact with the database, adding records using the database console or calling model.save(validate: false) aren't unheard of or too far out of the ordinary. To ensure we're working with known data we need something that fits in the middle of ActiveRecord::Validations and database-level validations. Enter ActiveRecord::serialize .

ActiveRecord#serialize with built in Ruby classes

To fill this need ActiveRecord offers the serialize class macro. For example, say you have a User class and the users table has a text column, for preferences . The intent with the preferences column is to store a collection of key-value pairs for a each user's preferences. Instead of manually parsing the stored text using String methods, ActiveRecord will coerce it into a Hash for you. Granted, in Postgres we would normally use hstore for this but there are other reasons to use serialize in this way, such as if you're using MySQL. Here's what the model would look like, just a one-line change:

class User < ActiveRecord :: Base serialize :preferences , Hash end

Now when we instantiate a new user and call preferences on that user, we will get back a hash. It also works for assignment. Let's take a look at what this looks like in the console:

$ bin/rails c Loading development environment ( Rails 4.2.3 ) irb(main):001:0> user = User.new => #<User id: nil, name: nil, email: nil, preferences: {}, created_at: nil, updated_at: nil> irb(main):002:0> user.preferences = { language: "ruby" , city: "San Antonio" } => { :language = > "ruby" , :city = > "San Antonio" } irb(main):003:0> user.save ( 0.5ms ) BEGIN SQL ( 5.8ms ) INSERT INTO "users" ( "preferences" , "created_at" , "updated_at" ) VALUES ( $1 , $2 , $3 ) RETURNING "id" [[ "preferences" , "---

:language: ruby

:city: San Antonio

" ] , [ "created_at" , "2015-08-17 23:10:40.429329" ] , [ "updated_at" , "2015-08-17 23:10:40.429329" ]] ( 62.6ms ) COMMIT => true irb(main):003:0> exit

As demonstrated above, to set a user's preferences, we use a Hash . ActiveRecord also serializes the text into a Hash when we pull the record out of the database.

That's the basics around ActiveRecord#serialize , and there are pretty big wins with it. The biggest wins come, though, when we write our own serializers using value objects.

Using a custom EmailAddress class with ActiveRecord#serialize

In the last post we created a basic EmailAddress class that included some basic validation and comparison. Here's the same class again after some refactoring:

require "forwardable" class EmailAddress include Comparable extend Forwardable def initialize ( string ) if string =~ /\A\z|@/ @raw_email_address = string . downcase . strip else raise ArgumentError , "email address must have an '@'" end end delegate [ :to_s , :<=> ] => :raw_email_address protected attr_reader :raw_email_address end

I'm leaving out the tests for the sake of simplicity but you can check them out in the GitHub repo.

An EmailAddress gets initialized with a String, performs some basic validation (the string can either be blank or must contain an "@"), and can be compared against other EmailAddresses and Strings. ActiveRecord requires 2 class methods to serialize an attribute with our custom data type: self.load and self.dump . .load takes the value as it will be stored in the database and returns an object of the type. .dump takes an object of the type and returns the value to be stored in the database. Let's add those methods to our EmailAddress class:

require "forwardable" class EmailAddress # ... def self . load ( raw_string ) new ( raw_string || "" ) end def self . dump ( email_address ) if ! email_address . empty? email_address . to_s end end # ... end

I'm using the || operator in .load to handle the case when the stored value is nil or when calling User.new (which will also be nil ). .dump is also handling nil checks for us: only store non-empty values or nil . As far as email addresses are concerned, this is the only place we need to account for nil in our system — we'll always have a valid EmailAddress type.

Using our custom class is the same as using the built-in, all we need to do is require it:

require "email_address" class User < ActiveRecord :: Base serialize :preferences , Hash serialize :email , EmailAddress end

Now let's see how this works in the console:

$ bin/rails c Loading development environment ( Rails 4.2.3 ) irb(main):001:0> User.first User Load ( 1.3ms ) SELECT "users" . * FROM "users" ORDER BY "users" . "id" ASC LIMIT 1 => #<User id: 1, name: nil, email: #<EmailAddress:0x007f8edfe4de80 @raw_email_address="">, preferences: {:language=>"ruby", :city=>"San Antonio"}, created_at: "2015-08-17 23:10:40", updated_at: "2015-08-19 19:14:52"> irb(main):002:0> user = User.new => #<User id: nil, name: nil, email: #<EmailAddress:0x007fefa4e551f0 @raw_email_address="">, preferences: {}, created_at: nil, updated_at: nil> irb(main):003:0> user.email = "hello@example.com" => "hello@example.com" irb(main):004:0> user => #<User id: nil, name: nil, email: #<EmailAddress:0x007fefa4e5d058 @raw_email_address="hello@example.com">, preferences: {}, created_at: nil, updated_at: nil> irb(main):005:0> user.save ( 0.2ms ) BEGIN SQL ( 0.6ms ) INSERT INTO "users" ( "email" , "created_at" , "updated_at" ) VALUES ( $1 , $2 , $3 ) RETURNING "id" [[ "email" , "hello@example.com" ] , [ "created_at" , "2015-08-21 19:40:37.639952" ] , [ "updated_at" , "2015-08-21 19:40:37.639952" ]] ( 2.5ms ) COMMIT => true irb(main):006:0> user.reload User Load ( 0.5ms ) SELECT "users" . * FROM "users" WHERE "users" . "id" = $1 LIMIT 1 [[ "id" , 4]] => #<User id: 2, name: nil, email: #<EmailAddress:0x007fefa49ba668 @raw_email_address="hello@example.com">, preferences: {}, created_at: "2015-08-21 19:40:37", updated_at: "2015-08-21 19:40:37"> irb(main):007:0> user.email == "hello@example.com" => true irb(main):008:0> User.find_by ( email: "hello@example.com" ) User Load ( 1.8ms ) SELECT "users" . * FROM "users" WHERE "users" . "email" = $1 LIMIT 1 [[ "email" , "hello@example.com" ]] => #<User id: 2, name: nil, email: #<EmailAddress:0x007fefa4ecc840 @raw_email_address="hello@example.com">, preferences: {}, created_at: "2015-08-21 19:40:37", updated_at: "2015-08-21 19:40:37"> irb(main):009:0> User.find_by ( email: EmailAddress.new ( "hello@example.com" )) User Load ( 0.5ms ) SELECT "users" . * FROM "users" WHERE "users" . "email" = $1 LIMIT 1 [[ "email" , "hello@example.com" ]] => #<User id: 2, name: nil, email: #<EmailAddress:0x007fefa4ed6458 @raw_email_address="hello@example.com">, preferences: {}, created_at: "2015-08-21 19:40:37", updated_at: "2015-08-21 19:40:37"> irb(main):010:0> exit

As you can see from the above console session, we can use all the same ActiveRecord methods we're accustomed to using in everyday Rails: attr= , find_by , etc. We don't lose anything there. But there are real productivity gains such as never having to deal with nil when it comes to a user's email.

Conclusion

We've come a long way. We can serialize and deserialize a User's email address with a custom class that can compare email addresses without downcase being littered all over the codebase. We know we'll always have a valid email address no matter where we are in the application.