Skip to content

Commit

Permalink
forgetting_assignment avoid serialization
Browse files Browse the repository at this point in the history
`#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
  • Loading branch information
akostadinov committed Apr 18, 2022
1 parent c9e5057 commit c7572f3
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 2 deletions.
32 changes: 30 additions & 2 deletions activemodel/lib/active_model/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,16 @@ def changed_in_place?
has_been_read? && type.changed_in_place?(original_value_for_database, value)
end

# Returns an attribute that no longer remembers previous assignments.
def forgetting_assignment
with_value_from_database(value_for_database)
case
when changed_in_place?
with_cast_value(value)
when changed_from_assignment?
dup_with_forgetting_assignment
else
self
end
end

def with_value_from_user(value)
Expand Down Expand Up @@ -155,6 +163,10 @@ def initialize_dup(other)
end
end

def dup_with_forgetting_assignment
self.dup.forget_original_assignment!
end

def changed_from_assignment?
assigned? && type.changed?(original_value, value, value_before_type_cast)
end
Expand All @@ -163,6 +175,13 @@ def _original_value_for_database
type.serialize(original_value)
end

protected
# only used when duplicating attribute before ever returning to the user
def forget_original_assignment!
@original_attribute = nil
self
end

class FromDatabase < Attribute # :nodoc:
def type_cast(value)
type.deserialize(value)
Expand All @@ -185,13 +204,22 @@ def came_from_user?
end

class WithCastValue < Attribute # :nodoc:
def initialize(*args, &block)
super

@initial_cast_value = !value_before_type_cast || value_before_type_cast.frozen? ? value_before_type_cast : value_before_type_cast.dup.freeze
end

def type_cast(value)
value
end

def changed_in_place?
false
initial_cast_value != value
end

private
attr_reader :initial_cast_value
end

class Null < Attribute # :nodoc:
Expand Down
41 changes: 41 additions & 0 deletions activemodel/test/cases/attribute_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,47 @@ def assert_valid_value(*)

assert changed.changed? # Check to avoid a false positive
assert_not_predicate forgotten, :changed?
assert_equal "foo", forgotten.value
end

class SerializingNonDeserializingType < Type::String
def serialize(value)
"serialized: #{value}"
end
end

test "forgetting assignments works when serialize/deserialize are not inverse" do
from_user = Attribute.from_user(:custom, "foo", SerializingNonDeserializingType.new).forgetting_assignment
from_db = Attribute.from_database(:custom, "foo", SerializingNonDeserializingType.new).forgetting_assignment
from_cast = Attribute.with_cast_value(:custom, "foo", SerializingNonDeserializingType.new).forgetting_assignment

assert_equal "foo", from_user.value
assert_equal "foo", from_db.value
assert_equal "foo", from_cast.value
end

test "forgetting assignments after assigning attribute" do
from_db = Attribute.from_database(:custom, +"bar", Type::String.new)
assigned = from_db.with_value_from_user("foo")
forgotten = assigned.forgetting_assignment

assert assigned.changed?
assert_not_predicate forgotten, :changed?
assert_equal "foo", forgotten.value
end

test "forgetting assignments after in-place mutation" do
from_user = Attribute.from_user(:custom, +"foo", Type::String.new)
from_db = Attribute.from_database(:custom, +"foo", Type::String.new)
from_cast = Attribute.with_cast_value(:custom, +"foo", Type::String.new)

from_user.value << " user"
from_db.value << " db"
from_cast.value << " cast"

assert_equal "foo user", from_user.forgetting_assignment.value
assert_equal "foo db", from_db.forgetting_assignment.value
assert_equal "foo cast", from_cast.forgetting_assignment.value
end

test "with_value_from_user validates the value" do
Expand Down

0 comments on commit c7572f3

Please sign in to comment.