Back to Blog

Life of save in ActiveRecord

on January 15, 2013

Following code was tested with edge rails (rails4) .

In a RubyonRails application we save records often. It is one of the most used methods in ActiveRecord. In the blog we are going to take a look at the life cycle of save operation.

ActiveRecord::Base

A typical model looks like this.

1class Article < ActiveRecord::Base
2end

Now lets look at ActiveRecord::Base class in its entirety.

1module ActiveRecord
2  class Base
3    extend ActiveModel::Naming
4
5    extend ActiveSupport::Benchmarkable
6    extend ActiveSupport::DescendantsTracker
7
8    extend ConnectionHandling
9    extend QueryCache::ClassMethods
10    extend Querying
11    extend Translation
12    extend DynamicMatchers
13    extend Explain
14
15    include Persistence
16    include ReadonlyAttributes
17    include ModelSchema
18    include Inheritance
19    include Scoping
20    include Sanitization
21    include AttributeAssignment
22    include ActiveModel::Conversion
23    include Integration
24    include Validations
25    include CounterCache
26    include Locking::Optimistic
27    include Locking::Pessimistic
28    include AttributeMethods
29    include Callbacks
30    include Timestamp
31    include Associations
32    include ActiveModel::SecurePassword
33    include AutosaveAssociation
34    include NestedAttributes
35    include Aggregations
36    include Transactions
37    include Reflection
38    include Serialization
39    include Store
40    include Core
41  end
42
43  ActiveSupport.run_load_hooks(:active_record, Base)
44end

Base class extends and includes a lot of modules. Here we are going to look at the four modules that have method def save .

1module ActiveRecord
2  class Base
3    ......................
4    include Persistence
5    .......................
6    include Validations
7    ........................
8    include AttributeMethods
9    ........................
10    include Transactions
11    ........................
12  end
13end

include Persistence

Module Persistence defines save method like this

1def save(*)
2  create_or_update
3rescue ActiveRecord::RecordInvalid
4  false
5end

Now lets see method create_or_update .

1def create_or_update
2  raise ReadOnlyRecord if readonly?
3  result = new_record? ? create_record : update_record
4  result != false
5end

So save method invokes create_or_update and create_or_update method either creates a record or updates a record. Dead simple.

include Validations

In module Validations the save method is defined as

1def save(options={})
2  perform_validations(options) ? super : false
3end

In this case the save method simply invokes a call to perform_validations .

include AttributeMethods

Module AttributeMethods includes a bunch of modules like this

1module ActiveRecord
2  module AttributeMethods
3    extend ActiveSupport::Concern
4    include ActiveModel::AttributeMethods
5
6    included do
7      include Read
8      include Write
9      include BeforeTypeCast
10      include Query
11      include PrimaryKey
12      include TimeZoneConversion
13      include Dirty
14      include Serialization
15    end

Here we want to look at Dirty module which has save method defined as following.

1def save(*)
2  if status = super
3    @previously_changed = changes
4    @changed_attributes.clear
5  end
6  status
7end

Since this module is all about tracking if a record is dirty or not, the save method tracks the changed values.

include Transactions

In module Transactions the save method is defined as

1def save(*) #:nodoc:
2  rollback_active_record_state! do
3    with_transaction_returning_status { super }
4  end
5end

The method rollback_active_record_state! is defined as

1def rollback_active_record_state!
2  remember_transaction_record_state
3  yield
4rescue Exception
5  restore_transaction_record_state
6  raise
7ensure
8  clear_transaction_record_state
9end

And the method with_transaction_returning_status is defined as

1def with_transaction_returning_status
2  status = nil
3  self.class.transaction do
4    add_to_transaction
5    begin
6      status = yield
7    rescue ActiveRecord::Rollback
8      @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
9      status = nil
10    end
11
12    raise ActiveRecord::Rollback unless status
13  end
14  status
15end

Together methods rollback_active_record_state! and with_transaction_returning_status ensure that all the operations happening inside save is happening in a single transaction.

Why save method needs to be in a transaction .

A model can define a number of callbacks including after_save and before_save. All those callbacks are operated within a transaction. It means if an after_save callback operation raises an exception then the save operation is rolled back.

Not only that a number of associations like has_many and belongs_to use callbacks to handle association manipulation. In order to ensure the integrity of the operation the save operation is wrapped in a transaction .

reverse order of operation

In the Base class the modules are included in the following order.

1module ActiveRecord
2  class Base
3    ......................
4    include Persistence
5    .......................
6    include Validations
7    ........................
8    include AttributeMethods
9    ........................
10    include Transactions
11    ........................
12  end
13end

All the four modules have save method. The way ruby works the last module to be included gets to act of the method first. So the order in which save method gets execute is Transactions, AttributeMethods, Validations and Persistence .

To get a visual feel, I added a puts inside each of the save methods. Here is the result.

1> User.new.save
21.9.1 :001 > User.new.save
3entering save in transactions
4   (0.1ms)  begin transaction
5entering save in attribute_methods
6entering save in validations
7entering save in persistence
8  SQL (47.3ms)  INSERT INTO "users" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", Mon, 21 Jan 2013 14:56:52 UTC +00:00], ["updated_at", Mon, 21 Jan 2013 14:56:52 UTC +00:00]]
9leaving save in persistence
10leaving save in validations
11leaving save in attribute_methods
12   (17.6ms)  rollback transaction
13leaving save in transactions
14 => nil

As you can see the order of operations is

1entering save in transactions
2entering save in attribute_methods
3entering save in validations
4entering save in persistence
5
6leaving save in persistence
7leaving save in validations
8leaving save in attribute_methods
9leaving save in transactions

You might also like

If you liked this blog post, check out similar ones from BigBinary