A Brief Introduction to AttributeMethods

A Brief Introduction to AttributeMethods

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

05fdba1ae381f24512e977f8fe2697b4?s=128

Chris Salzberg

November 10, 2017
Tweet

Transcript

  1. KichijojiKaigi #12 A Brief Introduction to A Brief Introduction to

    AttributeMethods AttributeMethods by Chris Salzberg
  2. きっかけ

  3. None
  4. Model Ancestry Topic.ancestors => [Topic(...), Topic::GeneratedAssociationMethods, #<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x0055c121def890>, 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::AttributeMethods::GeneratedAttributeMethods:0x0055c121def890>, 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
  5. Model Ancestry Topic.ancestors => [Topic(...), Topic::GeneratedAssociationMethods, #<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x0055c121def890>, 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::AttributeMethods::GeneratedAttributeMethods:0x0055c121def890>, 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?
  6. Generated Attribute Methods generated_methods = Topic.ancestors[2] => #<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x...> generated_methods.instance_methods(false) =>

    [] Topic.new => #<Topic:0x005555e9e18478 ... > 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] => #<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x...> generated_methods.instance_methods(false) => [] Topic.new => #<Topic:0x005555e9e18478 ... > 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, ... ]
  7. Generated Attribute Methods generated_methods = Topic.ancestors[2] => #<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x...> generated_methods.instance_methods(false) =>

    [] Topic.new => #<Topic:0x005555e9e18478 ... > 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] => #<ActiveRecord::AttributeMethods::GeneratedAttributeMethods:0x...> generated_methods.instance_methods(false) => [] Topic.new => #<Topic:0x005555e9e18478 ... > 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, ... ] ?
  8. My PR Topic.ancestors => [Topic(id: integer, title: string, author_name: string...),

    Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ... Topic.ancestors => [Topic(id: integer, title: string, author_name: string...), Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ...
  9. My PR Topic.ancestors => [Topic(id: integer, title: string, author_name: string...),

    Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ... Topic.ancestors => [Topic(id: integer, title: string, author_name: string...), Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ... ActiveRecord::AttributeMethods::Dirty
  10. My PR Topic.ancestors => [Topic(id: integer, title: string, author_name: string...),

    Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ... Topic.ancestors => [Topic(id: integer, title: string, author_name: string...), Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ... ActiveRecord::AttributeMethods::Dirty ActiveModel::Dirty
  11. My PR Topic.ancestors => [Topic(id: integer, title: string, author_name: string...),

    Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ... Topic.ancestors => [Topic(id: integer, title: string, author_name: string...), Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ... ActiveRecord::AttributeMethods::Dirty ActiveModel::Dirty ActiveRecord::AttributeMethods::Query ActiveRecord::AttributeMethods::BeforeTypeCast
  12. 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
  13. ActiveModel ActiveModel :: :: AttributeMethods AttributeMethods

  14. None
  15. 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
  16. 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
  17. 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.
  18. 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'
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. “Method Missing Target” "#{@prefix}attribute#{@suffix}"

  30. “Method Missing”? # Allows access to the object attributes, which

    are held in the hash # returned by <tt>attributes</tt>, as though they were first-class # methods. So a +Person+ class with a +name+ attribute can for example use # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use # the attributes hash -- except for multiple assignments with # <tt>ActiveRecord::Base#attributes=</tt>. # # It's also possible to instantiate related objects, so a <tt>Client</tt> # class belonging to the +clients+ table with a +master_id+ foreign key # can instantiate master through <tt>Client#master</tt>. 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 <tt>attributes</tt>, as though they were first-class # methods. So a +Person+ class with a +name+ attribute can for example use # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use # the attributes hash -- except for multiple assignments with # <tt>ActiveRecord::Base#attributes=</tt>. # # It's also possible to instantiate related objects, so a <tt>Client</tt> # class belonging to the +clients+ table with a +master_id+ foreign key # can instantiate master through <tt>Client#master</tt>. 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
  31. “Method Missing”? # # # # # # # #

    It's also possible to instantiate related objects, so a <tt>Client</tt> # class belonging to the +clients+ table with a +master_id+ foreign key # can instantiate master through <tt>Client#master</tt>. 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 <tt>Client</tt> # class belonging to the +clients+ table with a +master_id+ foreign key # can instantiate master through <tt>Client#master</tt>. 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 <tt>attributes</tt>, as though they were first-class # methods. So a +Person+ class with a +name+ attribute can for example use # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use # the attributes hash -- except for multiple assignments with # <tt>ActiveRecord::Base#attributes=</tt>.
  32. 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
  33. 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
  34. 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
  35. 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
  36. “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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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”)
  44. Method Missing Flow person.reset_name_to_default! person.reset_name_to_default!

  45. 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 #=> [#<struct AttributeMethodMatch # target=”reset_attribute_to_default!” attr_name=”name” ...>] matchers.map { |method| method.match(method_name) }.compact #=> [#<struct AttributeMethodMatch # target=”reset_attribute_to_default!” attr_name=”name” ...>]
  46. 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 #=> [#<struct AttributeMethodMatch # target=”reset_attribute_to_default!” attr_name=”name” ...>] matchers.map { |method| method.match(method_name) }.compact #=> [#<struct AttributeMethodMatch # target=”reset_attribute_to_default!” attr_name=”name” ...>] Which are included in attributes? { ‘name’ => @name }.include?(“name”) { ‘name’ => @name }.include?(“name”)
  47. 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 #=> [#<struct AttributeMethodMatch # target=”reset_attribute_to_default!” attr_name=”name” ...>] matchers.map { |method| method.match(method_name) }.compact #=> [#<struct AttributeMethodMatch # 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”)
  48. But...

  49. Rails almost never actually does this.

  50. 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
  51. 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
  52. 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
  53. 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!)
  54. 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)
  55. 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?
  56. 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
  57. 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
  58. 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)
  59. ActiveRecord ActiveRecord :: :: AttributeMethods AttributeMethods

  60. None
  61. 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
  62. # 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 ?
  63. 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
  64. 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!
  65. 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
  66. 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
  67. Why You No Memoize? to avoid check-then-set race conditions

  68. 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
  69. # 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
  70. # 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.
  71. # 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
  72. # 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
  73. # 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 ?
  74. # 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
  75. Initializing an AR Object

  76. 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
  77. 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
  78. 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.
  79. 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
  80. 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?
  81. 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 ?
  82. 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?
  83. 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?
  84. 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
  85. 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
  86. 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"}"
  87. 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="}"
  88. 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
  89. NASTY!

  90. 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
  91. 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)
  92. 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
  93. 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
  94. 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
  95. The Result Topic.ancestors => [Topic(id: integer, title: string, author_name: string...),

    Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ... Topic.ancestors => [Topic(id: integer, title: string, author_name: string...), Topic::GeneratedAssociationMethods, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_in_database)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change_to_be_saved)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:will_save_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_last_save)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:saved_change_to_)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:restore_)(.*)(?:!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previous_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_previously_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_was)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_will_change!)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_change)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_changed\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_came_from_user\?)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:_before_type_cast)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:=)$/>, <ActiveRecord::AttributeMethodMatcher: /^(?:)(.*)(?:)$/>, ActiveRecord::Base, ...
  96. 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
  97. Module Bloat?

  98. 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
  99. 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...
  100. None