mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
c4cb6862ba
This reduces the size of a YAML encoded Active Record object by ~80% depending on the number of columns. There were a number of wasteful things that occurred when we encoded the objects before that have resulted in numerous wins - We were emitting the result of `attributes_before_type_cast` as a hack to work around some laziness issues - The name of an attribute was emitted multiple times, since the attribute objects were in a hash keyed by the name. We now store them in an array instead, and reconstruct the hash using the name - The types were included for every attribute. This would use backrefs if multiple objects were encoded, but really we don't need to include it at all unless it differs from the type at the class level. (The only time that will occur is if the field is the result of a custom select clause) - `original_attribute:` was included over and over and over again since the ivar is almost always `nil`. We've added a custom implementation of `encode_with` on the attribute objects to ensure we don't write the key when the field is `nil`. This isn't without a cost though. Since we're no longer including the types, an object can find itself in an invalid state if the type changes on the class after serialization. This is the same as 4.1 and earlier, but I think it's worth noting. I was worried that I'd introduce some new state bugs as a result of doing this, so I've added an additional test that asserts mutation not being lost as the result of YAML round tripping. Fixes #25145
227 lines
5.1 KiB
Ruby
227 lines
5.1 KiB
Ruby
module ActiveRecord
|
|
class Attribute # :nodoc:
|
|
class << self
|
|
def from_database(name, value, type)
|
|
FromDatabase.new(name, value, type)
|
|
end
|
|
|
|
def from_user(name, value, type, original_attribute = nil)
|
|
FromUser.new(name, value, type, original_attribute)
|
|
end
|
|
|
|
def with_cast_value(name, value, type)
|
|
WithCastValue.new(name, value, type)
|
|
end
|
|
|
|
def null(name)
|
|
Null.new(name)
|
|
end
|
|
|
|
def uninitialized(name, type)
|
|
Uninitialized.new(name, type)
|
|
end
|
|
end
|
|
|
|
attr_reader :name, :value_before_type_cast, :type
|
|
|
|
# This method should not be called directly.
|
|
# Use #from_database or #from_user
|
|
def initialize(name, value_before_type_cast, type, original_attribute = nil)
|
|
@name = name
|
|
@value_before_type_cast = value_before_type_cast
|
|
@type = type
|
|
@original_attribute = original_attribute
|
|
end
|
|
|
|
def value
|
|
# `defined?` is cheaper than `||=` when we get back falsy values
|
|
@value = type_cast(value_before_type_cast) unless defined?(@value)
|
|
@value
|
|
end
|
|
|
|
def original_value
|
|
if assigned?
|
|
original_attribute.original_value
|
|
else
|
|
type_cast(value_before_type_cast)
|
|
end
|
|
end
|
|
|
|
def value_for_database
|
|
type.serialize(value)
|
|
end
|
|
|
|
def changed?
|
|
changed_from_assignment? || changed_in_place?
|
|
end
|
|
|
|
def changed_in_place?
|
|
has_been_read? && type.changed_in_place?(original_value_for_database, value)
|
|
end
|
|
|
|
def forgetting_assignment
|
|
with_value_from_database(value_for_database)
|
|
end
|
|
|
|
def with_value_from_user(value)
|
|
type.assert_valid_value(value)
|
|
self.class.from_user(name, value, type, self)
|
|
end
|
|
|
|
def with_value_from_database(value)
|
|
self.class.from_database(name, value, type)
|
|
end
|
|
|
|
def with_cast_value(value)
|
|
self.class.with_cast_value(name, value, type)
|
|
end
|
|
|
|
def with_type(type)
|
|
self.class.new(name, value_before_type_cast, type, original_attribute)
|
|
end
|
|
|
|
def type_cast(*)
|
|
raise NotImplementedError
|
|
end
|
|
|
|
def initialized?
|
|
true
|
|
end
|
|
|
|
def came_from_user?
|
|
false
|
|
end
|
|
|
|
def has_been_read?
|
|
defined?(@value)
|
|
end
|
|
|
|
def ==(other)
|
|
self.class == other.class &&
|
|
name == other.name &&
|
|
value_before_type_cast == other.value_before_type_cast &&
|
|
type == other.type
|
|
end
|
|
alias eql? ==
|
|
|
|
def hash
|
|
[self.class, name, value_before_type_cast, type].hash
|
|
end
|
|
|
|
def init_with(coder)
|
|
@name = coder["name"]
|
|
@value_before_type_cast = coder["value_before_type_cast"]
|
|
@type = coder["type"]
|
|
@original_attribute = coder["original_attribute"]
|
|
@value = coder["value"] if coder.map.key?("value")
|
|
end
|
|
|
|
def encode_with(coder)
|
|
coder["name"] = name
|
|
coder["value_before_type_cast"] = value_before_type_cast if value_before_type_cast
|
|
coder["type"] = type if type
|
|
coder["original_attribute"] = original_attribute if original_attribute
|
|
coder["value"] = value if defined?(@value)
|
|
end
|
|
|
|
protected
|
|
|
|
attr_reader :original_attribute
|
|
alias_method :assigned?, :original_attribute
|
|
|
|
def initialize_dup(other)
|
|
if defined?(@value) && @value.duplicable?
|
|
@value = @value.dup
|
|
end
|
|
end
|
|
|
|
def changed_from_assignment?
|
|
assigned? && type.changed?(original_value, value, value_before_type_cast)
|
|
end
|
|
|
|
def original_value_for_database
|
|
if assigned?
|
|
original_attribute.original_value_for_database
|
|
else
|
|
_original_value_for_database
|
|
end
|
|
end
|
|
|
|
def _original_value_for_database
|
|
value_for_database
|
|
end
|
|
|
|
class FromDatabase < Attribute # :nodoc:
|
|
def type_cast(value)
|
|
type.deserialize(value)
|
|
end
|
|
|
|
def _original_value_for_database
|
|
value_before_type_cast
|
|
end
|
|
end
|
|
|
|
class FromUser < Attribute # :nodoc:
|
|
def type_cast(value)
|
|
type.cast(value)
|
|
end
|
|
|
|
def came_from_user?
|
|
true
|
|
end
|
|
end
|
|
|
|
class WithCastValue < Attribute # :nodoc:
|
|
def type_cast(value)
|
|
value
|
|
end
|
|
|
|
def changed_in_place_from?(old_value)
|
|
false
|
|
end
|
|
end
|
|
|
|
class Null < Attribute # :nodoc:
|
|
def initialize(name)
|
|
super(name, nil, Type::Value.new)
|
|
end
|
|
|
|
def type_cast(*)
|
|
nil
|
|
end
|
|
|
|
def with_type(type)
|
|
self.class.with_cast_value(name, nil, type)
|
|
end
|
|
|
|
def with_value_from_database(value)
|
|
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
|
|
end
|
|
alias_method :with_value_from_user, :with_value_from_database
|
|
end
|
|
|
|
class Uninitialized < Attribute # :nodoc:
|
|
def initialize(name, type)
|
|
super(name, nil, type)
|
|
end
|
|
|
|
def value
|
|
if block_given?
|
|
yield name
|
|
end
|
|
end
|
|
|
|
def value_for_database
|
|
end
|
|
|
|
def initialized?
|
|
false
|
|
end
|
|
|
|
def with_type(type)
|
|
self.class.new(name, type)
|
|
end
|
|
end
|
|
private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
|
|
end
|
|
end
|