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 is thrown if that has occurred and the update is ignored. # # Check out ActiveRecord::Locking::Pessimistic for an alternative. # # == Usage # # Active Records support optimistic locking if the field lock_version 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 a 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 a 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. # # You must ensure that your database schema defaults the lock_version column to 0. # # This behavior can be turned off by setting ActiveRecord::Base.lock_optimistically = false. # To override the name of the lock_version column, invoke the set_locking_column method. # This method uses the same syntax as set_table_name module Optimistic extend ActiveSupport::Concern included do cattr_accessor :lock_optimistically, :instance_writer => false self.lock_optimistically = true alias_method_chain :update, :lock alias_method_chain :destroy, :lock alias_method_chain :attributes_from_column_definition, :lock class << self alias_method :locking_column=, :set_locking_column end end def locking_enabled? #:nodoc: self.class.locking_enabled? end private def attributes_from_column_definition_with_lock result = attributes_from_column_definition_without_lock # If the locking column has no default value set, # start the lock version at zero. Note we can't use # locking_enabled? at this point as @attributes may # not have been initialized yet if lock_optimistically && result.include?(self.class.locking_column) result[self.class.locking_column] ||= 0 end return result end def update_with_lock(attribute_names = @attributes.keys) #:nodoc: return update_without_lock(attribute_names) unless locking_enabled? return 0 if attribute_names.empty? lock_col = self.class.locking_column previous_value = send(lock_col).to_i send(lock_col + '=', previous_value + 1) attribute_names += [lock_col] attribute_names.uniq! begin affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking") UPDATE #{self.class.quoted_table_name} SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false, attribute_names))} WHERE #{self.class.primary_key} = #{quote_value(id)} AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)} end_sql unless affected_rows == 1 raise ActiveRecord::StaleObjectError, "Attempted to update a stale object" end affected_rows # If something went wrong, revert the version. rescue Exception send(lock_col + '=', previous_value) raise end end def destroy_with_lock #:nodoc: return destroy_without_lock unless locking_enabled? unless new_record? lock_col = self.class.locking_column previous_value = send(lock_col).to_i affected_rows = connection.delete( "DELETE FROM #{self.class.quoted_table_name} " + "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " + "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}", "#{self.class.name} Destroy" ) unless affected_rows == 1 raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object" end end freeze end module ClassMethods DEFAULT_LOCKING_COLUMN = 'lock_version' def self.extended(base) class < 1) if locking_enabled? update_counters_without_lock(id, counters) end end end end end