Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 373
- Log:
Initial import of Radiant 0.9.1, which is now packaged as a gem. This is an
import of the tagged 0.9.1 source checked out from GitHub, which isn't quite
the same as the gem distribution - but it doesn't seem to be available in an
archived form and the installed gem already has modifications, so this is
the closest I can get.
- Author:
- rool
- Date:
- Mon Mar 21 13:40:05 +0000 2011
- Size:
- 6719 Bytes
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 | # Optimistic locking will also check for stale data when objects are destroyed. Example: |
27 | # |
28 | # p1 = Person.find(1) |
29 | # p2 = Person.find(1) |
30 | # |
31 | # p1.first_name = "Michael" |
32 | # p1.save |
33 | # |
34 | # p2.destroy # Raises a ActiveRecord::StaleObjectError |
35 | # |
36 | # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, |
37 | # or otherwise apply the business logic needed to resolve the conflict. |
38 | # |
39 | # You must ensure that your database schema defaults the lock_version column to 0. |
40 | # |
41 | # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>. |
42 | # To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method. |
43 | # This method uses the same syntax as <tt>set_table_name</tt> |
44 | module Optimistic |
45 | def self.included(base) #:nodoc: |
46 | base.extend ClassMethods |
47 | |
48 | base.cattr_accessor :lock_optimistically, :instance_writer => false |
49 | base.lock_optimistically = true |
50 | |
51 | base.alias_method_chain :update, :lock |
52 | base.alias_method_chain :destroy, :lock |
53 | base.alias_method_chain :attributes_from_column_definition, :lock |
54 | |
55 | class << base |
56 | alias_method :locking_column=, :set_locking_column |
57 | end |
58 | end |
59 | |
60 | def locking_enabled? #:nodoc: |
61 | self.class.locking_enabled? |
62 | end |
63 | |
64 | private |
65 | def attributes_from_column_definition_with_lock |
66 | result = attributes_from_column_definition_without_lock |
67 | |
68 | # If the locking column has no default value set, |
69 | # start the lock version at zero. Note we can't use |
70 | # locking_enabled? at this point as @attributes may |
71 | # not have been initialized yet |
72 | |
73 | if lock_optimistically && result.include?(self.class.locking_column) |
74 | result[self.class.locking_column] ||= 0 |
75 | end |
76 | |
77 | return result |
78 | end |
79 | |
80 | def update_with_lock(attribute_names = @attributes.keys) #:nodoc: |
81 | return update_without_lock(attribute_names) unless locking_enabled? |
82 | return 0 if attribute_names.empty? |
83 | |
84 | lock_col = self.class.locking_column |
85 | previous_value = send(lock_col).to_i |
86 | send(lock_col + '=', previous_value + 1) |
87 | |
88 | attribute_names += [lock_col] |
89 | attribute_names.uniq! |
90 | |
91 | begin |
92 | affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking") |
93 | UPDATE #{self.class.quoted_table_name} |
94 | SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false, attribute_names))} |
95 | WHERE #{self.class.primary_key} = #{quote_value(id)} |
96 | AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)} |
97 | end_sql |
98 | |
99 | unless affected_rows == 1 |
100 | raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}" |
101 | end |
102 | |
103 | affected_rows |
104 | |
105 | # If something went wrong, revert the version. |
106 | rescue Exception |
107 | send(lock_col + '=', previous_value) |
108 | raise |
109 | end |
110 | end |
111 | |
112 | def destroy_with_lock #:nodoc: |
113 | return destroy_without_lock unless locking_enabled? |
114 | |
115 | unless new_record? |
116 | lock_col = self.class.locking_column |
117 | previous_value = send(lock_col).to_i |
118 | |
119 | affected_rows = connection.delete( |
120 | "DELETE FROM #{self.class.quoted_table_name} " + |
121 | "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " + |
122 | "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}", |
123 | "#{self.class.name} Destroy" |
124 | ) |
125 | |
126 | unless affected_rows == 1 |
127 | raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}" |
128 | end |
129 | end |
130 | |
131 | freeze |
132 | end |
133 | |
134 | module ClassMethods |
135 | DEFAULT_LOCKING_COLUMN = 'lock_version' |
136 | |
137 | def self.extended(base) |
138 | class <<base |
139 | alias_method_chain :update_counters, :lock |
140 | end |
141 | end |
142 | |
143 | # Is optimistic locking enabled for this table? Returns true if the |
144 | # +lock_optimistically+ flag is set to true (which it is, by default) |
145 | # and the table includes the +locking_column+ column (defaults to |
146 | # +lock_version+). |
147 | def locking_enabled? |
148 | lock_optimistically && columns_hash[locking_column] |
149 | end |
150 | |
151 | # Set the column to use for optimistic locking. Defaults to +lock_version+. |
152 | def set_locking_column(value = nil, &block) |
153 | define_attr_method :locking_column, value, &block |
154 | value |
155 | end |
156 | |
157 | # The version column used for optimistic locking. Defaults to +lock_version+. |
158 | def locking_column |
159 | reset_locking_column |
160 | end |
161 | |
162 | # Quote the column name used for optimistic locking. |
163 | def quoted_locking_column |
164 | connection.quote_column_name(locking_column) |
165 | end |
166 | |
167 | # Reset the column used for optimistic locking back to the +lock_version+ default. |
168 | def reset_locking_column |
169 | set_locking_column DEFAULT_LOCKING_COLUMN |
170 | end |
171 | |
172 | # Make sure the lock version column gets updated when counters are |
173 | # updated. |
174 | def update_counters_with_lock(id, counters) |
175 | counters = counters.merge(locking_column => 1) if locking_enabled? |
176 | update_counters_without_lock(id, counters) |
177 | end |
178 | end |
179 | end |
180 | end |
181 | end |
182 |