If you've ever created a Ruby on Rails application, you've used AttributeMethods. These two modules -- one in ActiveModel, another in ActiveRecord -- are included in every model in your application, their methods called every time you read or write to an attribute. Given their importance, you would think there would be a lot of information about them out there, but actually there's very little.
https://kichijojikaigi.doorkeeper.jp/events/66983
https://github.com/rails/rails/pull/30895
KichijojiKaigi #12
A Brief Introduction to
A Brief Introduction to
AttributeMethods
AttributeMethods
by Chris Salzberg
きっかけ
Model Ancestry
Topic.ancestors
=> [Topic(...),
Topic::GeneratedAssociationMethods,
#,
ActiveRecord::Base,
ActiveRecord::Suppressor,
ActiveRecord::SecureToken,
ActiveRecord::Store,
ActiveRecord::Serialization,
ActiveModel::Serializers::JSON,
ActiveModel::Serialization,
ActiveRecord::Reflection,
ActiveRecord::NoTouching,
ActiveRecord::TouchLater,
...
Topic.ancestors
=> [Topic(...),
Topic::GeneratedAssociationMethods,
#,
ActiveRecord::Base,
ActiveRecord::Suppressor,
ActiveRecord::SecureToken,
ActiveRecord::Store,
ActiveRecord::Serialization,
ActiveModel::Serializers::JSON,
ActiveModel::Serialization,
ActiveRecord::Reflection,
ActiveRecord::NoTouching,
ActiveRecord::TouchLater,
...
class Topic < ActiveRecord::Base
# ...
end
class Topic < ActiveRecord::Base
# ...
end
Model Ancestry
Topic.ancestors
=> [Topic(...),
Topic::GeneratedAssociationMethods,
#,
ActiveRecord::Base,
ActiveRecord::Base,
ActiveRecord::Suppressor,
ActiveRecord::SecureToken,
ActiveRecord::Store,
ActiveRecord::Serialization,
ActiveModel::Serializers::JSON,
ActiveModel::Serialization,
ActiveRecord::Reflection,
ActiveRecord::NoTouching,
ActiveRecord::TouchLater,
...
Topic.ancestors
=> [Topic(...),
Topic::GeneratedAssociationMethods,
#,
ActiveRecord::Base,
ActiveRecord::Base,
ActiveRecord::Suppressor,
ActiveRecord::SecureToken,
ActiveRecord::Store,
ActiveRecord::Serialization,
ActiveModel::Serializers::JSON,
ActiveModel::Serialization,
ActiveRecord::Reflection,
ActiveRecord::NoTouching,
ActiveRecord::TouchLater,
...
class Topic < ActiveRecord::Base
# ...
end
class Topic < ActiveRecord::Base
# ...
end
What’s this?
Generated Attribute Methods
generated_methods = Topic.ancestors[2]
=> #
generated_methods.instance_methods(false)
=> []
Topic.new
=> #
generated_methods.instance_methods(false)
=> [:type=,
:content,
:title_before_type_cast,
:title_came_from_user?,
:title_changed?,
:title_change,
:title_will_change!,
:title_was,
:title_previously_changed?,
:title_previous_change,
:restore_title!,
:saved_change_to_title?,
:saved_change_to_title,
:title_before_last_save,
:will_save_change_to_title?,
:title_change_to_be_saved,
:title_in_database,
... ]
generated_methods = Topic.ancestors[2]
=> #
generated_methods.instance_methods(false)
=> []
Topic.new
=> #
generated_methods.instance_methods(false)
=> [:type=,
:content,
:title_before_type_cast,
:title_came_from_user?,
:title_changed?,
:title_change,
:title_will_change!,
:title_was,
:title_previously_changed?,
:title_previous_change,
:restore_title!,
:saved_change_to_title?,
:saved_change_to_title,
:title_before_last_save,
:will_save_change_to_title?,
:title_change_to_be_saved,
:title_in_database,
... ]
Generated Attribute Methods
generated_methods = Topic.ancestors[2]
=> #
generated_methods.instance_methods(false)
=> []
Topic.new
=> #
generated_methods.instance_methods(false)
=> [:type=,
:content,
:title_before_type_cast,
:title_came_from_user?,
:title_changed?,
:title_change,
:title_will_change!,
:title_was,
:title_previously_changed?,
:title_previous_change,
:restore_title!,
:saved_change_to_title?,
:saved_change_to_title,
:title_before_last_save,
:will_save_change_to_title?,
:title_change_to_be_saved,
:title_in_database,
... ]
generated_methods = Topic.ancestors[2]
=> #
generated_methods.instance_methods(false)
=> []
Topic.new
=> #
generated_methods.instance_methods(false)
=> [:type=,
:content,
:title_before_type_cast,
:title_came_from_user?,
:title_changed?,
:title_change,
:title_will_change!,
:title_was,
:title_previously_changed?,
:title_previous_change,
:restore_title!,
:saved_change_to_title?,
:saved_change_to_title,
:title_before_last_save,
:will_save_change_to_title?,
:title_change_to_be_saved,
:title_in_database,
... ]
?
My PR
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
My PR
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
ActiveRecord::AttributeMethods::Dirty
My PR
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
ActiveRecord::AttributeMethods::Dirty
ActiveModel::Dirty
My PR
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
ActiveRecord::AttributeMethods::Dirty
ActiveModel::Dirty
ActiveRecord::AttributeMethods::Query
ActiveRecord::AttributeMethods::BeforeTypeCast
AttributeMethods: Rails has Two
●
ActiveModel::AttributeMethods
– Virtual attributes
– Prefixed/suffixed methods map to handlers
●
ActiveRecord::AttributeMethods
– Persisted attributes
– Uses prefixes/suffixes to define:
●
reader/writer/query methods
●
(persisted) dirty attributes
●
type casting methods
ActiveModel
ActiveModel
::
::
AttributeMethods
AttributeMethods
Provides a way to add prefixes and suffixes
to your methods as well as handling the
creation of ActiveRecord::Base-like class
methods such as table_name.
Summary
Provides a way to add prefixes and suffixes
to your methods as well as handling the
creation of ActiveRecord::Base-like class
methods such as table_name.
Summary
Usage
●
include ActiveModel::AttributeMethods in your class
●
Call each of its methods you want to add, such as
attribute_method_suffix or attribute_method_prefix.
●
Call define_attributes after the other methods are called.
●
Define the various generic _attribute methods that you have
declared (“handlers”)
●
Define an attributes method which returns a hash with each
attribute name in your model as hash key and the attribute value
as hash value. Hash keys must be strings.
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
Include the Module
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
Define prefixes/suffixes
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
Define Handlers
Handling Attribute Methods
●
Define prefix/suffix/affix and corresponding
handlers which receive attribute name
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
attribute_method_suffix "_previously_changed?", "_previous_change"
attribute_method_affix prefix: "restore_", suffix: "!"
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
attribute_method_suffix "_previously_changed?", "_previous_change"
attribute_method_affix prefix: "restore_", suffix: "!"
def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
!!changes_include?(attr) &&
(to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
(from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
end
def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
!!changes_include?(attr) &&
(to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) &&
(from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
end
def attribute_method_prefix(*prefixes)
self.attribute_method_matchers += prefixes.map! do |prefix|
AttributeMethodMatcher.new prefix: prefix
end
undefine_attribute_methods
end
def attribute_method_prefix(*prefixes)
self.attribute_method_matchers += prefixes.map! do |prefix|
AttributeMethodMatcher.new prefix: prefix
end
undefine_attribute_methods
end
def attribute_method_suffix(*suffixes)
self.attribute_method_matchers += suffixes.map! do |suffix|
AttributeMethodMatcher.new suffix: suffix
end
undefine_attribute_methods
end
def attribute_method_suffix(*suffixes)
self.attribute_method_matchers += suffixes.map! do |suffix|
AttributeMethodMatcher.new suffix: suffix
end
undefine_attribute_methods
end
def attribute_method_affix(*affixes)
self.attribute_method_matchers += affixes.map! do |affix|
AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix]
end
undefine_attribute_methods
end
def attribute_method_affix(*affixes)
self.attribute_method_matchers += affixes.map! do |affix|
AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix]
end
undefine_attribute_methods
end
Prefixes/Suffixes/Affixes
Attribute Method Matcher
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
Attribute Method Matcher
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
Attribute Method Matcher
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
Attribute Method Matcher
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
Attribute Method Matcher
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
class AttributeMethodMatcher
attr_reader :prefix, :suffix, :method_missing_target
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
def match(method_name)
if @regex =~ method_name
AttributeMethodMatch.new(method_missing_target, $1, method_name)
end
end
def method_name(attr_name)
@method_name % attr_name
end
def plain?
prefix.empty? && suffix.empty?
end
end
“Method Missing Target”
"#{@prefix}attribute#{@suffix}"
“Method Missing”?
# Allows access to the object attributes, which are held in the hash
# returned by attributes, as though they were first-class
# methods. So a +Person+ class with a +name+ attribute can for example use
# Person#name and Person#name= and never directly use
# the attributes hash -- except for multiple assignments with
# ActiveRecord::Base#attributes=.
#
# It's also possible to instantiate related objects, so a Client
# class belonging to the +clients+ table with a +master_id+ foreign key
# can instantiate master through Client#master.
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
# Allows access to the object attributes, which are held in the hash
# returned by attributes, as though they were first-class
# methods. So a +Person+ class with a +name+ attribute can for example use
# Person#name and Person#name= and never directly use
# the attributes hash -- except for multiple assignments with
# ActiveRecord::Base#attributes=.
#
# It's also possible to instantiate related objects, so a Client
# class belonging to the +clients+ table with a +master_id+ foreign key
# can instantiate master through Client#master.
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
“Method Missing”?
#
#
#
#
#
#
#
# It's also possible to instantiate related objects, so a Client
# class belonging to the +clients+ table with a +master_id+ foreign key
# can instantiate master through Client#master.
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
#
#
#
#
#
#
#
# It's also possible to instantiate related objects, so a Client
# class belonging to the +clients+ table with a +master_id+ foreign key
# can instantiate master through Client#master.
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
# Allows access to the object attributes, which are held in the hash
# returned by attributes, as though they were first-class
# methods. So a +Person+ class with a +name+ attribute can for example use
# Person#name and Person#name= and never directly use
# the attributes hash -- except for multiple assignments with
# ActiveRecord::Base#attributes=.
Method Missing Matching
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
Method Missing Matching
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
Method Missing Matching
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
Method Missing Matching
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
“Attribute Method”?
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
attribute_method?
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
attribute_method?
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
def attributes
{ 'name' => @name }
end
def attributes
{ 'name' => @name }
end
attribute_method?
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
def attributes
{ 'name' => @name }
end
def attributes
{ 'name' => @name }
end
●
Returns true if the attribute name is a key on
the model’s attributes hash
Attribute Missing?
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def matched_attribute_method(method_name)
matches = self.class.send(:attribute_method_matchers_matching, method_name)
matches.detect { |match| attribute_method?(match.attr_name) }
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
def self.attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no
# prefix/suffix will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
matchers.map { |method| method.match(method_name) }.compact
end
end
attribute_missing
# +attribute_missing+ is like +method_missing+, but for attributes. When
# +method_missing+ is called we check to see if there is a matching
# attribute method. If so, we tell +attribute_missing+ to dispatch the
# attribute. This method can be overloaded to customize the behavior.
def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
end
# +attribute_missing+ is like +method_missing+, but for attributes. When
# +method_missing+ is called we check to see if there is a matching
# attribute method. If so, we tell +attribute_missing+ to dispatch the
# attribute. This method can be overloaded to customize the behavior.
def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
end
attribute_missing
#
#
#
#
def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
end
#
#
#
#
def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
end
# +attribute_missing+ is like +method_missing+, but for attributes. When
# +method_missing+ is called we check to see if there is a matching
# attribute method. If so, we tell +attribute_missing+ to dispatch the
# attribute. This method can be overloaded to customize the behavior.
●
attribute_missing is method_missing,
but for attributes
– if reset_foo_to_default! matches prefix/suffix
matchers, attribute_missing is called with the
match target, the attribute, and any args/block
attribute_missing
# +attribute_missing+ is like +method_missing+, but for attributes. When
# +method_missing+ is called we check to see if there is a matching
# attribute method. If so, we tell +attribute_missing+ to dispatch the
# attribute. This method can be overloaded to customize the behavior.
def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
end
# +attribute_missing+ is like +method_missing+, but for attributes. When
# +method_missing+ is called we check to see if there is a matching
# attribute method. If so, we tell +attribute_missing+ to dispatch the
# attribute. This method can be overloaded to customize the behavior.
def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
end
@prefix @suffix attribute
name
reset_attribute_to_default!(“name”)
reset_attribute_to_default!(“name”)
Method Missing Flow
person.reset_name_to_default!
person.reset_name_to_default!
Method Missing Flow
person.reset_name_to_default!
person.reset_name_to_default!
Which matchers have matches?
matchers.map { |method| method.match(method_name) }.compact
#=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
matchers.map { |method| method.match(method_name) }.compact
#=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
Method Missing Flow
person.reset_name_to_default!
person.reset_name_to_default!
Which matchers have matches?
matchers.map { |method| method.match(method_name) }.compact
#=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
matchers.map { |method| method.match(method_name) }.compact
#=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
Which are included in attributes?
{ ‘name’ => @name }.include?(“name”)
{ ‘name’ => @name }.include?(“name”)
Method Missing Flow
person.reset_name_to_default!
person.reset_name_to_default!
Which matchers have matches?
matchers.map { |method| method.match(method_name) }.compact
#=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
matchers.map { |method| method.match(method_name) }.compact
#=> [## target=”reset_attribute_to_default!” attr_name=”name” ...>]
Which are included in attributes?
{ ‘name’ => @name }.include?(“name”)
{ ‘name’ => @name }.include?(“name”)
attribute_missing calls handler method
person.reset_attribute_to_default!(“name”)
person.reset_attribute_to_default!(“name”)
But...
Rails almost never actually does this.
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
class Person
include ActiveModel::AttributeMethods
attribute_method_affix prefix: 'reset_',
suffix: '_to_default!'
attribute_method_suffix '_contrived?'
attribute_method_prefix 'clear_'
define_attribute_methods :name
attr_accessor :name
def attributes
{ 'name' => @name }
end
private
def attribute_contrived?(attr)
true
end
def clear_attribute(attr)
send("#{attr}=", nil)
end
def reset_attribute_to_default!(attr)
send("#{attr}=", 'Default Name')
end
end
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
person = Person.new
person.name
# => 'Gem'
person.name_contrived?
# => true
person.clear_name
person.name
# => nil
person.reset_name_to_default!
person.name
# => 'Default Name'
Define Attribute Methods
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
Defining Attribute Methods
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
Defining Attribute Methods
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
Defining Attribute Methods
For AR
(Ugly!)
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
Defining Attribute Methods
Defines mapping to handler
(like method_missing)
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
Defining Attribute Methods
What’s this?
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
Defining Attribute Methods
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
Here too
def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
Tap, Include & Memoize
●
Common technique:
– Instantiate anonymous module (Module.new)
– include module into class
– memoize included module
●
Now dynamically define methods on
module, and they will be overridable in class
AM::AttributeMethods Recap
●
Defines prefixed/suffixed matchers
●
Overrides method_missing (respond_to?)
to dispatch to attribute methods
●
For performance, for each matcher also
defines methods on included anonymous
module to dispatch to attribute methods
●
(Also can be used to define aliases, similar to
generated methods but on class)
ActiveRecord
ActiveRecord
::
::
AttributeMethods
AttributeMethods
Some Differences
●
Assumes attributes are column values
●
Assumes including class is an AR::Base
●
Changes to make it threadsafe
●
Blacklists certain method names
●
Overrides respond_to?, hardcodes
assumption that attributes are columns
# frozen_string_literal: true
require "mutex_m"
module ActiveRecord
# = Active Record Attribute Methods
module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
initialize_generated_modules
include Read
include Write
include BeforeTypeCast
include Query
include PrimaryKey
include TimeZoneConversion
include Dirty
include Serialization
# frozen_string_literal: true
require "mutex_m"
module ActiveRecord
# = Active Record Attribute Methods
module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
initialize_generated_modules
include Read
include Write
include BeforeTypeCast
include Query
include PrimaryKey
include TimeZoneConversion
include Dirty
include Serialization
Extending AM:AttributeMethods
Include module in module
?
Initialize Generated Modules
def initialize_generated_modules
@generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
def initialize_generated_modules
@generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
Initialize Generated Modules
class GeneratedAttributeMethods < Module
include Mutex_m
end
class GeneratedAttributeMethods < Module
include Mutex_m
end
def initialize_generated_modules
@generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
def initialize_generated_modules
@generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
A Module Builder!
Initialize Generated Modules
class GeneratedAttributeMethods < Module
include Mutex_m
end
class GeneratedAttributeMethods < Module
include Mutex_m
end
def initialize_generated_modules
@generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
def initialize_generated_modules
@generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
Module Builder instance acts like a mutex
def initialize_generated_modules
@generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
def initialize_generated_modules
@generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
super
end
Eagerly Generate Modules
module ActiveRecord
module AttributeMethods
# ...
module ClassMethods
def inherited(child_class)
child_class.initialize_generated_modules
super
end
module ActiveRecord
module AttributeMethods
# ...
module ClassMethods
def inherited(child_class)
child_class.initialize_generated_modules
super
end
module ActiveRecord
module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
initialize_generated_modules
module ActiveRecord
module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
initialize_generated_modules
Why You No Memoize?
to avoid check-then-set race conditions
def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
Race Condition
●
Two (or more) threads could instantiate, include
and return two (or more) different modules
●
AR avoids this by eagerly assigning
@generated_attribute_methods when
including AttributeMethods or when
subclassing AR model class
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
define_attribute_methods
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
define_attribute_methods
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
define_attribute_methods
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
●
Since generated_attribute_methods is an
instance of a class (Module subclass) which
includes Mutex_m, we can call synchronize on it
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
define_attribute_methods
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
attribute_types.keys
else
[]
end
end
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
attribute_types.keys
else
[]
end
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
define_attribute_methods
?
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(attribute_names)
@attribute_methods_generated = true
end
end
define_attribute_methods
?
●
When attribute methods are defined on an AR
model, all superclasses up to AR::Base also
get their attribute methods defined
Initializing an AR Object
def initialize(attributes = nil)
self.class.define_attribute_methods
@attributes = self.class._default_attributes.deep_dup
init_internals
initialize_internals_callback
# ...
def initialize(attributes = nil)
self.class.define_attribute_methods
@attributes = self.class._default_attributes.deep_dup
init_internals
initialize_internals_callback
# ...
Eagerly Defining Methods
module ActiveRecord
module Core
# ...
module ClassMethods
def allocate
define_attribute_methods
super
end
module ActiveRecord
module Core
# ...
module ClassMethods
def allocate
define_attribute_methods
super
end
module ActiveRecord
module Core
# ...
def init_with(coder)
coder = LegacyYamlAdapter.convert(self.class, coder)
@attributes = self.class.yaml_encoder.decode(coder)
init_internals
@new_record = coder["new_record"]
self.class.define_attribute_methods
module ActiveRecord
module Core
# ...
def init_with(coder)
coder = LegacyYamlAdapter.convert(self.class, coder)
@attributes = self.class.yaml_encoder.decode(coder)
init_internals
@new_record = coder["new_record"]
self.class.define_attribute_methods
attribute_method?
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
ActiveModel::AttributeMethods
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
end
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
end
ActiveRecord::AttributeMethods
attribute_method?
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
ActiveModel::AttributeMethods
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
end
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, "")))
end
ActiveRecord::AttributeMethods
Private Method? Yuck.
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
instance_method_
already_implemented?
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
instance_method_
already_implemented?
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
Another Private Method?
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check
to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end
instance_method_
already_implemented?
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
def instance_method_already_implemented?(method_name)
generated_attribute_methods.method_defined?(method_name)
end
?
def respond_to?(name, include_private = false)
return false unless super
case name
when :to_partial_path
name = "to_partial_path".freeze
when :to_model
name = "to_model".freeze
else
name = name.to_s
end
# If the result is true then check for the select case.
# 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) && self.class.column_names.include?(name)
return has_attribute?(name)
end
true
end
def respond_to?(name, include_private = false)
return false unless super
case name
when :to_partial_path
name = "to_partial_path".freeze
when :to_model
name = "to_model".freeze
else
name = name.to_s
end
# If the result is true then check for the select case.
# 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) && self.class.column_names.include?(name)
return has_attribute?(name)
end
true
end
respond_to?
def respond_to?(name, include_private = false)
return false unless super
case name
when :to_partial_path
name = "to_partial_path".freeze
when :to_model
name = "to_model".freeze
else
name = name.to_s
end
# If the result is true then check for the select case.
# 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) && self.class.column_names.include?(name)
return has_attribute?(name)
end
true
end
def respond_to?(name, include_private = false)
return false unless super
case name
when :to_partial_path
name = "to_partial_path".freeze
when :to_model
name = "to_model".freeze
else
name = name.to_s
end
# If the result is true then check for the select case.
# 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) && self.class.column_names.include?(name)
return has_attribute?(name)
end
true
end
respond_to?
def respond_to?(name, include_private = false)
return false unless super
case name
when :to_partial_path
name = "to_partial_path".freeze
when :to_model
name = "to_model".freeze
else
name = name.to_s
end
# If the result is true then check for the select case.
# 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) && self.class.column_names.include?(name)
return has_attribute?(name)
end
true
end
def respond_to?(name, include_private = false)
return false unless super
case name
when :to_partial_path
name = "to_partial_path".freeze
when :to_model
name = "to_model".freeze
else
name = name.to_s
end
# If the result is true then check for the select case.
# 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) && self.class.column_names.include?(name)
return has_attribute?(name)
end
true
end
respond_to?
Coupled
to ActiveRecord
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_methods(*attr_names)
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
method_name = matcher.method_name(attr_name)
unless instance_method_already_implemented?(method_name)
generate_method = "define_method_#{matcher.method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, generated_attribute_methods, method_name,
matcher.method_missing_target, attr_name.to_s
end
end
end
attribute_method_matchers_cache.clear
end
More AM/AR Coupling
ActiveModel::AttributeMethods
def define_method_attribute(name)
safe_name = name.unpack("h*".freeze).first
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def #{temp_method}
#{sync_with_transaction_state}
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
STR
generated_attribute_methods.module_eval do
alias_method name, temp_method
undef_method temp_method
end
end
def define_method_attribute(name)
safe_name = name.unpack("h*".freeze).first
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def #{temp_method}
#{sync_with_transaction_state}
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
_read_attribute(name) { |n| missing_attribute(n, caller) }
end
STR
generated_attribute_methods.module_eval do
alias_method name, temp_method
undef_method temp_method
end
end
ActiveRecord::AttrMethods::Read
"define_method_#{"attribute"}"
def define_method_attribute=(name)
safe_name = name.unpack("h*".freeze).first
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def __temp__#{safe_name}=(value)
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
#{sync_with_transaction_state}
_write_attribute(name, value)
end
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
undef_method :__temp__#{safe_name}=
STR
end
def define_method_attribute=(name)
safe_name = name.unpack("h*".freeze).first
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
def __temp__#{safe_name}=(value)
name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
#{sync_with_transaction_state}
_write_attribute(name, value)
end
alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
undef_method :__temp__#{safe_name}=
STR
end
ActiveRecord::AttrMethods::Write
"define_method_#{"attribute="}"
Problems
●
Hard to extend
– AM/AR AttributeMethods heavily coupled
– Can’t use pure AM prefix/suffixes in AR model
●
Poor encapsulation
– Many private method overrides
●
Confusing and obscure internal API
– define_attribute_method
– define_method_attribute
NASTY!
Ok, you do better
●
Extract AttributeMethodMatcher into a
Module Builder :
– prefix/suffix + generated regex
– methods for defining attribute methods on itself
– dynamically defined method_missing + respond_to?
●
Each prefix/suffix/affix has its own module
●
ActiveRecord module builder can subclass
ActiveModel module builder
module ActiveModel
class AttributeMethodMatcher < Module
NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
attr_reader :prefix, :suffix, :method_missing_target, :method_names
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
@method_names = Set.new
define_method_missing
end
# ...
module ActiveModel
class AttributeMethodMatcher < Module
NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
attr_reader :prefix, :suffix, :method_missing_target, :method_names
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
@method_names = Set.new
define_method_missing
end
# ...
Module Builder Refactor (1)
def define_method_missing
matcher = self
define_method :method_missing do |method_name, *arguments, &method_block|
if (match = matcher.match(method_name.to_s)) &&
method_name != :attributes &&
attribute_method?(match.attr_name) &&
!respond_to_without_attributes?(method_name, true)
attribute_missing(match, *arguments, &method_block)
else
super(method_name, *arguments, &method_block)
end
end
end
def define_method_missing
matcher = self
define_method :method_missing do |method_name, *arguments, &method_block|
if (match = matcher.match(method_name.to_s)) &&
method_name != :attributes &&
attribute_method?(match.attr_name) &&
!respond_to_without_attributes?(method_name, true)
attribute_missing(match, *arguments, &method_block)
else
super(method_name, *arguments, &method_block)
end
end
end
Module Builder Refactor (2)
●
Access builder method using a closure
def define_attribute_method(attr_name)
name = method_name(attr_name)
method_names << name.to_sym
unless instance_method_already_implemented?(name)
generate_method = "define_method_#{method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, name, method_missing_target, attr_name.to_s
end
end
end
def define_attribute_method(attr_name)
name = method_name(attr_name)
method_names << name.to_sym
unless instance_method_already_implemented?(name)
generate_method = "define_method_#{method_missing_target}"
if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
else
define_proxy_call true, name, method_missing_target, attr_name.to_s
end
end
end
Module Builder Refactor (3)
●
Builder defines attribute methods on itself, no
passing around anonymous module
module ActiveRecord
class AttributeMethodMatcher < ActiveModel::AttributeMethodMatcher
include Mutex_m
def initialize(*)
super
@attribute_methods_generated = false
end
#...
def define_attribute_methods(*attr_names)
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
synchronize do
return false if @attribute_methods_generated
super(*attr_names)
@attribute_methods_generated = true
end
end
# ...
module ActiveRecord
class AttributeMethodMatcher < ActiveModel::AttributeMethodMatcher
include Mutex_m
def initialize(*)
super
@attribute_methods_generated = false
end
#...
def define_attribute_methods(*attr_names)
return false if @attribute_methods_generated
# Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
synchronize do
return false if @attribute_methods_generated
super(*attr_names)
@attribute_methods_generated = true
end
end
# ...
AR subclasses AM
The Result
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
Topic.ancestors
=> [Topic(id: integer, title: string, author_name: string...),
Topic::GeneratedAssociationMethods,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
ActiveRecord::Base,
...
AR decoupled from AM
●
AR and AM matchers are entirely isolated
➢
Can co-exist alongside each other in ancestors
➢
Can use AM::AttributeMethods in AR::Base
model
●
Subclass AR::AttributeMethodMatcher to
add new attribute features and use in any
AR::Base model
Module Bloat?
Alternative Implementation
●
Module Builder “lite” version using just one
builder
– All matchers in one builder instance
– Delegate attribute_method_prefix, etc to
module
●
Retains benefits of code encapsulation
●
Avoids “module bloat”, all methods on one
module
Alternative Implementation
●
Module Builder “lite” version using just one
builder
– All matchers in one builder instance
– Delegate attribute_method_prefix, etc to
module
●
Retains benefits of code encapsulation
●
Avoids “module bloat”, all methods on one
module
●
But still struggling with mysterious 4-5% performance drop...