mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Introduce an object to aid in creation and management of @attributes
Mostly delegation to start, but we can start moving a lot of behavior in bulk to this object.
This commit is contained in:
parent
dccf6da66b
commit
099af48d31
8 changed files with 120 additions and 23 deletions
|
@ -32,6 +32,7 @@ module ActiveRecord
|
|||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :Attribute
|
||||
autoload :AttributeSet
|
||||
autoload :Base
|
||||
autoload :Callbacks
|
||||
autoload :Core
|
||||
|
|
|
@ -230,7 +230,7 @@ module ActiveRecord
|
|||
# For queries selecting a subset of columns, return false for unselected columns.
|
||||
# We check defined?(@attributes) not to issue warnings if called on objects that
|
||||
# have been allocated but not yet initialized.
|
||||
if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name)
|
||||
if defined?(@attributes) && self.class.column_names.include?(name)
|
||||
return has_attribute?(name)
|
||||
end
|
||||
|
||||
|
@ -247,7 +247,7 @@ module ActiveRecord
|
|||
# person.has_attribute?('age') # => true
|
||||
# person.has_attribute?(:nothing) # => false
|
||||
def has_attribute?(attr_name)
|
||||
@attributes.has_key?(attr_name.to_s)
|
||||
@attributes.include?(attr_name.to_s)
|
||||
end
|
||||
|
||||
# Returns an array of names for the attributes available on this object.
|
||||
|
@ -367,12 +367,6 @@ module ActiveRecord
|
|||
|
||||
protected
|
||||
|
||||
def clone_attributes # :nodoc:
|
||||
@attributes.each_with_object({}) do |(name, attr), h|
|
||||
h[name] = attr.dup
|
||||
end
|
||||
end
|
||||
|
||||
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
|
||||
value = send(reader_method, attribute_name)
|
||||
value.duplicable? ? value.clone : value
|
||||
|
|
51
activerecord/lib/active_record/attribute_set.rb
Normal file
51
activerecord/lib/active_record/attribute_set.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
module ActiveRecord
|
||||
class AttributeSet # :nodoc:
|
||||
delegate :[], :[]=, :fetch, :include?, :keys, :each_with_object, to: :attributes
|
||||
|
||||
def initialize(attributes)
|
||||
@attributes = attributes
|
||||
end
|
||||
|
||||
def update(other)
|
||||
attributes.update(other.attributes)
|
||||
end
|
||||
|
||||
def freeze
|
||||
@attributes.freeze
|
||||
super
|
||||
end
|
||||
|
||||
def initialize_dup(_)
|
||||
@attributes = attributes.dup
|
||||
attributes.each do |key, attr|
|
||||
attributes[key] = attr.dup
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def initialize_clone(_)
|
||||
@attributes = attributes.clone
|
||||
super
|
||||
end
|
||||
|
||||
class Builder
|
||||
def initialize(types)
|
||||
@types = types
|
||||
end
|
||||
|
||||
def build_from_database(values, additional_types = {})
|
||||
attributes = values.each_with_object({}) do |(name, value), hash|
|
||||
type = additional_types.fetch(name, @types[name])
|
||||
hash[name] = Attribute.from_database(value, type)
|
||||
end
|
||||
AttributeSet.new(attributes)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :attributes
|
||||
|
||||
end
|
||||
end
|
|
@ -109,13 +109,14 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def clear_caches_calculated_from_columns
|
||||
@attributes_builder = nil
|
||||
@column_defaults = nil
|
||||
@column_names = nil
|
||||
@column_types = nil
|
||||
@columns = nil
|
||||
@columns_hash = nil
|
||||
@column_types = nil
|
||||
@column_defaults = nil
|
||||
@raw_column_defaults = nil
|
||||
@column_names = nil
|
||||
@content_columns = nil
|
||||
@raw_column_defaults = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -251,11 +251,10 @@ module ActiveRecord
|
|||
def initialize(attributes = nil, options = {})
|
||||
defaults = {}
|
||||
self.class.raw_column_defaults.each do |k, v|
|
||||
default = v.duplicable? ? v.dup : v
|
||||
defaults[k] = Attribute.from_database(default, type_for_attribute(k))
|
||||
defaults[k] = v.duplicable? ? v.dup : v
|
||||
end
|
||||
|
||||
@attributes = defaults
|
||||
@attributes = self.class.attributes_builder.build_from_database(defaults)
|
||||
@column_types = self.class.column_types
|
||||
|
||||
init_internals
|
||||
|
@ -325,7 +324,7 @@ module ActiveRecord
|
|||
##
|
||||
def initialize_dup(other) # :nodoc:
|
||||
pk = self.class.primary_key
|
||||
@attributes = other.clone_attributes
|
||||
@attributes = @attributes.dup
|
||||
@attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk))
|
||||
|
||||
run_callbacks(:initialize) unless _initialize_callbacks.empty?
|
||||
|
|
|
@ -219,12 +219,18 @@ module ActiveRecord
|
|||
connection.schema_cache.table_exists?(table_name)
|
||||
end
|
||||
|
||||
def attributes_builder # :nodoc:
|
||||
@attributes_builder ||= AttributeSet::Builder.new(column_types)
|
||||
end
|
||||
|
||||
def column_types # :nodoc:
|
||||
@column_types ||= Hash[columns.map { |column| [column.name, column.cast_type] }]
|
||||
@column_types ||= Hash.new(Type::Value.new).tap do |column_types|
|
||||
columns.each { |column| column_types[column.name] = column.cast_type }
|
||||
end
|
||||
end
|
||||
|
||||
def type_for_attribute(attr_name) # :nodoc:
|
||||
column_types.fetch(attr_name) { Type::Value.new }
|
||||
column_types[attr_name]
|
||||
end
|
||||
|
||||
# Returns a hash where the keys are column names and the values are
|
||||
|
|
|
@ -48,11 +48,7 @@ module ActiveRecord
|
|||
# how this "single-table" inheritance mapping is implemented.
|
||||
def instantiate(attributes, column_types = {})
|
||||
klass = discriminate_class_for_record(attributes)
|
||||
|
||||
attributes = attributes.each_with_object({}) do |(name, value), h|
|
||||
type = column_types.fetch(name) { klass.type_for_attribute(name) }
|
||||
h[name] = Attribute.from_database(value, type)
|
||||
end
|
||||
attributes = klass.attributes_builder.build_from_database(attributes, column_types)
|
||||
klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
|
||||
end
|
||||
|
||||
|
|
49
activerecord/test/cases/attribute_set_test.rb
Normal file
49
activerecord/test/cases/attribute_set_test.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
require 'cases/helper'
|
||||
|
||||
module ActiveRecord
|
||||
class AttributeSetTest < ActiveRecord::TestCase
|
||||
test "building a new set from raw attributes" do
|
||||
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
|
||||
attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
|
||||
|
||||
assert_equal 1, attributes[:foo].value
|
||||
assert_equal 2.2, attributes[:bar].value
|
||||
end
|
||||
|
||||
test "building with custom types" do
|
||||
builder = AttributeSet::Builder.new(foo: Type::Float.new)
|
||||
attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new })
|
||||
|
||||
assert_equal 3.3, attributes[:foo].value
|
||||
assert_equal 4, attributes[:bar].value
|
||||
end
|
||||
|
||||
test "duping creates a new hash and dups each attribute" do
|
||||
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
|
||||
attributes = builder.build_from_database(foo: 1, bar: 'foo')
|
||||
|
||||
# Ensure the type cast value is cached
|
||||
attributes[:foo].value
|
||||
attributes[:bar].value
|
||||
|
||||
duped = attributes.dup
|
||||
duped[:foo] = Attribute.from_database(2, Type::Integer.new)
|
||||
duped[:bar].value << 'bar'
|
||||
|
||||
assert_equal 1, attributes[:foo].value
|
||||
assert_equal 2, duped[:foo].value
|
||||
assert_equal 'foo', attributes[:bar].value
|
||||
assert_equal 'foobar', duped[:bar].value
|
||||
end
|
||||
|
||||
test "freezing cloned set does not freeze original" do
|
||||
attributes = AttributeSet.new({})
|
||||
clone = attributes.clone
|
||||
|
||||
clone.freeze
|
||||
|
||||
assert clone.frozen?
|
||||
assert_not attributes.frozen?
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue