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.
class Article < ActiveRecord::Base
end
Now lets look at ActiveRecord::Base class in its entirety.
module ActiveRecord
class Base
extend ActiveModel::Naming
extend ActiveSupport::Benchmarkable
extend ActiveSupport::DescendantsTracker
extend ConnectionHandling
extend QueryCache::ClassMethods
extend Querying
extend Translation
extend DynamicMatchers
extend Explain
include Persistence
include ReadonlyAttributes
include ModelSchema
include Inheritance
include Scoping
include Sanitization
include AttributeAssignment
include ActiveModel::Conversion
include Integration
include Validations
include CounterCache
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
include Callbacks
include Timestamp
include Associations
include ActiveModel::SecurePassword
include AutosaveAssociation
include NestedAttributes
include Aggregations
include Transactions
include Reflection
include Serialization
include Store
include Core
end
ActiveSupport.run_load_hooks(:active_record, Base)
end
Base
class extends and includes a lot of modules. Here we are going to look at the four modules that have method def save
.
module ActiveRecord
class Base
......................
include Persistence
.......................
include Validations
........................
include AttributeMethods
........................
include Transactions
........................
end
end
include Persistence
Module Persistence
defines save
method like this
def save(*)
create_or_update
rescue ActiveRecord::RecordInvalid
false
end
Now lets see method create_or_update
.
def create_or_update
raise ReadOnlyRecord if readonly?
result = new_record? ? create_record : update_record
result != false
end
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
def save(options={})
perform_validations(options) ? super : false
end
In this case the save
method simply invokes a call to perform_validations
.
include AttributeMethods
Module AttributeMethods
includes a bunch of modules like this
module ActiveRecord
module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
include Read
include Write
include BeforeTypeCast
include Query
include PrimaryKey
include TimeZoneConversion
include Dirty
include Serialization
end
Here we want to look at Dirty
module which has save
method defined as following.
def save(*)
if status = super
@previously_changed = changes
@changed_attributes.clear
end
status
end
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
def save(*) #:nodoc:
rollback_active_record_state! do
with_transaction_returning_status { super }
end
end
The method rollback_active_record_state!
is defined as
def rollback_active_record_state!
remember_transaction_record_state
yield
rescue Exception
restore_transaction_record_state
raise
ensure
clear_transaction_record_state
end
And the method with_transaction_returning_status
is defined as
def with_transaction_returning_status
status = nil
self.class.transaction do
add_to_transaction
begin
status = yield
rescue ActiveRecord::Rollback
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
status = nil
end
raise ActiveRecord::Rollback unless status
end
status
end
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.
module ActiveRecord
class Base
......................
include Persistence
.......................
include Validations
........................
include AttributeMethods
........................
include Transactions
........................
end
end
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.
> User.new.save
1.9.1 :001 > User.new.save
entering save in transactions
(0.1ms) begin transaction
entering save in attribute_methods
entering save in validations
entering save in persistence
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]]
leaving save in persistence
leaving save in validations
leaving save in attribute_methods
(17.6ms) rollback transaction
leaving save in transactions
=> nil
As you can see the order of operations is
entering save in transactions
entering save in attribute_methods
entering save in validations
entering save in persistence
leaving save in persistence
leaving save in validations
leaving save in attribute_methods
leaving save in transactions