Rails 3: Storing Model Metadata Attributes with ActiveRecord::Store

I recently discovered the excellent ActiveRecord store method whilst researching the best-practice for storing optional, flexible metadata against a record. store lets you keep simple key/value data into a single text column on your model.

By declaring stored attributes on your model, ActiveRecord will automatically generate the appropriate setter/accessor methods, and validations work just as you'd expect.

# db/migrate/create_cars.rb

# ...

create_table do |t|

  t.references :model

  t.references :manufacturer

  t.text :metadata # Note metadata is just a text column

  t.timestamps

end

# app/models/car.rb

class Car < ActiveRecord::Base

  belongs_to :model

  belongs_to :manufacturer

  # Manufacturer and Model are 'real', database-backed attributes

  attr_accessor :model_id, :manufacturer_id, :colour, :size, :notes, :product_url

  store :metadata, :accessors => [:colour, :size, :notes, :product_url]

  # Database-backed attributes

  validates :model, :presence, :presence => true

  validates :manufacturer, :presence => true

  # Metadata stored attributes

  validates :colour, :presence => true

  validates :size, :presence => true, :inclusion => { :in => %w(small medium large) }

  validates :product_url, :format => { :with => /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/ }

end

Note that the metadata is just a text column. Rails will automatically serialise the hash into this column.

With our Car model defined, and its metadata attributes declared with store, we can use them in just the same way as normal, database-backed attributes:

car = Car.new

car.model = CarModel.first

car.manufacturer = Manufacturer.first

car.colour = "Red"

car.size = "medium"

car.product_url = "http://www.example.com/"

car.save

# => true

car.colour

# => "red"

car.size

# => "medium"

Pros and Cons

The biggest advantage to this type is store is it provides a flexible data schema. This is perfect for storing non-indexed metadata about a model, and a use-case that often crops up when building apps. It provides a compromise between common relational databases (such as MySQL and PostgreSQL) and the flexibility of NoSQL databases such as MongoDB and CouchDB. In practical use, it means attributes can quickly be added to a model without the need to perform any migrations on the database schema.

A possible disadvantage is that stored attributes can not be indexed or used in queries (as they have no corresponding database column). For example, you could not call Car.where(:colour => "red"). However, as Garry Tan points out in his post, if this becomes a requirement in the future, you could always add a database-column to the schema at a later date, and just move the data at that time.

Helpful post? Let me know on Twitter.

References

  1. http://axonflux.com/one-of-my-favorite-additions-to-rails-3-activ