提交 2dca1ba0 编写于 作者: S Sean Griffin

Don't query the database schema when calling `serialize`

We need to decorate the types lazily. This is extracted to a separate
API, as there are other refactorings that will be able to make use of
it, and to allow unit testing the finer points more granularly.
上级 ecd4151a
module ActiveRecord
module AttributeDecorators # :nodoc:
extend ActiveSupport::Concern
included do
class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
self.attribute_type_decorations = Hash.new({})
end
module ClassMethods
def decorate_attribute_type(column_name, decorator_name, &block)
clear_caches_calculated_from_columns
column_name = column_name.to_s
# Create new hashes so we don't modify parent classes
decorations_for_column = attribute_type_decorations[column_name]
new_decorations = decorations_for_column.merge(decorator_name.to_s => block)
self.attribute_type_decorations = attribute_type_decorations.merge(column_name => new_decorations)
end
private
def add_user_provided_columns(*)
super.map do |column|
decorations = attribute_type_decorations[column.name].values
decorated_type = decorations.inject(column.cast_type) do |type, block|
block.call(type)
end
column.with_type(decorated_type)
end
end
end
end
end
......@@ -58,11 +58,9 @@ def serialize(attr_name, class_name_or_coder = Object)
Coders::YAMLColumn.new(class_name_or_coder)
end
type = columns_hash[attr_name.to_s].cast_type
if type.serialized?
type = type.subtype
decorate_attribute_type(attr_name, :serialize) do |type|
Type::Serialized.new(type, coder)
end
property attr_name, Type::Serialized.new(type, coder)
# merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
# has its own hash of own serialized attributes
......
......@@ -15,6 +15,7 @@
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/class/subclasses'
require 'arel'
require 'active_record/attribute_decorators'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
......@@ -323,6 +324,7 @@ class Base
include Serialization
include Store
include Properties
include AttributeDecorators
end
ActiveSupport.run_load_hooks(:active_record, Base)
......
......@@ -61,16 +61,13 @@ def initialize(name, default, cast_type, sql_type = nil, null = true, collation
@collation = collation
@extra = extra
super(name, default, cast_type, sql_type, null)
assert_valid_default(default)
end
def extract_default(default)
if blob_or_text_column?
if default.blank?
null || strict ? nil : ''
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
elsif missing_default_forged_as_empty_string?(default)
def default
@default ||= if blob_or_text_column?
null || strict ? nil : ''
elsif missing_default_forged_as_empty_string?(@original_default)
nil
else
super
......@@ -102,6 +99,12 @@ def case_sensitive?
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
def assert_valid_default(default)
if blob_or_text_column? && default.present?
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
end
end
##
......
......@@ -13,7 +13,7 @@ module Format
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
attr_reader :name, :default, :cast_type, :null, :sql_type, :default_function
attr_reader :name, :cast_type, :null, :sql_type, :default_function
delegate :type, :precision, :scale, :limit, :klass, :accessor,
:text?, :number?, :binary?, :serialized?, :changed?,
......@@ -35,7 +35,7 @@ def initialize(name, default, cast_type, sql_type = nil, null = true)
@cast_type = cast_type
@sql_type = sql_type
@null = null
@default = extract_default(default)
@original_default = default
@default_function = nil
end
......@@ -51,8 +51,15 @@ def human_name
Base.human_attribute_name(@name)
end
def extract_default(default)
type_cast(default)
def default
@default ||= type_cast(@original_default)
end
def with_type(type)
dup.tap do |clone|
clone.instance_variable_set('@default', nil)
clone.instance_variable_set('@cast_type', type)
end
end
end
......
require 'cases/helper'
module ActiveRecord
class AttributeDecoratorsTest < ActiveRecord::TestCase
class Model < ActiveRecord::Base
self.table_name = 'attribute_decorators_model'
end
class StringDecorator < SimpleDelegator
def initialize(delegate, decoration = "decorated!")
@decoration = decoration
super(delegate)
end
def type_cast(value)
"#{super} #{@decoration}"
end
end
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table :attribute_decorators_model, force: true do |t|
t.string :a_string
end
end
teardown do
return unless @connection
@connection.execute 'DROP TABLE IF EXISTS attribute_decorators_model'
Model.attribute_type_decorations.clear
Model.reset_column_information
end
test "attributes can be decorated" do
model = Model.new(a_string: 'Hello')
assert_equal 'Hello', model.a_string
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
model = Model.new(a_string: 'Hello')
assert_equal 'Hello decorated!', model.a_string
end
test "decoration does not eagerly load existing columns" do
assert_no_queries do
Model.reset_column_information
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
end
end
test "undecorated columns are not touched" do
Model.property :another_string, Type::String.new, default: 'something or other'
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
assert_equal 'something or other', Model.new.another_string
end
test "decorators can be chained" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
model = Model.new(a_string: 'Hello!')
assert_equal 'Hello! decorated! decorated!', model.a_string
end
test "decoration of the same type multiple times is idempotent" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
model = Model.new(a_string: 'Hello')
assert_equal 'Hello decorated!', model.a_string
end
test "decorations occur in order of declaration" do
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
Model.decorate_attribute_type(:a_string, :other) do |type|
StringDecorator.new(type, 'decorated again!')
end
model = Model.new(a_string: 'Hello!')
assert_equal 'Hello! decorated! decorated again!', model.a_string
end
test "decorating attributes does not modify parent classes" do
Model.property :another_string, Type::String.new, default: 'whatever'
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
child_class = Class.new(Model)
child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
model = Model.new(a_string: 'Hello!')
child = child_class.new(a_string: 'Hello!')
assert_equal 'Hello! decorated!', model.a_string
assert_equal 'whatever', model.another_string
assert_equal 'Hello! decorated! decorated!', child.a_string
# We are round tripping the default, and we don't undo our decoration
assert_equal 'whatever decorated! decorated!', child.another_string
end
test "defaults are decorated on the column" do
Model.property :a_string, Type::String.new, default: 'whatever'
Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
column = Model.columns_hash['a_string']
assert_equal 'whatever decorated!', column.default
end
end
end
......@@ -14,6 +14,13 @@ class SerializedAttributeTest < ActiveRecord::TestCase
Topic.serialize("content")
end
def test_serialize_does_not_eagerly_load_columns
assert_no_queries do
Topic.reset_column_information
Topic.serialize(:content)
end
end
def test_list_of_serialized_attributes
assert_equal %w(content), Topic.serialized_attributes.keys
end
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册