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 store
d 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.