ActiveRecord partial updates 7

Posted by pratik
on Tuesday, December 18

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 :

1
2
3
4
5
6
>> 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 !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>> 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 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 :
1
2
3
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

Comments

Leave a response

  1. LawrenceDecember 18, 2007 @ 07:21 AM

    It’s about time it did partial updates. ;)

    No invalid state possible:

    t.integer :lock_version, :null => false, :default => 0

  2. PratikDecember 18, 2007 @ 07:46 AM

    Err…Thanks Lawrence. I completely missed Optimistic locking.

  3. MislavDecember 18, 2007 @ 08:38 AM

    OMFG, 100x faster?? :))

    Nice post, Pratik. It’s about time someone did dirty field checking. Let’s make this a plugin!

  4. August LilleaasDecember 18, 2007 @ 11:51 AM

    This is pretty damn awesome. Looks like material for 2.1 imo. Not only do you get phat and efficient DB inserts, you also get an awesome way of figuring out which attributes that changed on an AR instance, which is useful outside of this particular context, too. Which again makes it very suitable material for a core patch, imo, not “just” a plugin.

  5. PratikDecember 18, 2007 @ 06:15 PM

    Mislav : I am playing with an idea of a plugin called “railsex” without the focus on “sex” part ;-) It’s supposed to be “rails extensions” for all the cool stuff which cannot be in the core for whatever reasons. So, do join in when I get it rolling! Chu Yeow is interested too. I think it might be a good idea.

    August : I agree that having something that having a good way to figure out dirty attributes is a good thing to have in the core and dirty does a very good job. But I’m not quite sure about partial updates thing. I guess on-demand partial updates might be a good fit. I’ll do the plugin first I think.

  6. Michael GenereuxDecember 21, 2007 @ 07:50 AM

    I agree this is good news for Rails. Pratik, have you seen the G framework?

  7. Luke Noel-StorrMarch 17, 2008 @ 03:18 PM

    Is this still working in edge Rails?

    I tried using it, and it doesn’t seem to have any affect (the logs suggest all fields are still being written). It also causes an exception to be thrown when using update_attribute().

Comment