From fbf9281f0e3ced714bc534821c8b241ed7ec358e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 31 Dec 2004 19:38:04 +0000 Subject: Added automated optimistic locking if the field lock_version is present #384 [Michael Koziarski] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@295 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/lib/active_record.rb | 6 ++-- activerecord/lib/active_record/base.rb | 2 ++ activerecord/lib/active_record/locking.rb | 57 +++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 activerecord/lib/active_record/locking.rb (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index ecf8499e16..963a14dd3e 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -39,11 +39,13 @@ require 'active_record/reflection' require 'active_record/timestamp' require 'active_record/acts/list' require 'active_record/acts/tree' +require 'active_record/locking' ActiveRecord::Base.class_eval do include ActiveRecord::Validations - include ActiveRecord::Timestamp include ActiveRecord::Callbacks + include ActiveRecord::Locking + include ActiveRecord::Timestamp include ActiveRecord::Associations include ActiveRecord::Aggregations include ActiveRecord::Transactions @@ -55,4 +57,4 @@ end require 'active_record/connection_adapters/mysql_adapter' require 'active_record/connection_adapters/postgresql_adapter' require 'active_record/connection_adapters/sqlite_adapter' -require 'active_record/connection_adapters/sqlserver_adapter' \ No newline at end of file +require 'active_record/connection_adapters/sqlserver_adapter' diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e4283ac157..849fb1578b 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -26,6 +26,8 @@ module ActiveRecord #:nodoc: end class PreparedStatementInvalid < ActiveRecordError #:nodoc: end + class StaleObjectError < ActiveRecordError #:nodoc: + end # Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change diff --git a/activerecord/lib/active_record/locking.rb b/activerecord/lib/active_record/locking.rb new file mode 100644 index 0000000000..e08e2fcec8 --- /dev/null +++ b/activerecord/lib/active_record/locking.rb @@ -0,0 +1,57 @@ +module ActiveRecord + module Locking + # 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 + # + # 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. + def self.append_features(base) + super + base.class_eval do + alias_method :update_without_lock, :update + alias_method :update, :update_with_lock + end + end + + def update_with_lock + if locking_enabled? + previous_value = self.lock_version + self.lock_version = previous_value + 1 + + affected_rows = connection.update( + "UPDATE #{self.class.table_name} "+ + "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " + + "WHERE #{self.class.primary_key} = #{quote(id)} AND lock_version = #{quote(previous_value)}", + "#{self.class.name} Update with optimistic locking" + ) + + raise(ActiveRecord::StaleObjectError, "Attempted to update a stale object") unless affected_rows == 1 + else + update_without_lock + end + end + end + + class Base + @@lock_optimistically = true + cattr_accessor :lock_optimistically + + def locking_enabled? + lock_optimistically && respond_to?(:lock_version) + end + end +end \ No newline at end of file -- cgit v1.2.3