Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

RailsConf 2022 - Reflecting on Active Record As...

RailsConf 2022 - Reflecting on Active Record Associations

Active Record associations seem magical—add a has_many here, a belongs_to there, and suddenly your models are loaded with behavior. Could it be magic, or is it plain old Ruby with some thoughtful design and a bit of metaprogramming? In this talk we'll study Active Record associations by writing our own belongs_to and has_many macros. We'll dynamically define methods, cache query results, replace a a Relation with a CollectionProxy, and automatically prevent N+1 queries with inverses. You'll leave with a deeper understanding of associations, and a new appreciation for their magic.

Daniel Colson

May 26, 2022
Tweet

More Decks by Daniel Colson

Other Decks in Programming

Transcript

  1. • repository • repository=(repository) • build_repository • create_repository • create_repository!

    • reload_repository • repository_changed? • repository_previously_changed?
  2. • repository • repository=(repository) • build_repository • create_repository • create_repository!

    • reload_repository • repository_changed? • repository_previously_changed? • Presence Validations • Caching • Autosaving • Preloading • Destroy Callbacks • Scopes • Extensions • Etc.
  3. Metaprogramming > pull_request => <#PullRequest @repository_id=42> > repository => <#Repository

    @id=77> > pull_request.repository = repository => <#PullRequest @repository_id=77> Writer Method
  4. Metaprogramming class PullRequest < ApplicationRecord belongs_to :repository end klass =>

    Repository primary_key => :id foreign_key => :repository_id ?
  5. Reflections • #klass => Repository • #primary_key => :id •

    #foreign_key Reflection @active_record = PullRequest @name = :repository
  6. Reflection @active_record = PullRequest @name = :repository Reflections • #klass

    => Repository • #primary_key => :id • #foreign_key => :repository_id
  7. Reflection @active_record = PullRequest @name = :repository Reflections • #klass

    => Repository • #primary_key => :id • #foreign_key => :repository_id
  8. Reflections def self.belongs_to(name) reflection = Reflection.new(name, self) klass = reflection.klass

    primary_key = reflection.primary_key foreign_key = reflection.foreign_key
  9. Caching > repo = pull_request.repository => #<Repository:0x307468 @id=42> > same_repo

    = pull_request.repository => #<Repository:0x647570 @id=42>
  10. Caching > repo = pull_request.repository => #<Repository:0x307468 @id=42> > same_repo

    = pull_request.repository => #<Repository:0x647570 @id=42> Different objects
  11. Caching > repo = pull_request.repository => #<Repository:0x307468 @id=42> > same_repo

    = pull_request.repository => #<Repository:0x647570 @id=42> > repo.name = "new_name" > same_repo.name => "old_name" Different objects
  12. Caching > repo = pull_request.repository => #<Repository:0x307468 @id=42> > same_repo

    = pull_request.repository => #<Repository:0x647570 @id=42> > repo.name = "new_name" > same_repo.name => "old_name" Different objects Inconsistent data ,
  13. Caching class Association def reader klass.where(primary_key => @owner[foreign_key]).first end def

    writer(record) @owner[foreign_key] = record[primary_key] end end Association Reader Association Writer
  14. Caching def self.belongs_to(name) define_method(name) do association(name).reader end define_method("#{name}=") do |record|

    association(name).writer(record) end end Use Association Reader and Writer Get Association by name
  15. Caching def reader if loaded? else end end Association @owner

    = #<PullRequest> @reflection = #<Reflection> @loaded = false @target = nil
  16. Caching def reader if loaded? else end end Association @owner

    = #<PullRequest> @reflection = #<Reflection> @loaded = false @target = nil
  17. Association @owner = #<PullRequest> @reflection = #<Reflection> @loaded = false

    @target = nil Caching def reader if loaded? else self.target = klass.where(...).first end end
  18. Association @owner = #<PullRequest> @reflection = #<Reflection> @loaded = false

    @target = nil Caching def reader if loaded? else self.target = klass.where(...).first end end true #<Repository>
  19. Caching def reader if loaded? else self.target = klass.where(...).first end

    end Association @owner = #<PullRequest> @reflection = #<Reflection> @loaded = true @target = #<Repository>
  20. Caching def reader if loaded? target else self.target = klass.where(...).first

    end end Association @owner = #<PullRequest> @reflection = #<Reflection> @loaded = true @target = #<Repository> Association @owner = #<PullRequest> @reflection = #<Reflection> @loaded = true @target = #<Repository>
  21. Caching > repo = pull_request.repository => #<Repository:0x776f6e @id=42> > same_repo

    = pull_request.repository => #<Repository:0x776f6e @id=42>
  22. Caching > repo = pull_request.repository => #<Repository:0x776f6e @id=42> > same_repo

    = pull_request.repository => #<Repository:0x776f6e @id=42> Same object
  23. Caching > repo = pull_request.repository => #<Repository:0x776f6e @id=42> > same_repo

    = pull_request.repository => #<Repository:0x776f6e @id=42> > repo.name = "new_name" > same_repo.name => "new_name" Same object
  24. Caching > repo = pull_request.repository => #<Repository:0x776f6e @id=42> > same_repo

    = pull_request.repository => #<Repository:0x776f6e @id=42> > repo.name = "new_name" > same_repo.name => "new_name" Same object Consistent data -
  25. def reader if loaded? target else self.target = klass.where(primary_key =>

    @owner[foreign_key]).first end end Relations BelongsToAssociation#reader
  26. def reader if loaded? target else self.target = klass.where(primary_key =>

    @owner[foreign_key]).first end end Relations BelongsToAssociation#reader Primary Key Foreign Key
  27. def reader if loaded? target else self.target = klass.where(primary_key =>

    @owner[foreign_key]).first end end Relations BelongsToAssociation#reader Primary Key Foreign Key Single Record
  28. def reader if loaded? target else self.target = klass.where(foreign_key =>

    @owner[primary_key]).to_a end end Relations HasManyAssociation#reader
  29. def reader if loaded? target else self.target = klass.where(foreign_key =>

    @owner[primary_key]).to_a end end Relations Foreign Key Primary Key HasManyAssociation#reader
  30. def reader if loaded? target else self.target = klass.where(foreign_key =>

    @owner[primary_key]).to_a end end Relations Foreign Key Primary Key Array of Records HasManyAssociation#reader
  31. Relations > repository.pull_requests => SELECT * FROM pull_requests WHERE repository_id

    = 42 => [#<PullRequest @repository_id=42>] > repository.pull_requests => [#<PullRequest @repository_id=42>]
  32. Relations > pull_requests = PullRequest.where(repository_id: 42) => <#ActiveRecord::Relation> • Super-powered

    array of records • Lazy loading • #create • And much more What is a Relation?
  33. Relations > pull_requests = repository.pull_requests => <#ActiveRecord::Relation> > pull_requests.to_a Association

    @loaded = false @target = nil true [<#PullRequest>] => [<#PullRequest>] Association Relations?
  34. def load_target if loaded? target else self.target = klass.where(foreign_key =>

    @owner[primary_key]).to_a end end Relations HasManyAssociation#load_target Moved From Reader
  35. Inverses > pull_requests = repository.pull_requests => SELECT * FROM pull_requests

    WHERE repository_id = 42 > pull_requests.map(&:repository)
  36. Inverses > pull_requests = repository.pull_requests => SELECT * FROM pull_requests

    WHERE repository_id = 42 > pull_requests.map(&:repository) => SELECT * FROM repositories WHERE id = 42 LIMIT 1 => SELECT * FROM repositories WHERE id = 42 LIMIT 1 => SELECT * FROM repositories WHERE id = 42 LIMIT 1 => SELECT * FROM repositories WHERE id = 42 LIMIT 1 => SELECT * FROM repositories WHERE id = 42 LIMIT 1 0
  37. BelongsToAssociation @owner = #<PullRequest> @loaded = false @target = nil

    pull_request.repository Inverses HasManyAssociation @owner = #<Repository> @loaded = true @target = [#<PullRequest>, ...] repository.pull_requests
  38. BelongsToAssociation @owner = #<PullRequest> @loaded = false @target = nil

    pull_request.repository Inverses true #<Repository> HasManyAssociation @owner = #<Repository> @loaded = true @target = [#<PullRequest>, ...] repository.pull_requests
  39. Inverses @target.each do |record| association = record.association(:repository) association.target = @owner

    #<PullRequest> HasManyAssociation @owner = #<Repository> @loaded = true @target = [#<PullRequest>, ...]
  40. Inverses @target.each do |record| association = record.association(:repository) association.target = @owner

    #<Repository> #<PullRequest> HasManyAssociation @owner = #<Repository> @loaded = true @target = [#<PullRequest>, ...]
  41. Inverses > pull_requests = repository.pull_requests => SELECT * FROM pull_requests

    WHERE repository_id = 42 > pull_requests.map(&:repository)
  42. Inverses > pull_requests = repository.pull_requests => SELECT * FROM pull_requests

    WHERE repository_id = 42 > pull_requests.map(&:repository) 1
  43. 2

  44. Further Studies • Through Associations, Polymorphic Associations, Scoping, Etc. •

    github.com/rails/rails • activerecord/lib/active_record/associations.rb • activerecord/lib/active_record/reflection.rb • activerecord/lib/active_record/associations/* • danieljamescolson.com/blog
  45. Practical Applications • Use Rails features • Write less custom

    code • Write more efficient code • Avoid inconsistent data • Code with confidence