Skip to content

Commit 2c8428c

Browse files
Merge pull request #44666 from jonathanhefner/active_record-use-attribute_registration
Use `ActiveModel::AttributeRegistration` in AR
2 parents a8a5ec4 + e0a55b0 commit 2c8428c

File tree

10 files changed

+75
-88
lines changed

10 files changed

+75
-88
lines changed

activemodel/lib/active_model/attribute_registration.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module ClassMethods # :nodoc:
1212
def attribute(name, type = nil, default: (no_default = true), **options)
1313
name = resolve_attribute_name(name)
1414
type = resolve_type_name(type, **options) if type.is_a?(Symbol)
15+
type = hook_attribute_type(name, type) if type
1516

1617
pending_attribute_modifications << PendingType.new(name, type) if type || no_default
1718
pending_attribute_modifications << PendingDefault.new(name, default) unless no_default
@@ -76,9 +77,13 @@ def apply_pending_attribute_modifications(attribute_set)
7677
end
7778

7879
def reset_default_attributes
80+
reset_default_attributes!
81+
subclasses.each { |subclass| subclass.send(__method__) }
82+
end
83+
84+
def reset_default_attributes!
7985
@default_attributes = nil
8086
@attribute_types = nil
81-
subclasses.each { |subclass| subclass.send(__method__) }
8287
end
8388

8489
def resolve_attribute_name(name)
@@ -88,6 +93,13 @@ def resolve_attribute_name(name)
8893
def resolve_type_name(name, **options)
8994
Type.lookup(name, **options)
9095
end
96+
97+
# Hook for other modules to override. The attribute type is passed
98+
# through this method immediately after it is resolved, before any type
99+
# decorations are applied.
100+
def hook_attribute_type(attribute, type)
101+
type
102+
end
91103
end
92104
end
93105
end

