We write about Ruby on Rails, React.js, React Native, remote work, open source, engineering and design.
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.
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
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.
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
.
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.
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.
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.
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