ActiveRecord partial updates
Published over 6 years ago

OMFG! This is the moment ya all have been waiting for..ActiveRecord partial updates are now possible !! It’ll make your application run 100x faster!!

Background

ActiveRecord updates all the columns when you save the object, without bothering to see if the column was changed or not.

Notice the UPDATE statement in the following console session :

>> p = Person.find :first
  Person Load (0.002784)   SELECT * FROM people LIMIT 1
=> #<Person id: 1, name: "Pratik", address: "Shangri-la", history: "pff", created_at: "2007-12-18 05:07:53", updated_at: "2007-12-18 06:08:13">
>> p.save
  Person Update (0.001178)   UPDATE people SET "created_at" = '2007-12-18 05:07:53', "name" = 'Pratik', "history" = 'pff', "address" = 'Shangri-la', "updated_at" = '2007-12-18 06:20:11' WHERE "id" = 1
=> true

O MAN !

module ActiveRecord
  module Changed
    def self.included(base)
      base.alias_method_chain :write_attribute, :changed
      base.alias_method_chain :update_without_timestamps, :changed
      base.alias_method_chain :save, :changed
      base.alias_method_chain :save!, :changed
    end

    private

    def write_attribute_with_changed(attr_name, value)
      # If you're accessing attr= method, you should change the value ;-)
      changed_attributes << attr_name.to_s
      write_attribute_without_changed(attr_name, value)
    end

    def update_without_timestamps_with_changed      
      quoted_attributes = attributes_with_quotes(false, false)
      quoted_attributes.reject! { |key, value| !changed_attributes.include?(key.to_s)}
      return 0 if quoted_attributes.empty?
      connection.update(
        "UPDATE #{self.class.quoted_table_name} " +
        "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " +
        "WHERE #{connection.quote_column_name(self.class.primary_key)} " +
        "= #{quote_value(id)}",
        "#{self.class.name} Update"
      )
    end

    def changed_attributes
      @changed_attributes ||= Set.new
    end

    def save_with_changed
      save_without_changed ensure changed_attributes.clear
    end

    def save_with_changed!
      save_without_changed! ensure changed_attributes.clear
    end

  end
end

ActiveRecord::Base.send :include, ActiveRecord::Changed

Ok, you won’t too much of performance boost with this. I lied. It was a joke. Get over it.

Using this code :

>> p = Person.find :first
  Person Load (0.000538)   SELECT * FROM people LIMIT 1
=> #<Person id: 1, name: "Hellll", address: "whatever", history: "pff", created_at: "2007-12-18 05:07:53", updated_at: "2007-12-18 05:40:45">
>> p.save
  Person Update (0.000536)   UPDATE people SET "updated_at" = '2007-12-18 06:07:55' WHERE "id" = 1
=> true
>> p.name = "Pratik"
=> "Pratik"
>> p.save
  Person Update (0.000908)   UPDATE people SET "name" = 'Pratik', "updated_at" = '2007-12-18 06:08:04' WHERE "id" = 1
=> true
>> p.address = "Shangri-la"
=> "Shangri-la"
>> p.save
  Person Update (0.000542)   UPDATE people SET "address" = 'Shangri-la', "updated_at" = '2007-12-18 06:08:13' WHERE "id" = 1
=> true

But..

Yes, there is a big fat but here

Do partial updates make sense ?

class Whatever < <Something>::Base
  def validate
    errors.add("Invalid") if self.foo == "Hello" and self.bar == "World"
  end
end

Current Record State : { :foo => "abc", :bar => "xyz" }

( Two processes fetch the same record concurrently )
t0 : Process 1 : Fetch the record | Process 2 : Fetch the record

t1 : Process 1 : set 'foo' to "Hello". keep 'bar' as "xyz"
 { :foo => "Hello", :bar => "xyz" }
Partial update will execute the query only to update :foo

t2 : Process 2 : set 'bar' to "World". keep 'bar' as "abc"
 { :foo => "abc", :bar => "World" }
Partial update will execute the query only to update :bar

t3 : Final state of the record :
 { :foo => "Hello", :bar => "World" }

Yes, it can leave your records in invalid state

Awww…Did I make you sad :-( ?

Well, don’t worry. The point is :

  • It is very simple to do partial updates with ActiveRecord. It’s no rocket science.
  • Know what you’re doing. Make sure you don’t screw up validations. Use optimistic locking. ( Thanks to Lawrence for pointing it out )
  • Probably go for more declarative style if you really have a situation where partial updates will make a difference. Something like :
class Whatever < <Something>::Base
  lazy_attributes :some_column_which_is_huge_and_rarely_changes
end

And apply the partial update logic only to the attributes supplied to lazy_attributes