Skip to content

Commit c7572f3

Browse files
committed
forgetting_assignment avoid serialization
`#forgetting_assignment` was introduced with 07723c2, and it involved deserializing and then serializing current value. This is an unnecessary extra operation, and also it is a problem for custom attribute types where `#serialize` and `#deserialize` are not inverse to each other operations. This is a problem for example when using oracle-enhanced-adapter. fixes rails#44317 and rails#42738 and rsim/oracle-enhanced#2268
1 parent c9e5057 commit c7572f3

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

activemodel/lib/active_model/attribute.rb

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,16 @@ def changed_in_place?
6868
has_been_read? && type.changed_in_place?(original_value_for_database, value)
6969
end
7070

71+
# Returns an attribute that no longer remembers previous assignments.
7172
def forgetting_assignment
72-
with_value_from_database(value_for_database)
73+
case
74+
when changed_in_place?
75+
with_cast_value(value)
76+
when changed_from_assignment?
77+
dup_with_forgetting_assignment
78+
else
79+
self
80+
end
7381
end
7482

7583
def with_value_from_user(value)
@@ -155,6 +163,10 @@ def initialize_dup(other)
155163
end
156164
end
157165

166+
def dup_with_forgetting_assignment
167+
self.dup.forget_original_assignment!
168+
end
169+
158170
def changed_from_assignment?
159171
assigned? && type.changed?(original_value, value, value_before_type_cast)
160172
end
@@ -163,6 +175,13 @@ def _original_value_for_database
163175
type.serialize(original_value)
164176
end
165177

178+
protected
179+
# only used when duplicating attribute before ever returning to the user
180+
def forget_original_assignment!
181+
@original_attribute = nil
182+
self
183+
end
184+
166185
class FromDatabase < Attribute # :nodoc:
167186
def type_cast(value)
168187
type.deserialize(value)
@@ -185,13 +204,22 @@ def came_from_user?
185204
end
186205

187206
class WithCastValue < Attribute # :nodoc:
207+
def initialize(*args, &block)
208+
super
209+
210+
@initial_cast_value = !value_before_type_cast || value_before_type_cast.frozen? ? value_before_type_cast : value_before_type_cast.dup.freeze
211+
end
212+
188213
def type_cast(value)
189214
value
190215
end
191216

192217
def changed_in_place?
193-
false
218+
initial_cast_value != value
194219
end
220+
221+
private
222+
attr_reader :initial_cast_value
195223
end
196224

197225
class Null < Attribute # :nodoc:

activemodel/test/cases/attribute_test.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,47 @@ def assert_valid_value(*)
227227

228228
assert changed.changed? # Check to avoid a false positive
229229
assert_not_predicate forgotten, :changed?
230+
assert_equal "foo", forgotten.value
231+
end
232+
233+
class SerializingNonDeserializingType < Type::String
234+
def serialize(value)
235+
"serialized: #{value}"
236+
end
237+
end
238+
239+
test "forgetting assignments works when serialize/deserialize are not inverse" do
240+
from_user = Attribute.from_user(:custom, "foo", SerializingNonDeserializingType.new).forgetting_assignment
241+
from_db = Attribute.from_database(:custom, "foo", SerializingNonDeserializingType.new).forgetting_assignment
242+
from_cast = Attribute.with_cast_value(:custom, "foo", SerializingNonDeserializingType.new).forgetting_assignment
243+
244+
assert_equal "foo", from_user.value
245+
assert_equal "foo", from_db.value
246+
assert_equal "foo", from_cast.value
247+
end
248+
249+
test "forgetting assignments after assigning attribute" do
250+
from_db = Attribute.from_database(:custom, +"bar", Type::String.new)
251+
assigned = from_db.with_value_from_user("foo")
252+
forgotten = assigned.forgetting_assignment
253+
254+
assert assigned.changed?
255+
assert_not_predicate forgotten, :changed?
256+
assert_equal "foo", forgotten.value
257+
end
258+
259+
test "forgetting assignments after in-place mutation" do
260+
from_user = Attribute.from_user(:custom, +"foo", Type::String.new)
261+
from_db = Attribute.from_database(:custom, +"foo", Type::String.new)
262+
from_cast = Attribute.with_cast_value(:custom, +"foo", Type::String.new)
263+
264+
from_user.value << " user"
265+
from_db.value << " db"
266+
from_cast.value << " cast"
267+
268+
assert_equal "foo user", from_user.forgetting_assignment.value
269+
assert_equal "foo db", from_db.forgetting_assignment.value
270+
assert_equal "foo cast", from_cast.forgetting_assignment.value
230271
end
231272

232273
test "with_value_from_user validates the value" do

0 commit comments

Comments
 (0)