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.