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