Data Migrations with Serialized Fields in Rails

A while ago I blogged about the different patterns available for doing data migrations in Rails. Today I am going to look at how to deal with serialized fields in data migrations.

The serialize method is used when we have an attribute that needs to be saved as an object – a good example is user preferences.

class User < ActiveRecord::Base
  serialize :preferences
end

This attribute will be serialized using YAML and saved to the database as a string. How do we handle data migrations affecting this serialized attribute, for example - what if we wanted to add or remove a value from this serialized object in a migration?

Migration Models

The easiest way to do this is to use migration models - this means we are simply using the built-in functionality to do our migration.

class AddDefaultLastLoginAtToUserTracking < ActiveRecord::Migration
  class User < ActiveRecord::Base
    serialize :tracking
  end
  
  def up
    User.find_each do |user|
      user.tracking[:last_login_at] = Time.current
      user.update_column(:tracking, user.tracking)
    end
    
    User.reset_column_information
  end
  
  def down
    User.find_each do |user|
      user.tracking.delete(:last_login_at)
      user.update_column(:tracking, user.tracking)      
    end
  
    User.reset_column_information
  end
end

Manual SQL

Sometimes migration models doesn’t really cut it though and you need to manually manipulate your SQL. In this case you need better knowledge of how ActiveRecord converts these serialized attributes to string.

By default the serialized attribute will be an instance of ActiveSupport::HashWithIndifferentAccess - you can see this by looking at the attribute’s value in the database. Alternatively you can specify a class for the serialized attribute - in this case the class instance will be used.

In order to convert your attribute to string ActiveRecord will use YAML.dump - which means we can also use this method in our data migrations.

class AddDefaultLastLoginAtToUserTracking < ActiveRecord::Migration
  class User < ActiveRecord::Base
    serialize :tracking
  end
  
  def up
    User.find_each do |user|
      tracking = user.tracking || ActiveSupport::HashWithIndifferentAccess.new
      tracking[:last_seen] = Time.current
      
      execute "UPDATE users SET tracking = #{db.quote(YAML.dump(tracking))} WHERE id = #{user.id}"
    end
    
    User.reset_column_information
  end
  
  def down
    User.find_each do |user|
      user.tracking.delete(:last_login_at)
      execute "UPDATE users SET tracking = #{db.quote(YAML.dump(user.tracking))} WHERE id = #{user.id}"      
    end
  
    User.reset_column_information
  end
  
  private
  
  def db
    ActiveRecord::Base.connection
  end
end

That’s all there is to it! Happy coding.