1
0
Fork 0
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:
Sean Griffin 2014-06-19 11:13:19 -06:00
parent dccf6da66b
commit 099af48d31
8 changed files with 120 additions and 23 deletions

View file

@ -32,6 +32,7 @@ module ActiveRecord
extend ActiveSupport::Autoload extend ActiveSupport::Autoload
autoload :Attribute autoload :Attribute
autoload :AttributeSet
autoload :Base autoload :Base
autoload :Callbacks autoload :Callbacks
autoload :Core autoload :Core

View file

@ -230,7 +230,7 @@ module ActiveRecord
# For queries selecting a subset of columns, return false for unselected columns. # 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 # We check defined?(@attributes) not to issue warnings if called on objects that
# have been allocated but not yet initialized. # 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) return has_attribute?(name)
end end
@ -247,7 +247,7 @@ module ActiveRecord
# person.has_attribute?('age') # => true # person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false # person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name) def has_attribute?(attr_name)
@attributes.has_key?(attr_name.to_s) @attributes.include?(attr_name.to_s)
end end
# Returns an array of names for the attributes available on this object. # Returns an array of names for the attributes available on this object.
@ -367,12 +367,6 @@ module ActiveRecord
protected 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: def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name) value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value value.duplicable? ? value.clone : value

View 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

View file

@ -109,13 +109,14 @@ module ActiveRecord
end end
def clear_caches_calculated_from_columns def clear_caches_calculated_from_columns
@attributes_builder = nil
@column_defaults = nil
@column_names = nil
@column_types = nil
@columns = nil @columns = nil
@columns_hash = nil @columns_hash = nil
@column_types = nil
@column_defaults = nil
@raw_column_defaults = nil
@column_names = nil
@content_columns = nil @content_columns = nil
@raw_column_defaults = nil
end end
end end
end end

View file

@ -251,11 +251,10 @@ module ActiveRecord
def initialize(attributes = nil, options = {}) def initialize(attributes = nil, options = {})
defaults = {} defaults = {}
self.class.raw_column_defaults.each do |k, v| self.class.raw_column_defaults.each do |k, v|
default = v.duplicable? ? v.dup : v defaults[k] = v.duplicable? ? v.dup : v
defaults[k] = Attribute.from_database(default, type_for_attribute(k))
end end
@attributes = defaults @attributes = self.class.attributes_builder.build_from_database(defaults)
@column_types = self.class.column_types @column_types = self.class.column_types
init_internals init_internals
@ -325,7 +324,7 @@ module ActiveRecord
## ##
def initialize_dup(other) # :nodoc: def initialize_dup(other) # :nodoc:
pk = self.class.primary_key pk = self.class.primary_key
@attributes = other.clone_attributes @attributes = @attributes.dup
@attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk)) @attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk))
run_callbacks(:initialize) unless _initialize_callbacks.empty? run_callbacks(:initialize) unless _initialize_callbacks.empty?

View file

@ -219,12 +219,18 @@ module ActiveRecord
connection.schema_cache.table_exists?(table_name) connection.schema_cache.table_exists?(table_name)
end end
def attributes_builder # :nodoc:
@attributes_builder ||= AttributeSet::Builder.new(column_types)
end
def column_types # :nodoc: 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 end
def type_for_attribute(attr_name) # :nodoc: def type_for_attribute(attr_name) # :nodoc:
column_types.fetch(attr_name) { Type::Value.new } column_types[attr_name]
end end
# Returns a hash where the keys are column names and the values are # Returns a hash where the keys are column names and the values are

View file

@ -48,11 +48,7 @@ module ActiveRecord
# how this "single-table" inheritance mapping is implemented. # how this "single-table" inheritance mapping is implemented.
def instantiate(attributes, column_types = {}) def instantiate(attributes, column_types = {})
klass = discriminate_class_for_record(attributes) klass = discriminate_class_for_record(attributes)
attributes = klass.attributes_builder.build_from_database(attributes, column_types)
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
klass.allocate.init_with('attributes' => attributes, 'new_record' => false) klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
end end

View 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