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

Reduce the amount of work performed when instantiating AR models

We don't know which attributes will or won't be used, and we don't want
to create massive bottlenecks at instantiation. Rather than doing *any*
iteration over types and values, we can lazily instantiate the object.

The lazy attribute hash should not fully implement hash, or subclass
hash at any point in the future. It is not meant to be a replacement,
but instead implement its own interface which happens to overlap.
This commit is contained in:
Sean Griffin 2014-11-14 11:20:28 -07:00
parent 70d1b5a7f8
commit 0f29c21607
6 changed files with 80 additions and 28 deletions

View file

@ -120,6 +120,7 @@ module ActiveRecord
def primary_key=(value) def primary_key=(value)
@primary_key = value && value.to_s @primary_key = value && value.to_s
@quoted_primary_key = nil @quoted_primary_key = nil
@attributes_builder = nil
end end
end end
end end

View file

@ -2,8 +2,6 @@ require 'active_record/attribute_set/builder'
module ActiveRecord module ActiveRecord
class AttributeSet # :nodoc: class AttributeSet # :nodoc:
delegate :keys, to: :initialized_attributes
def initialize(attributes) def initialize(attributes)
@attributes = attributes @attributes = attributes
end end
@ -25,6 +23,10 @@ module ActiveRecord
attributes.key?(name) && self[name].initialized? attributes.key?(name) && self[name].initialized?
end end
def keys
attributes.initialized_keys
end
def fetch_value(name, &block) def fetch_value(name, &block)
self[name].value(&block) self[name].value(&block)
end end
@ -43,7 +45,7 @@ module ActiveRecord
end end
def initialize_dup(_) def initialize_dup(_)
@attributes = attributes.transform_values(&:dup) @attributes = attributes.dup
super super
end end
@ -58,12 +60,6 @@ module ActiveRecord
end end
end end
def ensure_initialized(key)
unless self[key].initialized?
write_from_database(key, nil)
end
end
protected protected
attr_reader :attributes attr_reader :attributes

View file

@ -1,35 +1,83 @@
module ActiveRecord module ActiveRecord
class AttributeSet # :nodoc: class AttributeSet # :nodoc:
class Builder # :nodoc: class Builder # :nodoc:
attr_reader :types attr_reader :types, :always_initialized
def initialize(types) def initialize(types, always_initialized = nil)
@types = types @types = types
@always_initialized = always_initialized
end end
def build_from_database(values = {}, additional_types = {}) def build_from_database(values = {}, additional_types = {})
attributes = build_attributes_from_values(values, additional_types) if always_initialized && !values.key?(always_initialized)
add_uninitialized_attributes(attributes) values[always_initialized] = nil
end
attributes = LazyAttributeHash.new(types, values, additional_types)
AttributeSet.new(attributes) AttributeSet.new(attributes)
end end
private private
end
end
def build_attributes_from_values(values, additional_types) class LazyAttributeHash
values.each_with_object({}) do |(name, value), hash| delegate :select, :transform_values, to: :materialize
type = additional_types.fetch(name, types[name]) delegate :[], :[]=, :freeze, to: :delegate_hash
hash[name] = Attribute.from_database(name, value, type)
end
end
def add_uninitialized_attributes(attributes) def initialize(types, values, additional_types)
types.each_key do |name| @types = types
next if attributes.key? name @values = values
type = types[name] @additional_types = additional_types
attributes[name] = @materialized = false
Attribute.uninitialized(name, type) @delegate_hash = {}
assign_default_proc
end
def key?(key)
delegate_hash.key?(key) || values.key?(key) || types.key?(key)
end
def initialized_keys
delegate_hash.keys | values.keys
end
def initialize_dup(_)
@delegate_hash = delegate_hash.transform_values(&:dup)
assign_default_proc
super
end
def initialize_clone(_)
@delegate_hash = delegate_hash.clone
super
end
protected
attr_reader :types, :values, :additional_types, :delegate_hash
private
def assign_default_proc
delegate_hash.default_proc = proc do |hash, name|
type = additional_types.fetch(name, types[name])
if values.key?(name)
hash[name] = Attribute.from_database(name, values[name], type)
elsif type
hash[name] = Attribute.uninitialized(name, type)
end end
end end
end end
def materialize
unless @materialized
values.each_key { |key| delegate_hash[key] }
types.each_key { |key| delegate_hash[key] }
@materialized = true
end
delegate_hash
end
end end
end end

View file

@ -536,8 +536,6 @@ module ActiveRecord
end end
def init_internals def init_internals
@attributes.ensure_initialized(self.class.primary_key)
@aggregation_cache = {} @aggregation_cache = {}
@association_cache = {} @association_cache = {}
@readonly = false @readonly = false

View file

@ -231,7 +231,7 @@ module ActiveRecord
end end
def attributes_builder # :nodoc: def attributes_builder # :nodoc:
@attributes_builder ||= AttributeSet::Builder.new(column_types) @attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key)
end end
def column_types # :nodoc: def column_types # :nodoc:

View file

@ -123,6 +123,15 @@ module ActiveRecord
assert_nil attributes.fetch_value(:bar) assert_nil attributes.fetch_value(:bar)
end end
test "the primary_key is always initialized" do
builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo)
attributes = builder.build_from_database
assert attributes.key?(:foo)
assert_equal [:foo], attributes.keys
assert attributes[:foo].initialized?
end
class MyType class MyType
def type_cast_from_user(value) def type_cast_from_user(value)
return if value.nil? return if value.nil?