Skip to content

Commit 4db95d8

Browse files
committed
Restore the ability that update/destroy optimistic locking object without default
The ability has lost due to reverted rails#39321 in rails#41049. We should allow updating with dirty locking value to work the documented usage, but if casted value has no difference (i.e. regarded as no dirty), identify the object by the original (uninitialized default) value.
1 parent 8c60a21 commit 4db95d8

File tree

3 files changed

+57
-14
lines changed

3 files changed

+57
-14
lines changed

activemodel/lib/active_model/attribute.rb

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,13 @@ def encode_with(coder)
133133
coder["value"] = value if defined?(@value)
134134
end
135135

136-
protected
137-
def original_value_for_database
138-
if assigned?
139-
original_attribute.original_value_for_database
140-
else
141-
_original_value_for_database
142-
end
136+
def original_value_for_database
137+
if assigned?
138+
original_attribute.original_value_for_database
139+
else
140+
_original_value_for_database
143141
end
142+
end
144143

145144
private
146145
attr_reader :original_attribute
@@ -165,9 +164,10 @@ def type_cast(value)
165164
type.deserialize(value)
166165
end
167166

168-
def _original_value_for_database
169-
value_before_type_cast
170-
end
167+
private
168+
def _original_value_for_database
169+
value_before_type_cast
170+
end
171171
end
172172

173173
class FromUser < Attribute # :nodoc:

activerecord/lib/active_record/locking/optimistic.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ def _update_row(attribute_names, attempted_action = "update")
8989

9090
begin
9191
locking_column = self.class.locking_column
92-
previous_lock_value = attribute_before_type_cast(locking_column)
92+
lock_attribute_was = @attributes[locking_column]
93+
lock_value_for_database = _lock_value_for_database(locking_column)
94+
9395
attribute_names = attribute_names.dup if attribute_names.frozen?
9496
attribute_names << locking_column
9597

@@ -98,7 +100,7 @@ def _update_row(attribute_names, attempted_action = "update")
98100
affected_rows = self.class._update_record(
99101
attributes_with_values(attribute_names),
100102
@primary_key => id_in_database,
101-
locking_column => previous_lock_value
103+
locking_column => lock_value_for_database
102104
)
103105

104106
if affected_rows != 1
@@ -109,7 +111,7 @@ def _update_row(attribute_names, attempted_action = "update")
109111

110112
# If something went wrong, revert the locking_column value.
111113
rescue Exception
112-
self[locking_column] = previous_lock_value.to_i
114+
@attributes[locking_column] = lock_attribute_was
113115
raise
114116
end
115117
end
@@ -121,7 +123,7 @@ def destroy_row
121123

122124
affected_rows = self.class._delete_record(
123125
@primary_key => id_in_database,
124-
locking_column => attribute_before_type_cast(locking_column)
126+
locking_column => _lock_value_for_database(locking_column)
125127
)
126128

127129
if affected_rows != 1
@@ -131,6 +133,14 @@ def destroy_row
131133
affected_rows
132134
end
133135

136+
def _lock_value_for_database(locking_column)
137+
if will_save_change_to_attribute?(locking_column)
138+
@attributes[locking_column].value_for_database
139+
else
140+
@attributes[locking_column].original_value_for_database
141+
end
142+
end
143+
134144
module ClassMethods
135145
DEFAULT_LOCKING_COLUMN = "lock_version"
136146

activerecord/test/cases/locking_test.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,39 @@ def test_lock_without_default_should_work_with_null_in_the_database
340340
assert_equal "new title2", t2.title
341341
end
342342

343+
def test_update_with_lock_version_without_default_should_work_on_dirty_value_before_type_cast
344+
ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')")
345+
t1 = LockWithoutDefault.last
346+
347+
assert_equal 0, t1.lock_version
348+
assert_nil t1.lock_version_before_type_cast
349+
350+
t1.lock_version = t1.lock_version
351+
352+
assert_equal 0, t1.lock_version
353+
assert_equal 0, t1.lock_version_before_type_cast
354+
355+
assert_nothing_raised { t1.update!(title: "new title1") }
356+
assert_equal 1, t1.lock_version
357+
assert_equal "new title1", t1.title
358+
end
359+
360+
def test_destroy_with_lock_version_without_default_should_work_on_dirty_value_before_type_cast
361+
ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')")
362+
t1 = LockWithoutDefault.last
363+
364+
assert_equal 0, t1.lock_version
365+
assert_nil t1.lock_version_before_type_cast
366+
367+
t1.lock_version = t1.lock_version
368+
369+
assert_equal 0, t1.lock_version
370+
assert_equal 0, t1.lock_version_before_type_cast
371+
372+
assert_nothing_raised { t1.destroy! }
373+
assert_predicate t1, :destroyed?
374+
end
375+
343376
def test_lock_without_default_queries_count
344377
t1 = LockWithoutDefault.create(title: "title1")
345378

0 commit comments

Comments
 (0)