123456789_123456789_123456789_123456789_123456789_

Module: ActiveRecord::Locking::Optimistic

Relationships & Source Files
Namespace Children
Modules:
Extension / Inclusion / Inheritance Descendants
Included In:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
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

Instance Method Summary

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)

This method is for internal use only.
[ GitHub ]

  
# 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!

This method is for internal use only.
[ GitHub ]

  
# 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)

This method is for internal use only.
[ GitHub ]

  
# File 'activerecord/lib/active_record/locking/optimistic.rb', line 72

def initialize_dup(other) # :nodoc:
  super
  _clear_locking_column if locking_enabled?
end