activerecord/lib/active_record/attribute_methods/serialization.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ def serialize(attr_name, class_name_or_coder = nil, coder: nil, type: Object, ya
214214

215215
column_serializer = build_column_serializer(attr_name, coder, type, yaml)
216216

217-
attribute(attr_name, **options) do |cast_type|
217+
attribute(attr_name, **options)
218+
219+
decorate_attributes([attr_name]) do |attr_name, cast_type|
218220
if type_incompatible_with_serialize?(cast_type, coder, type)
219221
raise ColumnNotSerializableError.new(attr_name, cast_type)
220222
end

activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,14 @@ def map_avoiding_infinite_recursion(value)
6969
end
7070

7171
module ClassMethods # :nodoc:
72-
def define_attribute(name, cast_type, **)
73-
if create_time_zone_conversion_attribute?(name, cast_type)
74-
cast_type = TimeZoneConverter.new(cast_type)
72+
private
73+
def hook_attribute_type(name, cast_type)
74+
if create_time_zone_conversion_attribute?(name, cast_type)
75+
cast_type = TimeZoneConverter.new(cast_type)
76+
end
77+
super
7578
end
76-
super
77-
end
7879

79-
private
8080
def create_time_zone_conversion_attribute?(name, cast_type)
8181
enabled_for_column = time_zone_aware_attributes &&
8282
!skip_time_zone_conversion_for_attributes.include?(name.to_sym)

activerecord/lib/active_record/attributes.rb

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ module ActiveRecord
66
# See ActiveRecord::Attributes::ClassMethods for documentation
77
module Attributes
88
extend ActiveSupport::Concern
9+
include ActiveModel::AttributeRegistration
910

10-
included do
11-
class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal:
12-
end
1311
# = Active Record \Attributes
1412
module ClassMethods
13+
# :method: attribute
14+
# :call-seq: attribute(name, cast_type = nil, **options)
15+
#
1516
# Defines an attribute with a type on this model. It will override the
1617
# type of existing attributes if needed. This allows control over how
1718
# values are converted to and from SQL when assigned to a model. It also
@@ -205,37 +206,13 @@ module ClassMethods
205206
# tracking is performed. The methods +changed?+ and +changed_in_place?+
206207
# will be called from ActiveModel::Dirty. See the documentation for those
207208
# methods in ActiveModel::Type::Value for more details.
208-
def attribute(name, cast_type = nil, default: NO_DEFAULT_PROVIDED, **options)
209-
name = name.to_s
210-
name = attribute_aliases[name] || name
211-
212-
reload_schema_from_cache
213-
214-
case cast_type
215-
when Symbol
216-
cast_type = Type.lookup(cast_type, **options, adapter: Type.adapter_name_from(self))
217-
when nil
218-
if (prev_cast_type, prev_default = attributes_to_define_after_schema_loads[name])
219-
default = prev_default if default == NO_DEFAULT_PROVIDED
220-
else
221-
prev_cast_type = -> subtype { subtype }
222-
end
223-
224-
cast_type = if block_given?
225-
-> subtype { yield Proc === prev_cast_type ? prev_cast_type[subtype] : prev_cast_type }
226-
else
227-
prev_cast_type
228-
end
229-
end
230-
231-
self.attributes_to_define_after_schema_loads =
232-
attributes_to_define_after_schema_loads.merge(name => [cast_type, default])
233-
end
209+
#
210+
#--
211+
# Implemented by ActiveModel::AttributeRegistration#attribute.
234212

235213
# This is the low level API which sits beneath +attribute+. It only
236214
# accepts type objects, and will do its work immediately instead of
237-
# waiting for the schema to load. Automatic schema detection and
238-
# ClassMethods#attribute both call this under the hood. While this method
215+
# waiting for the schema to load. While this method
239216
# is provided so it can be used by plugin authors, application code
240217
# should probably use ClassMethods#attribute.
241218
#
@@ -260,14 +237,25 @@ def define_attribute(
260237
define_default_attribute(name, default, cast_type, from_user: user_provided_default)
261238
end
262239

263-
def load_schema! # :nodoc:
264-
super
265-
attributes_to_define_after_schema_loads.each do |name, (cast_type, default)|
266-
cast_type = cast_type[type_for_attribute(name)] if Proc === cast_type
267-
define_attribute(name, cast_type, default: default)
240+
def _default_attributes # :nodoc:
241+
@default_attributes ||= begin
242+
attributes_hash = columns_hash.transform_values do |column|
243+
ActiveModel::Attribute.from_database(column.name, column.default, type_for_column(column))
244+
end
245+
246+
attribute_set = ActiveModel::AttributeSet.new(attributes_hash)
247+
apply_pending_attribute_modifications(attribute_set)
248+
attribute_set
268249
end
269250
end
270251

252+
def reload_schema_from_cache(*)
253+
reset_default_attributes!
254+
super
255+
end
256+
257+
alias :reset_default_attributes :reload_schema_from_cache
258+
271259
private
272260
NO_DEFAULT_PROVIDED = Object.new # :nodoc:
273261
private_constant :NO_DEFAULT_PROVIDED
@@ -287,6 +275,14 @@ def define_default_attribute(name, value, type, from_user:)
287275
end
288276
_default_attributes[name] = default_attribute
289277
end
278+
279+
def resolve_type_name(name, **options)
280+
Type.lookup(name, **options, adapter: Type.adapter_name_from(self))
281+
end
282+
283+
def type_for_column(column)
284+
hook_attribute_type(column.name, super)
285+
end
290286
end
291287
end
292288
end

activerecord/lib/active_record/encryption/encryptable_record.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def global_previous_schemes_for(scheme)
8484
def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
8585
encrypted_attributes << name.to_sym
8686

87-
attribute name do |cast_type|
87+
decorate_attributes([name]) do |name, cast_type|
8888
scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, \
8989
downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
9090

activerecord/lib/active_record/enum.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,9 @@ def self.extended(base) # :nodoc:
168168
end
169169

170170
def load_schema! # :nodoc:
171-
attributes_to_define_after_schema_loads.each do |name, (cast_type, _default)|
172-
unless columns_hash.key?(name)
173-
cast_type = cast_type[type_for_attribute(name)] if Proc === cast_type
174-
raise "Unknown enum attribute '#{name}' for #{self.name}" if Enum::EnumType === cast_type
171+
defined_enums.each_key do |name|
172+
unless columns_hash.key?(resolve_attribute_name(name))
173+
raise "Unknown enum attribute '#{name}' for #{self.name}"
175174
end
176175
end
177176
end
@@ -254,7 +253,9 @@ def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods
254253
detect_enum_conflict!(name, name)
255254
detect_enum_conflict!(name, "#{name}=")
256255

257-
attribute(name, **options) do |subtype|
256+
attribute(name, **options)
257+
258+
decorate_attributes([name]) do |name, subtype|
258259
subtype = subtype.subtype if EnumType === subtype
259260
EnumType.new(name, enum_values, subtype, raise_on_invalid_values: !validate)
260261
end

activerecord/lib/active_record/locking/optimistic.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,14 @@ def update_counters(id, counters)
182182
super
183183
end
184184

185-
def define_attribute(name, cast_type, **) # :nodoc:
186-
if lock_optimistically && name == locking_column
187-
cast_type = LockingType.new(cast_type)
185+
private
186+
def hook_attribute_type(name, cast_type)
187+
if lock_optimistically && name == locking_column
188+
cast_type = LockingType.new(cast_type)
189+
end
190+
super
188191
end
189-
super
190-
end
191192

192-
private
193193
def inherited(base)
194194
super
195195
base.class_eval do

activerecord/lib/active_record/model_schema.rb

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -435,11 +435,6 @@ def _returning_columns_for_insert # :nodoc:
435435
end
436436
end
437437

438-
def attribute_types # :nodoc:
439-
load_schema
440-
@attribute_types ||= Hash.new(Type.default_value)
441-
end
442-
443438
def yaml_encoder # :nodoc:
444439
@yaml_encoder ||= ActiveModel::AttributeSet::YAMLEncoder.new(attribute_types)
445440
end
@@ -493,11 +488,6 @@ def column_defaults
493488
@column_defaults ||= _default_attributes.deep_dup.to_hash.freeze
494489
end
495490

496-
def _default_attributes # :nodoc:
497-
load_schema
498-
@default_attributes ||= ActiveModel::AttributeSet.new({})
499-
end
500-
501491
# Returns an array of column names as strings.
502492
def column_names
503493
@column_names ||= columns.map(&:name).freeze
@@ -577,9 +567,7 @@ def reload_schema_from_cache(recursive = true)
577567
@arel_table = nil
578568
@column_names = nil
579569
@symbol_column_to_string_name_hash = nil
580-
@attribute_types = nil
581570
@content_columns = nil
582-
@default_attributes = nil
583571
@column_defaults = nil
584572
@attributes_builder = nil
585573
@columns = nil
@@ -616,17 +604,7 @@ def load_schema!
616604
columns_hash = connection.schema_cache.columns_hash(table_name)
617605
columns_hash = columns_hash.except(*ignored_columns) unless ignored_columns.empty?
618606
@columns_hash = columns_hash.freeze
619-
@columns_hash.each do |name, column|
620-
type = connection.lookup_cast_type_from_column(column)
621-
type = _convert_type_from_options(type)
622-
define_attribute(
623-
name,
624-
type,
625-
default: column.default,
626-
user_provided_default: false
627-
)
628-
alias_attribute :id_value, :id if name == "id"
629-
end
607+
alias_attribute :id_value, :id if @columns_hash.key?("id")
630608

631609
super
632610
end
@@ -654,12 +632,12 @@ def compute_table_name
654632
end
655633
end
656634

657-
def _convert_type_from_options(type)
635+
def type_for_column(column)
636+
type = connection.lookup_cast_type_from_column(column)
658637
if immutable_strings_by_default && type.respond_to?(:to_immutable_string)
659-
type.to_immutable_string
660-
else
661-
type
638+
type = type.to_immutable_string
662639
end
640+
type
663641
end
664642
end
665643
end

activerecord/lib/active_record/normalization.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,8 @@ module ClassMethods
7878
#
7979
# User.normalize_value_for(:phone, "+1 (555) 867-5309") # => "5558675309"
8080
def normalizes(*names, with:, apply_to_nil: false)
81-
names.each do |name|
82-
attribute(name) do |cast_type|
83-
NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
84-
end
81+
decorate_attributes(names) do |name, cast_type|
82+
NormalizedValueType.new(cast_type: cast_type, normalizer: with, normalize_nil: apply_to_nil)
8583
end
8684

8785
self.normalized_attributes += names.map(&:to_sym)

activerecord/test/cases/base_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def test_incomplete_schema_loading
128128

129129
Topic.reset_column_information
130130

131-
Topic.connection.stub(:lookup_cast_type_from_column, ->(_) { raise "Some Error" }) do
131+
Topic.connection.stub(:schema_cache, -> { raise "Some Error" }) do
132132
assert_raises RuntimeError do
133133
Topic.columns_hash
134134
end

0 commit comments

Comments
 (0)