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 lifecycle of save operation.
ActiveRecord::Base
A typical model looks like this.
1
2
class Article < ActiveRecord :: Base
end
Now lets look at ActiveRecord::Base class in its entirety.
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
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 .
1
2
3
4
5
6
7
8
9
10
11
12
13
module ActiveRecord
class Base
. . . . . . . . . . . . . . . . . . . . . .
include Persistence
. . . . . . . . . . . . . . . . . . . . . . .
include Validations
. . . . . . . . . . . . . . . . . . . . . . . .
include AttributeMethods
. . . . . . . . . . . . . . . . . . . . . . . .
include Transactions
. . . . . . . . . . . . . . . . . . . . . . . .
end
end
include Persistence
Module Persistence defines save method like this
1
2
3
4
5
def save ( * )
create_or_update
rescue ActiveRecord :: RecordInvalid
false
end
Now lets see method create_or_update .
1
2
3
4
5
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_udpate method either creates a record or updates a
record. Dead simple.
include Validations
In module Validations the save method is defined as
1
2
3
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.
1
2
3
4
5
6
7
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
1
2
3
4
5
def save ( * ) #:nodoc:
rollback_active_record_state! do
with_transaction_returning_status { super }
end
end
The method rollback_active_record_state! is defined as
1
2
3
4
5
6
7
8
9
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 rolledback.
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
> User . new . save
1 . 9 . 1 : 001 > User . new . save
entering save in transactions
( 0 . 1 ms ) begin transaction
entering save in attribute_methods
entering save in validations
entering save in persistence
SQL ( 47 . 3 ms ) 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 . 6 ms ) rollback transaction
leaving save in transactions
=> nil
As you can see the order of operations is
1
2
3
4
5
6
7
8
9
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