ActiveRecord

Serialization

Disclaimer

  • I will not talk about how to serialize your models
  • But how some attributes on your models can be stored as serialized data

Why ?

AKA "screw 1NF"

Because

  • I don't need to query it
  • It's NULL most of the time
  • Changing schema is problematic (MySQL...)
  • I need to store an array of values
  • I need to easily store a snapshot of a complex object
  • The data is very inconsistent
  • We're all consenting adults here

serialize

Since Rails 0.x

class Shop < ActiveRecord::Base
  serialize :settings
end
>> shop.settings = {language: 'en'}

Serialize anything to YAML by default

serialize

class Shop < ActiveRecord::Base
  serialize :settings, Settings
end

A class can be specified to ensure that only expected types are serialized

serialize

class Settings
  def self.dump(settings)
    MultiJson.dump(settings.to_hash)
  end

  def self.load(string)
    new(MultiJson.load(string))
  end 
end

If both load and dump are implemented, the class is responsible of his own serialization

serialize - Good for

  • Complex objects snapshots
  • Blind data dumps

serialize - Drawbacks

  • Partial updates may be complicated
  • Data have to be validated an sanitized manually
  • Can be null
  • Reserialized on every save.
  • May be fixed for rails 4.1, see rails/rails/#8328

composed_of

Since Rails 0.x (killed and then resurrected in 4.0)

class Shop < ActiveRecord::Base
  composed_of :address, mapping: [%w(address_street street), %w(address_city city)]
end

class Address
  def intialize(street, city)
    @street, @city = street, city
  end
end
>> shop.address_street
=> '126 York Street'
>> shop.address
=> #<Address:0x007fc584725c08 @street="126 York Street", @city="Ottawa">

Build a plain ruby object from a mapping of attributes

composed_of - Good for

  • Still being able to query using pure SQL
  • Getting rid of an has_one association and thus a query or join
  • Moving logic into another object without schema change

composed_of - Drawbacks

  • Still require a schema change when adding fields
  • The object have to be immutable

store

Since Rails 3.2

class Shop < ActiveRecord::Base
  store :settings
end
>> shop.settings[:language] = 'en'

Always serialize a Hash (in YAML by default)

store

class Shop < ActiveRecord::Base
  store :settings, accessors: [:language]
end
>> shop.language = 'en'

Allow to define accessors

store

Since Rails 4.0

class Shop < ActiveRecord::Base
  store :settings, coder: MultiJson
end

Can change the serialization method

store - Good for

  • Key/Value store
  • Schema description
  • Is always a hash so no need to worry about null column
  • Indifferent access
  • Can use standard validators

store - Drawbacks

  • Reserialized on every save. (Backed up by serialize)
  • Accessors are not real attributes (no dirty tracking, no query)
  • No guarantee on values type ('1' instead of true)

typed_store

Compatible with Rails >= 3.2

class Shop < ActiveRecord::Base
  typed_store :settings do |s|
    s.string :language
  end
end
>> shop.language = 'en'

Same behavior than store

typed_store

class Shop < ActiveRecord::Base
  typed_store :settings do |s|
    s.integer :drinking_age
  end
end
>> shop.drinking_age = '18'
>> shop.drinking_age
=> 18

But type can be specified

typed_store

Supported types

  • string
  • integer
  • float
  • boolean
  • date
  • datetime
  • any

typed_store

class Shop < ActiveRecord::Base
  typed_store :settings do |s|
    s.integer :drinking_age, default: 21
  end
end
>> Shop.new.drinking_age
=> 21

Just like default value

typed_store

class Shop < ActiveRecord::Base
  typed_store :settings do |s|
    s.integer :drinking_age, default: 21, null: false
  end
end
>> shop.drinking_age = nil
>> shop.drinking_age
=> 21

And null can be prevented

typed_store

class Shop < ActiveRecord::Base
  typed_store :settings do |s|
    s.string :language, default: 'en', blank: false
  end
end
>> shop.language = '  '
>> shop.language
=> 'en'

And blank too

typed_store

class Shop < ActiveRecord::Base
  typed_store :settings do |s|
    s.string :tags, array: true, default: [], null: false
  end
end
>> shop.tags = ['ruby', 'web']

You can even store an array of values

typed_store

>> shop.dinking_age?
=> true
>> shop.dinking_age_changed?
=> false
>> shop.dinking_age = 18
>> shop.dinking_age_changed?
=> true
>> shop.dinking_age_was
=> 21

Accessors are plain attributes

store - Good for

  • Same things as store
  • Data consistency
  • Type casting rules and attribute behavior are exactly the same as a for real database columns
  • If not, please fill an issue

store - Drawbacks

  • Reserialized on every save. (Backed up by store)

Any Question ?

Jean Boussier

Shopify

https://github.com/byroot

https://github.com/byroot/activerecord-typedstore