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.
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
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.
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
.
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.
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.
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 .
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
If this blog was helpful, check out our full blog archive.