The Elusive Attribute

The Elusive Attribute

Talk on the internals of attributes in ActiveModel and ActiveRecord at RailsConf 2019.

Video: https://www.youtube.com/watch?v=PNNrmNTQx2s
Summary: https://railsconf.com/program/sessions#session-751

References:
- Give GeneratedAttributeMethods module a name: https://github.com/rails/rails/pull/35595
- Make plain matcher match first, not last: https://github.com/rails/rails/pull/36005 (PR #36005, mentioned in slide 77)
- Velocipedia: https://www.behance.net/gallery/35437979/Velocipedia

05fdba1ae381f24512e977f8fe2697b4?s=128

Chris Salzberg

May 02, 2019
Tweet

Transcript

  1. 2.

    As we know, there are known knowns; there are things

    we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns - the ones we don't know we don't know. - Donald Rumsfeld United States Secretary of Defense (2003)
  2. 3.
  3. 4.
  4. 5.
  5. 6.
  6. 7.
  7. 8.
  8. 9.

    People who are really into bikes [...] start out from

    the frame. They represent that small percentage of people who get the drawing perfectly right. Most other people tend to start from what they are most sure of, so for nearly everyone it’s the wheels. [...] I saw many people drawing all the rest in the hope that positioning handlebar, saddle and pedals would help them figure out the frame. - Gianluca Gimini
  9. 10.
  10. 11.
  11. 12.
  12. 13.
  13. 14.

    @attributes Name: Chris Salzberg Handle: shioyama Born: Montreal, CA Based:

    Tokyo, JP Company: Degica Blog: dejimata.com Tags: Module Builder, Mobility
  14. 16.
  15. 18.

    class Post < ApplicationRecord end Post.ancestors => [Post, Post::GeneratedAssociationMethods, #<AR::AttributeMethods::GeneratedAttributeMethods:...>,

    ApplicationRecord(abstract), ApplicationRecord::GeneratedAssociationMethods, #<AR::AttributeMethods::GeneratedAttributeMethods:...>, AR::Base, ... ] < 6.0
  16. 19.

    class Post < ApplicationRecord end Post.ancestors => [Post, Post::GeneratedAssociationMethods, #<AR::AttributeMethods::GeneratedAttributeMethods:...>,

    ApplicationRecord(abstract), ApplicationRecord::GeneratedAssociationMethods, #<AR::AttributeMethods::GeneratedAttributeMethods:...>, AR::Base, ... ] < 6.0
  17. 20.

    class Post < ApplicationRecord end Post.ancestors => [Post, Post::GeneratedAssociationMethods, Post::GeneratedAttributeMethods,

    ApplicationRecord(abstract), ApplicationRecord::GeneratedAssociationMethods, ApplicationRecord::GeneratedAttributeMethods, ActiveRecord::Base, ... ] >= 6.0
  18. 24.

    => [ ... :title_before_type_cast, :title_came_from_user?, :title?, :title_changed?, :title_change, :title_will_change!, :title_was,

    :title_previously_changed?, ... ] post = Post.new(title: "The Elusive Attribute") post.method(:title).owner.instance_methods Attribute Methods
  19. 25.
  20. 26.
  21. 29.

    post = Post.new(title: "The Elusive Attribute") mod = post.method(:title).owner mod.instance_methods.each

    do |meth| mod.send(:remove_method, meth) end mod.instance_methods #=> []
  22. 30.

    post = Post.new(title: "The Elusive Attribute") mod = post.method(:title).owner mod.instance_methods.each

    do |meth| mod.send(:remove_method, meth) end mod.instance_methods post.method(:title) # raises NameError: undefined method `title'
  23. 31.

    post = Post.new(title: "The Elusive Attribute") mod = post.method(:title).owner mod.instance_methods.each

    do |meth| mod.send(:remove_method, meth) end mod.instance_methods post.method(:title) post.title #=> ...
  24. 32.

    post = Post.new(title: "The Elusive Attribute") mod = post.method(:title).owner mod.instance_methods.each

    do |meth| mod.send(:remove_method, meth) end mod.instance_methods post.method(:title) post.title #=> "The Elusive Attribute"
  25. 33.
  26. 34.
  27. 41.

    cache = AR::Base.connection.schema_cache columns_hash = cache.instance_variable_get(:@columns_hash) columns_hash["posts"] = {} post

    = Post.new post.method(:title=) post.title = "The Elusive Attribute" # raises ActiveModel::UnknownAttributeError
  28. 42.

    cache = AR::Base.connection.schema_cache columns_hash = cache.instance_variable_get(:@columns_hash) columns_hash["posts"] = {} post

    = Post.new post.method(:title=) post.title = "The Elusive Attribute" post.attributes #=> {}
  29. 47.

    cache = AR::Base.connection.schema_cache columns_hash = cache.instance_variable_get(:@columns_hash) columns_hash["posts"] = {} post

    = Post.first post.method(:title) post.title post.attributes #=> {"id"=>1, "title"=>"The Elusive Attribute", # "created_at"=>2019-03-13 05:09:10 UTC, # "updated_at"=>2019-03-29 07:57:54 UTC}
  30. 49.

    result = AR::Base.connection.exec_query( 'SELECT posts.* FROM posts') #=> #<ActiveRecord::Result:0x...> result.columns

    #=> ["id", "title", "created_at", "updated_at"] result.rows #=> [[1, "The Elusive Attribute", "2019-03-15...
  31. 50.

    result = AR::Base.connection.exec_query( 'SELECT posts.* FROM posts') #=> #<ActiveRecord::Result:0x...> result.columns

    #=> ["id", "title", "created_at", "updated_at"] result.rows #=> [[1, "The Elusive Attribute", "2019-03-15... Post.column_names #=> []
  32. 51.
  33. 52.
  34. 53.
  35. 54.
  36. 57.
  37. 61.

    Constraints 1. Methods are fast, method missing is slow 2.

    schema is static, attributes are dynamic
  38. 62.

    Constraints 1. Methods are fast, method missing is slow 2.

    schema is static, attributes are dynamic “front door” “back door”
  39. 69.

    “Front Door” attribute_method_matchers.each do |matcher| method_name = matcher.method_name(attr_name) define_proxy_call true,

    generated_attribute_methods, method_name, matcher.target, attr_name.to_s end “title”
  40. 70.

    “Front Door” attribute_method_matchers.each do |matcher| method_name = matcher.method_name(attr_name) define_proxy_call true,

    generated_attribute_methods, method_name, matcher.target, attr_name.to_s end “title_was” “attribute_was” “title”
  41. 79.
  42. 80.
  43. 81.
  44. 82.
  45. 83.
  46. 84.
  47. 85.
  48. 86.
  49. 87.

    module ActiveModel::Dirty extend ActiveSupport::Concern include ActiveModel::AttributeMethods included do attribute_method_suffix "_changed?",

    "_change", "_will_change!", "_was" # Dispatch target for *_was attribute methods. def attribute_was(attr) attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) end
  50. 88.

    module ActiveModel::Dirty extend ActiveSupport::Concern include ActiveModel::AttributeMethods included do attribute_method_suffix "_changed?",

    "_change", "_will_change!", "_was" # Dispatch target for *_was attribute methods. def attribute_was(attr) attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) end
  51. 91.

    module ActiveModel::AttributeMethods included do class_attribute :attribute_method_matchers, instance_writer: false, default: [

    ClassMethods::AttributeMethodMatcher.new ] # ... end end def initialize(options = {}) @prefix = options.fetch(:prefix, "") @suffix = options.fetch(:suffix, "")
  52. 92.
  53. 93.
  54. 94.

    module ActiveRecord module AttributeMethods module Read # ... def _read_attribute(attr_name,

    &b) @attributes.fetch_value(attr_name.to_s, &b) end alias :attribute :_read_attribute private :attribute end end end
  55. 95.
  56. 96.
  57. 98.
  58. 101.
  59. 102.

    post = Post.select('title as foo').first post.foo #=> "The Elusive Attribute"

    post.foo = "This cannot be saved" post.foo_was post.foo_change
  60. 104.

    post = Post.select('1 as one').first post.one #=> 1 post.one =

    2 # post.one #=> 2 post.one_was #=> 1 post.one_change #=> [1, 2]
  61. 106.

    payments = Payment.select( 'amount, amount*0.08 as tax, amount*0.02 as tax_change')

    payment = payments.first payment.amount #=> 100 payment.tax #=> 8 payment.tax_change #=> ?
  62. 107.

    payments = Payment.select( 'amount, amount*0.08 as tax, amount*0.02 as tax_change')

    payment = payments.first payment.amount #=> 100 payment.tax #=> 8 payment.tax_change #=> nil
  63. 109.