description
The Property gem lets you define “properties” for your models. Properties behave like normal rails attributes with the following differences:
what you gain
- they should be faster (less type casting)
- you can add properties to sub-classes without affecting the parent class
- you can dynamically define properties in model instances when needed (virtual classes)
what you loose
- you cannot use them in queries (at least for now)
- you could directly index the json data, but that might not be optimal
wraps model properties into a single database column and declare properties from within the model.
This class is 100% compatible with ActiveRecord. This means that if you have been using a model with class attributes, you should be able migrate the data and copy paste the table definition from the schema into your model and it should work as before, except for sql queries of course and self['foo'] accessors.
source code
The code is on github
Usage
Install with:
$ sudo gem install property
You then need to create a migration to add a ‘text’ field named ‘properties’ to
your model. Something like this:
class AddPropertyToContact < ActiveRecord::Migration
def self.up
add_column :contacts, :properties, :text
end
def self.down
remove_column :contacts, :properties
end
end
Once your database is ready, you need to declare the property columns:
class Contact < ActiveRecord::Base
include Property
property do |p|
p.string 'first_name', 'name', 'phone'
p.datetime 'contacted_at', :default => Proc.new {Time.now}
p.string 'encoding', :default => :get_encoding
end
end
You can now read property values with:
@contact.prop['first_name']
@contact.first_name
And set them with:
@contact.update_attributes('first_name' => 'Mahatma')
@contact.prop['name'] = 'Gandhi'
@contact.name = 'Gandhi'
If you want to read as fast as possible, prop['attribute'] is your friend.
integration with versioning
It’s nice to have properties, but it would be even nicer if we could easily enable versioning. This is very easy: you just need to specify the name of a method to reach the model that will hold the properties storage. Here is an example that uses the brand new Versions gem:
class Contact < ActiveRecord::Base
include Versions::Multi
has_multiple :versions
include Property
store_properties_in :version
property do |t|
t.string 'name'
t.string 'first_name'
t.integer 'age'
t.datetime 'seen_at', :default => Proc.new { Time.now }
end
end
Your Version model would look like this:
class Version < ActiveRecord::Base
include Versions::Auto
def should_clone?
true # always clone on update
end
end
And that’s it. You can use the Contact class as if it had ‘name’ and ‘first_name’ attributes and the content will be transparently versioned.
advanced topics
time and dates
We have changed the ‘to_json’ method in Time so that it’s fast and works automatically when parsing the string (no typecasting).
Without Property, Time.now.utc.to_json looks like this out of rails:
"Thu Feb 11 17:50:18 UTC 2010"
With rails you get:
"2010/02/11 17:50:18 +0000"
With Property, you will have:
{"data":"2010-02-11 17:50:18","json_class":"Time"}
Note that this is the only version where this is true:
t = Time.utc(2010,02,11,17,50,18)
t == JSON.parse(t.to_json)
serialization engine
After intensive testing of YAML, Marshal and JSON serialization (all available and tested in the gem), we have decided to use JSON because it’s the fastest solution in real-world cases and it’s less problematic on the long run.
In case you have read that Marshal is faster then JSON, this might be the case if:
- you don’t read (decode) more often then you encode (not the case in a web app)
- the tests don’t include the ‘pack’ and ‘unpack’ necessary to move the data in the database
My own benchmarks show:
user system total real
JSON 0.710000 0.010000 0.720000 ( 0.720144)
Marshal 0.830000 0.050000 0.880000 ( 0.888717)
You can download the “testfile” if you want.
PS: In fact, Marshal is faster if you encode lots of tiny objects, but there are many disadvantages to using Marshal encoding that the tiny speed difference is just not worth it.