Serializing ActiveRecord objects

Posted by Brian in Rails, snacks (March 19th, 2007)

One neat feature of Rails’ ActiveRecord objects is the #serialize method which will allow you to store an object in the database as YAML. This means, for example, you could have a User and a Profile. The Profile could be a hash containing a bunch of values that you want to manage, but that don’t necessarily need their own database table.


  class User < ActiveRecord::Base
  
    serialize :profile
  end
 
  u = User.find 1
  u.profile = {"url" => "http://www.rubyonrails.com", :nickname =>"Ninja master"}
  u.save
 
  u = User.find 1
  u.profile.class
  => Hash
 

The magic of the #serialize method takes the given object, serializes it to YAML, stores the YAML in the database, and then unserializes it when you retrieve the data.

This is all well and good, and I thought I would take advantage of this to help me easily cache some data. I have a system in which I have tasks, and a task belongs to a Service which contains the rate we charge for the task. Now, I really want to be able to store the service name and rate on the task when it’s assigned so that the task won’t be affected when I change my rates in the future.

“Aha!” I thought,”I can just use serialize and store the Service object right on the task!” I created a migration that added a service_data field to my database


./script/generate migration AddServiceDataToTasks


 
class AddServiceDataToTasks< ActiveRecord::Migration
  def self.up
    add_column :tasks, :service_data, :text
  end
 
  def self.down
    remove_column :tasks, :service_data
  end
end
 

I wrote a quick unit test which I knew would come in handy later.


   def test_saves_service_when_task_is_created
      @service = Service.find_by_name "Rails development"
      task = Task.create :name=>"Create user registration site", :esthours => 5, :service => @service
      t = Task.find task.id
      assert_not_nil t.service_data
      assert_kind_of Service, t.service_data
  
   end

Then I modified my Task model


  class Task < ActiveRecord::Base
    belongs_to :service
    serialize :service_data
 
    after_create  :sync_service_data!
  
    def sync_service_data!
       self.service_data = self.service
       self.save!
    end
  end

That seemed simple enough. However, when I tried it, I got a nasty surprise…


 
  t = Task.find 1
  t.service_data
  => nil
 

No matter what i tried, the service data always came back empty.

Running my unit test proved that something was definitely wrong, as I kept seeing “nil expected to not be nil”.

After searching and playing, I decided that #serialize was just not capable of serializing ActiveRecord objects. To get around this, I simply changed my code slightly. I knew that #serialize can handle Hashes so I stored just the attributes hash. Then I redefined #service_data “getter” method to create a new instance of Service from that hash.


  class Task < ActiveRecord::Base
    belongs_to :service
    serialize :service_data
 
    def sync_service_data!
       self.service_data = self.service.attributes
       self.save!
    end
 
    def service_data
      Service.new(self.attributes["service_data"]
    end
end

A quick run of the tests showed that I was now getting what I wanted.

Shortly after I discovered this solution, Jon Garvin offered a much cleaner solution…. don’t use Serialize. He found that Serialize does some strange magical things that often get in the way of our intended results. He proposed that I try

  class Task < ActiveRecord::Base
    belongs_to :service
 
    after_create  :sync_service_data!
  
    def sync_service_data!
       self.service_data = self.service
       self.save!
    end
 
    def service_data
     self[:service_data] ? Marshal.load(self[:service_data]) : nil
    end
 
    def service_data=(service)
      self[:service_data] = Marshal.dump(service)
    end
 
  end

This method simply creates a setter that manually marshals the data to the database column, and a getter that retrieves it again. This method works great, and I thank Jon for his quick solution!

2 Responses to ' Serializing ActiveRecord objects '

Subscribe to comments with RSS or TrackBack to ' Serializing ActiveRecord objects '.

  1. Jeff said,
    on March 29th, 2007 at 6:05 am

    That’s cool, I’m looking to do something similar in one of my models, but I have a question. Doesn’t that cause two database hits (one for the create, one for the sync). I’m on a production app and have to worry about such things.

    Thanks for the tip.

  2. Brian said,
    on April 2nd, 2007 at 6:10 am

    @Jeff:

    Sure does. In this case, I am going to do things like calculate the total bill for a project, and I’d rather do 2 hits on creation rather than end up with bad calculations later on (like if I change a task,

    But what if services were stored on a different database server? Then my calculation routine would have to do a remote request for that service info as I loop over the tasks. Ugh!

Leave a reply

:mrgreen: :neutral: :twisted: :shock: :smile: :???: :cool: :evil: :grin: :oops: :razz: :roll: :wink: :cry: :eek: :lol: :mad: :sad: