mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
ab8b12eaf6
Instantiating attributes hash from raw database values is one of the
slower part of attributes.
Why that is necessary is to detect mutations. In other words, that isn't
necessary until mutations are happened.
`LazyAttributeHash` which was introduced at 0f29c21
is to instantiate
attribute lazily until first accessing the attribute (i.e.
`Model.find(1)` isn't slow yet, but `Model.find(1).attr_name` is still
slow).
This introduces `LazyAttributeSet` to instantiate attribute more lazily,
it doesn't instantiate attribute until first assigning/dirty checking
the attribute (i.e. `Model.find(1).attr_name` is no longer slow).
It makes attributes access about 35% faster for readonly (non-mutation)
usage.
https://gist.github.com/kamipo/4002c96a02859d8fe6503e26d7be4ad8
Before:
```
IPS
Warming up --------------------------------------
attribute access 1.000 i/100ms
Calculating -------------------------------------
attribute access 3.444 (± 0.0%) i/s - 18.000 in 5.259030s
MEMORY
Calculating -------------------------------------
attribute access 38.902M memsize ( 0.000 retained)
350.044k objects ( 0.000 retained)
15.000 strings ( 0.000 retained)
```
After (with `immutable_strings_by_default = true`):
```
IPS
Warming up --------------------------------------
attribute access 1.000 i/100ms
Calculating -------------------------------------
attribute access 4.652 (±21.5%) i/s - 23.000 in 5.034853s
MEMORY
Calculating -------------------------------------
attribute access 27.782M memsize ( 0.000 retained)
170.044k objects ( 0.000 retained)
15.000 strings ( 0.000 retained)
```
152 lines
4 KiB
Ruby
152 lines
4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "active_model/attribute"
|
|
|
|
module ActiveModel
|
|
class AttributeSet # :nodoc:
|
|
class Builder # :nodoc:
|
|
attr_reader :types, :default_attributes
|
|
|
|
def initialize(types, default_attributes = {})
|
|
@types = types
|
|
@default_attributes = default_attributes
|
|
end
|
|
|
|
def build_from_database(values = {}, additional_types = {})
|
|
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
|
|
LazyAttributeSet.new(attributes)
|
|
end
|
|
end
|
|
end
|
|
|
|
class LazyAttributeSet < AttributeSet # :nodoc:
|
|
def fetch_value(name, &block)
|
|
attributes.fetch_value(name, &block)
|
|
end
|
|
end
|
|
|
|
class LazyAttributeHash # :nodoc:
|
|
delegate :transform_values, :each_key, :each_value, :fetch, :except, to: :materialize
|
|
|
|
def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
|
|
@types = types
|
|
@values = values
|
|
@additional_types = additional_types
|
|
@default_attributes = default_attributes
|
|
@delegate_hash = delegate_hash
|
|
@casted_values = {}
|
|
@materialized = false
|
|
end
|
|
|
|
def key?(key)
|
|
delegate_hash.key?(key) || values.key?(key) || types.key?(key)
|
|
end
|
|
|
|
def [](key)
|
|
delegate_hash[key] || assign_default_value(key)
|
|
end
|
|
|
|
def []=(key, value)
|
|
delegate_hash[key] = value
|
|
end
|
|
|
|
def fetch_value(name, &block)
|
|
if attr = delegate_hash[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 = assign_default_value(name, value_present, value) || Attribute.null(name)
|
|
attr.value(&block)
|
|
end
|
|
end
|
|
end
|
|
|
|
def deep_dup
|
|
dup.tap do |copy|
|
|
copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
|
|
end
|
|
end
|
|
|
|
def initialize_dup(_)
|
|
@delegate_hash = Hash[delegate_hash]
|
|
super
|
|
end
|
|
|
|
def select
|
|
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
|
|
end
|
|
|
|
def ==(other)
|
|
if other.is_a?(LazyAttributeHash)
|
|
materialize == other.materialize
|
|
else
|
|
materialize == other
|
|
end
|
|
end
|
|
|
|
def marshal_dump
|
|
[@types, @values, @additional_types, @default_attributes, @delegate_hash]
|
|
end
|
|
|
|
def marshal_load(values)
|
|
if values.is_a?(Hash)
|
|
ActiveSupport::Deprecation.warn(<<~MSG)
|
|
Marshalling load from legacy attributes format is deprecated and will be removed in Rails 6.2.
|
|
MSG
|
|
empty_hash = {}.freeze
|
|
initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
|
|
@materialized = true
|
|
else
|
|
initialize(*values)
|
|
end
|
|
end
|
|
|
|
protected
|
|
def materialize
|
|
unless @materialized
|
|
values.each_key { |key| self[key] }
|
|
types.each_key { |key| self[key] }
|
|
unless frozen?
|
|
@materialized = true
|
|
end
|
|
end
|
|
delegate_hash
|
|
end
|
|
|
|
private
|
|
attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
|
|
|
|
def assign_default_value(
|
|
name,
|
|
value_present = true,
|
|
value = values.fetch(name) { value_present = false }
|
|
)
|
|
type = additional_types.fetch(name, types[name])
|
|
|
|
if value_present
|
|
delegate_hash[name] = Attribute.from_database(name, value, type, @casted_values[name])
|
|
elsif types.key?(name)
|
|
attr = default_attributes[name]
|
|
if attr
|
|
delegate_hash[name] = attr.dup
|
|
else
|
|
delegate_hash[name] = Attribute.uninitialized(name, type)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|