module ActiveRecord::Locking::Optimistic
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 ActiveRecord::Locking::Pessimistic
for an alternative.
Usage¶ ↑
Active Record
supports optimistic locking if the lock_version
field is present. Each update to the record increments the lock_version
column and the locking facilities ensure that records instantiated twice will let the last one saved raise a 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
Private Instance Methods
# File activerecord/lib/active_record/locking/optimistic.rb, line 71 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
# File activerecord/lib/active_record/locking/optimistic.rb, line 80 def _update_record(attribute_names = self.attribute_names) return super unless locking_enabled? return 0 if attribute_names.empty? begin lock_col = self.class.locking_column previous_lock_value = read_attribute_before_type_cast(lock_col) increment_lock attribute_names.push(lock_col) relation = self.class.unscoped affected_rows = relation.where( self.class.primary_key => id, lock_col => previous_lock_value ).update_all( attributes_for_update(attribute_names).map do |name| [name, _read_attribute(name)] end.to_h ) unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") end affected_rows # If something went wrong, revert the locking_column value. rescue Exception send("#{lock_col}=", previous_lock_value.to_i) raise end end
# File activerecord/lib/active_record/locking/optimistic.rb, line 118 def destroy_row affected_rows = super if locking_enabled? && affected_rows != 1 raise ActiveRecord::StaleObjectError.new(self, "destroy") end affected_rows end
# File activerecord/lib/active_record/locking/optimistic.rb, line 65 def increment_lock lock_col = self.class.locking_column previous_lock_value = send(lock_col) send("#{lock_col}=", previous_lock_value + 1) end
# File activerecord/lib/active_record/locking/optimistic.rb, line 128 def relation_for_destroy relation = super if locking_enabled? locking_column = self.class.locking_column relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column)) end relation end