# frozen_string_literal: true module ActiveRecord module Locking # == 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 integer column +lock_version+ 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 # module Optimistic extend ActiveSupport::Concern included do class_attribute :lock_optimistically, instance_writer: false, default: true end def locking_enabled? # :nodoc: self.class.locking_enabled? end 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 def initialize_dup(other) # :nodoc: super _clear_locking_column if locking_enabled? end private 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 def _touch_row(attribute_names, time) @_touch_attr_names << self.class.locking_column if locking_enabled? super end 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 def destroy_row affected_rows = super if locking_enabled? && affected_rows != 1 raise ActiveRecord::StaleObjectError.new(self, "destroy") end affected_rows end 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 def _clear_locking_column self[self.class.locking_column] = nil clear_attribute_change(self.class.locking_column) end 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 module ClassMethods DEFAULT_LOCKING_COLUMN = "lock_version" # Returns true if the +lock_optimistically+ flag is set to true # (which it is, by default) and the table includes the # +locking_column+ column (defaults to +lock_version+). def locking_enabled? lock_optimistically && columns_hash[locking_column] end # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) reload_schema_from_cache @locking_column = value.to_s end # The version column used for optimistic locking. Defaults to +lock_version+. attr_reader :locking_column # Reset the column used for optimistic locking back to the +lock_version+ default. def reset_locking_column self.locking_column = DEFAULT_LOCKING_COLUMN end # Make sure the lock version column gets updated when counters are # updated. def update_counters(id, counters) counters = counters.merge(locking_column => 1) if locking_enabled? super end private def hook_attribute_type(name, cast_type) if lock_optimistically && name == locking_column cast_type = LockingType.new(cast_type) end super end def inherited(base) super base.class_eval do @locking_column = DEFAULT_LOCKING_COLUMN end end end end # In de/serialize we change `nil` to 0, so that we can allow passing # `nil` values to `lock_version`, and not result in `ActiveRecord::StaleObjectError` # during update record. class LockingType < DelegateClass(Type::Value) # :nodoc: def self.new(subtype) self === subtype ? subtype : super end def deserialize(value) super.to_i end def serialize(value) super.to_i end def init_with(coder) __setobj__(coder["subtype"]) end def encode_with(coder) coder["subtype"] = __getobj__ end end end end