Serialized Attributes in ActiveRecord 3.2
Rails has had serialized attributes for a while, and every time I try to do something fancy with them, I have to rediscover how they work.
I have used composed_of
to do some of these things in the past, so take a look at it if
serialize
isn’t quite what you’re looking for.
The basics
To get started with serialize, add a text
column to your table,
and add the serialize directive to your model.
class MyModel < ActiveRecord::Base
# This serializes whatever you assign.
serialize :my_field
# This serializes a hash. If the DB value is nil, the AR value will be {}
serialize :my_field, Hash
# This serializes some class that you've written.
serialize :my_field, MyField
end
If you choose the last option, what aspects of serialization can you control?
Default serialization with YAML
By default, your attribute will be serialized as YAML. Let’s say you define MyField like this:
class MyField
end
It will be serialized like this:
--- !ruby/object:MyField {}
It will deserialize back to a MyField
instance.
Custom serialization
You can customize the serialization in a couple of different ways.
One way is to implement the instance method to_yaml
. I wouldn’t
do this, because you’ll need to do it in a way that Yaml.load
can
use to create an instance of your class.
The other way to do it is to add dump
and load
class methods to
your class. For example:
class MyField
def self.dump(obj)
# ...
end
def self.load(string)
# ...
end
end
Dump converts your object to a string, and load converts a string into an instance of your class. Here’s an example of a custom formatting of attributes:
class MyField
def self.dump(obj)
coder.dump(:a => obj.a, :b => obj.b)
end
def self.load(string)
new coder.load(string)
end
def self.coder
ActiveRecord::Coders::YAMLColumn.new(Hash)
end
def initialize(options = {})
@a = options[:a]
@b = options[:b]
end
attr_accessor :a, :b
end
Now, let’s say you want to be able to assign a hash to the serialized object, maybe from a form. You can do that:
class MyField
def self.dump(obj)
case obj
when Hash then coder.dump(obj)
else coder.dump(:a => obj.a, :b => obj.b)
end
end
def self.load(string)
new coder.load(string)
end
def self.coder
ActiveRecord::Coders::YAMLColumn.new(Hash)
end
def initialize(options = {})
@a = options[:a]
@b = options[:b]
end
attr_accessor :a, :b
end
It’s worth noting that you’ll be without the instance of MyField at first.
>>> rec = MyModel.new
>>> rec.my_field.class
=> MyField
>>> rec.my_field = {}
>>> rec.my_field.class
=> Hash
>>> rec.save
=> true
>>> rec.my_field.class
=> MyField
So, another option is to override the setter on MyModel:
class MyModel < ActiveRecord::Base
serialize :my_field, MyField
attr_accessible :my_field
def my_field= value
case value
when Hash
super MyField.new(value)
else
super
end
end
end
>>> rec = MyModel.new
>>> rec.my_field.class
=> MyField
>>> rec.my_field = {}
>>> rec.my_field.class
=> MyField
>>> rec.save
=> true
>>> rec.my_field.class
=> MyField
>>> rec.attributes = {:my_field => {}}
>>> rec.my_field.class
=> MyField