Get dirty objects in Rails

Dirty object functionality has been in edge rails for a month now, allowing you to see what changes have been made to an object prior to saving it. Here's an example from Ryan's edge rails blog -
article = Article.find(:first)
article.changed? #=> false

# Track changes to individual attributes with
# attr_name_changed? accessor
article.title #=> "Title"
article.title = "New Title"
article.title_changed? #=> true

# Access previous value with attr_name_was accessor
article.title_was #=> "Title"

# See both previous and current value with attr_name_change accessor
article.title_change #=> ["Title", "New Title"]
Dirty objects should be available with the next release of Rails. Unfortunately, you need it in your Rails 2.0 project right away and can't wait. Well, you're in luck; adding this functionality to a Rails project turns out to be dead simple thanks to Dirty being a nicely decoupled module.
Here's what you need to do:
  • Download the source for dirty.rb from http://dev.rubyonrails.org/browser/trunk/activerecord/lib/active_record/dirty.rb?rev=9127. I'm specifically picking revision 9127 because later versions incorporate partial updates (also new in edge rails), which complicates matters.
  • Once you've got the file create a directory called active_record under RAILS_ROOT/config/initializers and copy dirty.rb into it. Thus, if your project is under /home/work/myproj, the path to dirty.rb should be /home/work/myproj/config/initializers/dirty.rb.
  • Reopen active_record and mixin Dirty. Do this by creating a file called active_record.rb under RAILS_ROOT/config/initializers and putting the following bit of code in it:
    require "#{File.dirname(__FILE__)}/active_record/dirty"

    ActiveRecord::Base.class_eval do
    include ActiveRecord::Dirty
    end

There, you're all set. You can verify that Dirty actually works by adding the following spec to your suite.
require File.dirname(__FILE__) + '/../spec_helper'

class TestModel < ActiveRecord::Base
end

describe 'Models with Dirty enabled' do
before(:all) do
connection = ActiveRecord::Base.connection
begin
connection.create_table(:test_models) do |t|
t.column :name, :string
t.column :age, :integer
end
rescue Exception => e
puts "Error #{e} when creating test table"
end
end

it "should mark new (unsaved) objects as changed" do
TestModel.new(:name => 'Ooga', :age => 15).should be_changed
end

it "should mark all fields of a new (unsaved) object as changed" do
t = TestModel.new(:name => 'Ooga', :age => 15)
t.name_changed?.should be_true
t.age_changed?.should be_true
end

it "should mark newly created objects as unchanged" do
TestModel.create(:name => 'Ooga', :age => 15).should_not be_changed
end

it "should consider objects retrieved from the database to be unchanged" do
TestModel.create(:name => 'Ooga', :age => 15)
TestModel.find(:first).should_not be_changed
end

it "should know when an object is dirty" do
t = TestModel.create(:name => 'Ooga', :age => 15)
t.age = 5
t.should be_changed
end

it "should know when a field is dirty" do
t = TestModel.create(:name => 'Ooga', :age => 15)
t.age = 5
t.age_changed?.should be_true
end

it "should mark an object as clean after a successful save" do
t = TestModel.create(:name => 'Ooga', :age => 15)
t.age = 5
t.save.should be_true
t.should_not be_changed
end

after(:all) do
drop_tables(ActiveRecord::Base.connection, :test_models)
end
end

Note that Rails automatically loads files under config/initializers - you can put these files elsewhere in your project, but make sure you tell Rails to pick them up and that the paths are alright. It is also possible that Dirty Objects may break some plugins, acts_as_audited being a case in point.
Post a Comment