1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Merge pull request #39612 from kamipo/faster_attributes

PERF: 45% faster attributes for readonly usage
This commit is contained in:
Ryuta Kamizono 2020-06-17 20:29:47 +09:00 committed by GitHub
commit d2cdf0be67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 105 additions and 33 deletions

View file

@ -5,16 +5,16 @@ require "active_support/core_ext/object/duplicable"
module ActiveModel
class Attribute # :nodoc:
class << self
def from_database(name, value, type)
FromDatabase.new(name, value, type)
def from_database(name, value_before_type_cast, type, value = nil)
FromDatabase.new(name, value_before_type_cast, type, nil, value)
end
def from_user(name, value, type, original_attribute = nil)
FromUser.new(name, value, type, original_attribute)
def from_user(name, value_before_type_cast, type, original_attribute = nil)
FromUser.new(name, value_before_type_cast, type, original_attribute)
end
def with_cast_value(name, value, type)
WithCastValue.new(name, value, type)
def with_cast_value(name, value_before_type_cast, type)
WithCastValue.new(name, value_before_type_cast, type)
end
def null(name)
@ -30,11 +30,12 @@ module ActiveModel
# This method should not be called directly.
# Use #from_database or #from_user
def initialize(name, value_before_type_cast, type, original_attribute = nil)
def initialize(name, value_before_type_cast, type, original_attribute = nil, value = nil)
@name = name
@value_before_type_cast = value_before_type_cast
@type = type
@original_attribute = original_attribute
@value = value unless value.nil?
end
def value

View file

@ -13,11 +13,11 @@ module ActiveModel
end
def [](name)
attributes[name] || Attribute.null(name)
@attributes[name] || default_attribute(name)
end
def []=(name, value)
attributes[name] = value
@attributes[name] = value
end
def values_before_type_cast
@ -25,9 +25,9 @@ module ActiveModel
end
def to_hash
initialized_attributes.transform_values(&:value)
keys.index_with { |name| self[name].value }
end
alias_method :to_h, :to_hash
alias :to_h :to_hash
def key?(name)
attributes.key?(name) && self[name].initialized?
@ -42,38 +42,36 @@ module ActiveModel
end
def write_from_database(name, value)
attributes[name] = self[name].with_value_from_database(value)
@attributes[name] = self[name].with_value_from_database(value)
end
def write_from_user(name, value)
raise FrozenError, "can't modify frozen attributes" if frozen?
attributes[name] = self[name].with_value_from_user(value)
@attributes[name] = self[name].with_value_from_user(value)
value
end
def write_cast_value(name, value)
attributes[name] = self[name].with_cast_value(value)
@attributes[name] = self[name].with_cast_value(value)
value
end
def freeze
@attributes.freeze
attributes.freeze
super
end
def deep_dup
self.class.allocate.tap do |copy|
copy.instance_variable_set(:@attributes, attributes.deep_dup)
end
AttributeSet.new(attributes.deep_dup)
end
def initialize_dup(_)
@attributes = attributes.dup
@attributes = @attributes.dup
super
end
def initialize_clone(_)
@attributes = attributes.clone
@attributes = @attributes.clone
super
end
@ -84,7 +82,7 @@ module ActiveModel
end
def accessed
attributes.select { |_, attr| attr.has_been_read? }.keys
attributes.each_key.select { |name| self[name].has_been_read? }
end
def map(&block)
@ -100,8 +98,8 @@ module ActiveModel
attr_reader :attributes
private
def initialized_attributes
attributes.select { |_, attr| attr.initialized? }
def default_attribute(name)
Attribute.null(name)
end
end
end

View file

@ -13,14 +13,86 @@ module ActiveModel
end
def build_from_database(values = {}, additional_types = {})
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
AttributeSet.new(attributes)
LazyAttributeSet.new(values, types, additional_types, default_attributes)
end
end
end
class LazyAttributeSet < AttributeSet # :nodoc:
def initialize(values, types, additional_types, default_attributes, attributes = {})
super(attributes)
@values = values
@types = types
@additional_types = additional_types
@default_attributes = default_attributes
@casted_values = {}
@materialized = false
end
def key?(name)
(values.key?(name) || types.key?(name) || @attributes.key?(name)) && self[name].initialized?
end
def keys
keys = values.keys | types.keys | @attributes.keys
keys.keep_if { |name| self[name].initialized? }
end
def fetch_value(name, &block)
if attr = @attributes[name]
return attr.value(&block)
end
@casted_values.fetch(name) do
value_present = true
value = values.fetch(name) { value_present = false }
if value_present
type = additional_types.fetch(name, types[name])
@casted_values[name] = type.deserialize(value)
else
attr = default_attribute(name, value_present, value)
attr.value(&block)
end
end
end
protected
def attributes
unless @materialized
values.each_key { |key| self[key] }
types.each_key { |key| self[key] }
@materialized = true
end
@attributes
end
private
attr_reader :values, :types, :additional_types, :default_attributes
def default_attribute(
name,
value_present = true,
value = values.fetch(name) { value_present = false }
)
type = additional_types.fetch(name, types[name])
if value_present
@attributes[name] = Attribute.from_database(name, value, type, @casted_values[name])
elsif types.key?(name)
if attr = default_attributes[name]
@attributes[name] = attr.dup
else
@attributes[name] = Attribute.uninitialized(name, type)
end
else
Attribute.null(name)
end
end
end
class LazyAttributeHash # :nodoc:
delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
delegate :transform_values, :each_value, :fetch, :except, to: :materialize
def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
@types = types
@ -54,14 +126,9 @@ module ActiveModel
super
end
def select
def each_key(&block)
keys = types.keys | values.keys | delegate_hash.keys
keys.each_with_object({}) do |key, hash|
attribute = self[key]
if yield(key, attribute)
hash[key] = attribute
end
end
keys.each(&block)
end
def ==(other)

View file

@ -219,6 +219,12 @@ module ActiveModel
test "marshalling dump/load legacy materialized attribute hash" do
builder = AttributeSet::Builder.new(foo: Type::String.new)
def builder.build_from_database(values = {}, additional_types = {})
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
AttributeSet.new(attributes)
end
attributes = builder.build_from_database(foo: "1")
attributes.instance_variable_get(:@attributes).instance_eval do