Module: ActiveRecord::Locking::Optimistic
Relationships & Source Files | |
Namespace Children | |
Modules:
| |
Extension / Inclusion / Inheritance Descendants | |
Included In:
| |
Super Chains via Extension / Inclusion / Inheritance | |
Class Chain:
self,
::ActiveSupport::Concern
|
|
Defined in: | activerecord/lib/active_record/locking/optimistic.rb |
Overview
What is Optimistic Locking
Optimistic
locking allows multiple users to access the same record for edits, and assumes a minimum of conflicts with the data. It does this by checking whether another process has made changes to a record since it was opened, an ::ActiveRecord::StaleObjectError
exception is thrown if that has occurred and the update is ignored.
Check out Pessimistic
for an alternative.
Usage
Active Record supports optimistic locking if the lock_version
field is present. Each update to the record increments the integer column lock_version
and the locking facilities ensure that records instantiated twice will let the last one saved raise a ::ActiveRecord::StaleObjectError
if the first was also updated. Example:
p1 = Person.find(1)
p2 = Person.find(1)
p1.first_name = "Michael"
p1.save
p2.first_name = "should fail"
p2.save # Raises an ActiveRecord::StaleObjectError
Optimistic
locking will also check for stale data when objects are destroyed. Example:
p1 = Person.find(1)
p2 = Person.find(1)
p1.first_name = "Michael"
p1.save
p2.destroy # Raises an ActiveRecord::StaleObjectError
You’re then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, or otherwise apply the business logic needed to resolve the conflict.
This locking mechanism will function inside a single Ruby process. To make it work across all web requests, the recommended approach is to add lock_version
as a hidden field to your form.
This behavior can be turned off by setting ActiveRecord::Base.lock_optimistically = false
. To override the name of the lock_version
column, set the locking_column
class attribute:
class Person < ActiveRecord::Base
self.locking_column = :lock_person
end
Class Method Summary
::ActiveSupport::Concern
- Extended
class_methods | Define class methods from given block. |
included | Evaluate given block in context of base class, so that you can write class macros here. |
prepended | Evaluate given block in context of base class, so that you can write class macros here. |
append_features, prepend_features |
Instance Attribute Summary
- #locking_enabled? ⇒ Boolean readonly Internal use only
Instance Method Summary
- #_clear_locking_column private
- #_create_record(attribute_names = self.attribute_names) private
- #_lock_value_for_database(locking_column) private
- #_query_constraints_hash private
- #_touch_row(attribute_names, time) private
- #_update_row(attribute_names, attempted_action = "update") private
- #destroy_row private
- #increment! Internal use only
- #initialize_dup(other) Internal use only
DSL Calls
included
[ GitHub ]55 56 57
# File 'activerecord/lib/active_record/locking/optimistic.rb', line 55
included do class_attribute :lock_optimistically, instance_writer: false, default: true end
Instance Attribute Details
#locking_enabled? ⇒ Boolean
(readonly)
# File 'activerecord/lib/active_record/locking/optimistic.rb', line 59
def locking_enabled? # :nodoc: self.class.locking_enabled? end
Instance Method Details
#_clear_locking_column (private)
[ GitHub ]# File 'activerecord/lib/active_record/locking/optimistic.rb', line 142
def _clear_locking_column self[self.class.locking_column] = nil clear_attribute_change(self.class.locking_column) end
#_create_record(attribute_names = self.attribute_names) (private)
[ GitHub ]# File 'activerecord/lib/active_record/locking/optimistic.rb', line 78
def _create_record(attribute_names = self.attribute_names) if locking_enabled? # We always want to persist the locking version, even if we don't detect # a change from the default, since the database might have no default attribute_names |= [self.class.locking_column] end super end
#_lock_value_for_database(locking_column) (private)
[ GitHub ]# File 'activerecord/lib/active_record/locking/optimistic.rb', line 134
def _lock_value_for_database(locking_column) if will_save_change_to_attribute?(locking_column) @attributes[locking_column].value_for_database else @attributes[locking_column].original_value_for_database end end
#_query_constraints_hash (private)
[ GitHub ]# File 'activerecord/lib/active_record/locking/optimistic.rb', line 147
def _query_constraints_hash return super unless locking_enabled? locking_column = self.class.locking_column super.merge(locking_column => _lock_value_for_database(locking_column)) end
#_touch_row(attribute_names, time) (private)
[ GitHub ]# File 'activerecord/lib/active_record/locking/optimistic.rb', line 87
def _touch_row(attribute_names, time) @_touch_attr_names << self.class.locking_column if locking_enabled? super end
#_update_row(attribute_names, attempted_action = "update") (private)
[ GitHub ]# File 'activerecord/lib/active_record/locking/optimistic.rb', line 92
def _update_row(attribute_names, attempted_action = "update") return super unless locking_enabled? begin locking_column = self.class.locking_column lock_attribute_was = @attributes[locking_column] update_constraints = _query_constraints_hash attribute_names = attribute_names.dup if attribute_names.frozen? attribute_names << locking_column self[locking_column] += 1 affected_rows = self.class._update_record( attributes_with_values(attribute_names), update_constraints ) if affected_rows != 1 raise ActiveRecord::StaleObjectError.new(self, attempted_action) end affected_rows # If something went wrong, revert the locking_column value. rescue Exception @attributes[locking_column] = lock_attribute_was raise end end
#destroy_row (private)
[ GitHub ]# File 'activerecord/lib/active_record/locking/optimistic.rb', line 124
def destroy_row affected_rows = super if locking_enabled? && affected_rows != 1 raise ActiveRecord::StaleObjectError.new(self, "destroy") end affected_rows end
#increment!
# File 'activerecord/lib/active_record/locking/optimistic.rb', line 63
def increment!(*, **) # :nodoc: super.tap do if locking_enabled? self[self.class.locking_column] += 1 clear_attribute_change(self.class.locking_column) end end end
#initialize_dup(other)
# File 'activerecord/lib/active_record/locking/optimistic.rb', line 72
def initialize_dup(other) # :nodoc: super _clear_locking_column if locking_enabled? end