Froze rails gems
[depot.git] / vendor / rails / activerecord / lib / active_record / locking / optimistic.rb
1 module ActiveRecord
2 module Locking
3 # == What is Optimistic Locking
4 #
5 # Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
6 # conflicts with the data. It does this by checking whether another process has made changes to a record since
7 # it was opened, an ActiveRecord::StaleObjectError is thrown if that has occurred and the update is ignored.
8 #
9 # Check out ActiveRecord::Locking::Pessimistic for an alternative.
10 #
11 # == Usage
12 #
13 # Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
14 # record increments the lock_version column and the locking facilities ensure that records instantiated twice
15 # will let the last one saved raise a StaleObjectError if the first was also updated. Example:
16 #
17 # p1 = Person.find(1)
18 # p2 = Person.find(1)
19 #
20 # p1.first_name = "Michael"
21 # p1.save
22 #
23 # p2.first_name = "should fail"
24 # p2.save # Raises a ActiveRecord::StaleObjectError
25 #
26 # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
27 # or otherwise apply the business logic needed to resolve the conflict.
28 #
29 # You must ensure that your database schema defaults the lock_version column to 0.
30 #
31 # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
32 # To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
33 # This method uses the same syntax as <tt>set_table_name</tt>
34 module Optimistic
35 def self.included(base) #:nodoc:
36 base.extend ClassMethods
37
38 base.cattr_accessor :lock_optimistically, :instance_writer => false
39 base.lock_optimistically = true
40
41 base.alias_method_chain :update, :lock
42 base.alias_method_chain :attributes_from_column_definition, :lock
43
44 class << base
45 alias_method :locking_column=, :set_locking_column
46 end
47 end
48
49 def locking_enabled? #:nodoc:
50 self.class.locking_enabled?
51 end
52
53 private
54 def attributes_from_column_definition_with_lock
55 result = attributes_from_column_definition_without_lock
56
57 # If the locking column has no default value set,
58 # start the lock version at zero. Note we can't use
59 # locking_enabled? at this point as @attributes may
60 # not have been initialized yet
61
62 if lock_optimistically && result.include?(self.class.locking_column)
63 result[self.class.locking_column] ||= 0
64 end
65
66 return result
67 end
68
69 def update_with_lock(attribute_names = @attributes.keys) #:nodoc:
70 return update_without_lock(attribute_names) unless locking_enabled?
71 return 0 if attribute_names.empty?
72
73 lock_col = self.class.locking_column
74 previous_value = send(lock_col).to_i
75 send(lock_col + '=', previous_value + 1)
76
77 attribute_names += [lock_col]
78 attribute_names.uniq!
79
80 begin
81 affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
82 UPDATE #{self.class.quoted_table_name}
83 SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false, attribute_names))}
84 WHERE #{self.class.primary_key} = #{quote_value(id)}
85 AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
86 end_sql
87
88 unless affected_rows == 1
89 raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
90 end
91
92 affected_rows
93
94 # If something went wrong, revert the version.
95 rescue Exception
96 send(lock_col + '=', previous_value)
97 raise
98 end
99 end
100
101 module ClassMethods
102 DEFAULT_LOCKING_COLUMN = 'lock_version'
103
104 def self.extended(base)
105 class <<base
106 alias_method_chain :update_counters, :lock
107 end
108 end
109
110 # Is optimistic locking enabled for this table? Returns true if the
111 # +lock_optimistically+ flag is set to true (which it is, by default)
112 # and the table includes the +locking_column+ column (defaults to
113 # +lock_version+).
114 def locking_enabled?
115 lock_optimistically && columns_hash[locking_column]
116 end
117
118 # Set the column to use for optimistic locking. Defaults to +lock_version+.
119 def set_locking_column(value = nil, &block)
120 define_attr_method :locking_column, value, &block
121 value
122 end
123
124 # The version column used for optimistic locking. Defaults to +lock_version+.
125 def locking_column
126 reset_locking_column
127 end
128
129 # Quote the column name used for optimistic locking.
130 def quoted_locking_column
131 connection.quote_column_name(locking_column)
132 end
133
134 # Reset the column used for optimistic locking back to the +lock_version+ default.
135 def reset_locking_column
136 set_locking_column DEFAULT_LOCKING_COLUMN
137 end
138
139 # Make sure the lock version column gets updated when counters are
140 # updated.
141 def update_counters_with_lock(id, counters)
142 counters = counters.merge(locking_column => 1) if locking_enabled?
143 update_counters_without_lock(id, counters)
144 end
145 end
146 end
147 end
148 end