Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ActiveRecord の歩み方

osyo
October 24, 2020
430

ActiveRecord の歩み方

osyo

October 24, 2020
Tweet

Transcript

  1. Relation::VALUE_METHODS.each do |name| method_name = \ case name when *Relation::MULTI_VALUE_METHODS

    then "#{name}_values" when *Relation::SINGLE_VALUE_METHODS then "#{name}_value" when *Relation::CLAUSE_METHODS then "#{name}_clause" end class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{method_name} # def includes_values default = DEFAULT_VALUES[:#{name}] # default = DEFAULT_VALUES[:includes] @values.fetch(:#{name}, default) # @values.fetch(:includes, default) end # end def #{method_name}=(value) # def includes_values=(value) assert_mutability! # assert_mutability! @values[:#{name}] = value # @values[:includes] = value end # end CODE end
  2. QUERYING_METHODS = [ :find, :find_by, :find_by!, :take, :take!, :first, :first!,

    :last, :last!, :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, :exists?, :any?, :many?, :none?, :one?, :first_or_create, :first_or_create!, :first_or_initialize, :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :create_or_find_by, :create_or_find_by!, :destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by, :find_each, :find_in_batches, :in_batches, :select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins, :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or, :having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only, :count, :average, :minimum, :maximum, :sum, :calculate, :annotate, :pluck, :pick, :ids ].freeze # :nodoc: delegate(*QUERYING_METHODS, to: :all)
  3. module ActiveRecord class Base extend ActiveModel::Naming extend ActiveSupport::Benchmarkable extend ActiveSupport::DescendantsTracker

    extend ConnectionHandling extend QueryCache::ClassMethods extend Querying extend Translation extend DynamicMatchers extend Explain extend Enum extend Delegation::DelegateCache extend Aggregations::ClassMethods include Serialization include Store include SecureToken include Suppressor end
  4. ⾃⼰紹介 ⾃⼰紹介 名前 : osyo Twitter : github : ブログ

    : 趣味で Ruby にパッチを投げたり、気になった Ruby のチケットをブ ログにまとめたりしてる Ruby で⼀番好きな機能は Refinements 普段は を開発してる に参加したり エディタは Vim 質問や気になる点があればぜひ Twitter で! @pink_bangbi osyo-manga Secret Garden(Instrumental) activerecord-bitemporal RubyKaigi Takeout 2020 感想戦 @ 仮想松本
  5. アジェンダ アジェンダ ActiveRecord を読む前に Ruby の知識を蓄える 知っておくとよい Ruby の知識 コードを読むときに使う道具の紹介

    ActiveRecord を読むときのコツ ActiveRecord の構造を知る 読む⽬的を明確にする ライブリーディング コードを読む前の下準備 ActiveRecord を読んでみる まとめ 話さないこと ActiveRecord ⾃体の機能解説はしない 今⽇の話を聞いて実際に⾃分で読んでみよう!!
  6. Ruby の知識を蓄える Ruby の知識を蓄える ActiveRecord は Ruby で書かれている なので Ruby

    が読めれば ActiveRecord も読むことはできる しかし ActiveRecord では普段はあまり使わないような Ruby の機能が 使われている メタプロだったり動的なオブジェクト⽣成だったり などでちゃんと読むためにはある程度 Ruby に精通した知識が必要に なってくる 知らない Ruby の書き⽅が出てきてもギョッとせずに冷静に対応する 事が⼤事 知らない機能やメソッドが出てきたらちゃんと調べる!
  7. 知っておくとよい Ruby の知識 知っておくとよい Ruby の知識 メタプログラミングの知識 #define_method や #respond_to?,

    #method_missing, #send 動的なクラス/ モジュール⽣成 Class.new や Module.new が出てきてもびっくりしない コンテキストを切り替えたブロックの評価 instance_exec や instance_eval, class_eval 継承関連 いろんなところで include/prepend や extend してる User.ancestors とか⾒るとめっちゃ継承してる… ActiveSupport::Concern や delegate の機能 Ruby ではなくて Rails の拡張機能 ActiveRecord で広く使われているので知っておくとよい
  8. コードを読むときに使う道具の紹介 コードを読むときに使う道具の紹介 Object#method メソッドの定義位置などを調べる メタ情報などを取得するメソッド __method__, caller, .class ⾃作デバッグ出⼒ gem

    : 実⾏時デバッグ機能 binding.irb や binding.pry や $debug = true と pp hoge if $debug の組み合わせ 必要なときのみデバッグ出⼒を ON にする ActiveRecord の #to_sql 実⾏される SQL ⽂を取得する エディタを便利にする エディタ上でファイラをつかったり grep したり binding-debug ruby_jard
  9. メソッド定義位置を調べる メソッド定義位置を調べる Object#method を使⽤してメソッドの定義位置を調べられる # test.rb class Super def hoge;

    end end class Sub < Super def hoge; end end obj = Sub.new # hoge メソッドのオブジェクトを取得する meth = obj.method(:hoge) # メソッドの定義位置が出⼒される pp meth.source_location # => ["/home/hoge/test.rb", 7] # こうすると親メソッドも参照する事ができる pp meth.super_method.source_location # => ["/home/hoge/test.rb", 3]
  10. メタ情報をデバッグ出⼒ メタ情報をデバッグ出⼒ よく利⽤するデバッグ出⼒情報 def plus(a, b) # メソッド名を出⼒ pp __method__

    # => :plus # クラス名を出⼒ pp a.class.name # => "Integer" # コールスタックを出⼒ pp caller a + b end plus 1, 2
  11. ⾃作デバッグ出⼒ gem ⾃作デバッグ出⼒ gem pp { expr } を実⾏すると "expr"

    とその結果の両⽅が出⼒される gem install binding-debug require "binding/debug" using BindingDebug def plus(a, b) # メソッド名を出⼒ pp { __method__ } # output: __method__ # => :plus # クラス名を出⼒ pp { a.class.name } # output: a.class.name # => "Integer" a + b end plus 1, 2
  12. $debug = true と pp hoge if $debug の組み合わせ $debug

    = true と pp hoge if $debug の組み合わせ pp でデバッグ出⼒を仕込むと調べたい箇所以外のタイミングでも出 ⼒される事がある pp だけではなくて pp hoge if $debug で制御することで任意のタイミ ングでのみデバッグ出⼒できる def twice(a) pp __method__ if $debug pp a if $debug a + a end twice(42) $debug = true # ここで呼び出された場合のみデバッグ出⼒する twice("homu") $debug = false twice(42)
  13. #to_sql で SQL ⽂を取得 #to_sql で SQL ⽂を取得 #to_sql で実⾏される

    SQL ⽂を取得する事ができる # puts で出⼒する puts User.where(active: true).to_sql # output: SELECT "users".* FROM "users" WHERE "users"."active" = 1 # p や pp だと " がエスケープされるので注意 p User.where(active: true).to_sql # => "SELECT \"users\".* FROM \"users\" WHERE \"users\".\"active\" = 1"
  14. ActiveRecord の構造を知る ActiveRecord の構造を知る ActiveRecord にはいろいろな機能が実装されている モデル定義 関連付けの設定 バリデーション SQL

    の実⾏‧読み込み‧モデルのインスタンス化 etc... 調べたい機能に対してどこを読むべきなのかを調べるのが難しい 単純に ActiveRecord ⾃体のコード量が多い ある程度 ActiveRecord の構造を知っておくとあたりをつけて読みや すい 慣れの部分もあるがある程度全体像を把握しておくとよい
  15. 例えば… ActiveRecord::Persistence モデルの⽣成、更新、削除 .create, #update, #destroy ActiveRecord::Relation や ActiveRecord::QueryMethods クエリメソッドの定義や

    Arel の構築処理 .where, .order, .limit ActiveRecord::Reflection 関連付けの設定定義 has_many, has_one, belongs_to ActiveRecord::Associations #associations や関連先の読み込みの処理 ActiveRecord::Migration migration 関連の処理 などなど… ActiveRecord 以外で定義されている処理もあるので注意する
  16. 読む⽬的を明確にする 読む⽬的を明確にする ActiveRecord というのはいろいろなことをやっている ActiveRecord を読む!と⾔っても漠然としてて迷⾛しがち まずは『ActiveRecord のこういう機能を読む!』という⽬的を⾒つ けると⼊り⼝が明確になって読みやすい User.where(name:

    "Tom").to_sql で SQL がどう構築されるのか has_many :comments を⾏うと何が起きるのか バリデーションの仕組みって… ?コールバックの仕組みって… ? 特に『このメソッドを呼ぶと何が起きるのか』みたいな⽬的だとコ ードリーディングに⼊りやすい has_many がなにをやっているのか調べる、ってなると has_many のメソッドを調べるところから始められる
  17. コードを読む前の下準備 コードを読む前の下準備 何を読むのか⽬的を明確にする 今回は scope の実装を読んでみる 調べたい機能の挙動を調べておく その挙動がどうやって Ruby で表現されているのか⾒るのが⼤事

    scope のドキュメントは ドキュメントを参照する場合は がおすすめ 最⼩構成のコードを⽤意する 最⼩構成のほうが余計な処理がなく実⾏も早いし、意図しない動 作が発⽣することも少ない Rails が を⽤意しているのでそれを 使うと1 ファイルだけで簡単に ActiveRecord のコードを動かすこ とができる ActiveRecord 以外のテンプレートもあるのでよしなに利⽤できる こちらここ バグレポート⽤のテンプレート
  18. 01. その機能を使ってみる 01. その機能を使ってみる まず実際にどういう挙動をするのか使ってみる class User < ActiveRecord::Base #

    こんな感じで where を scope として定義する scope :actives, -> { where(active: true) } scope :owners, -> { where(owner: true) } end # 定義した scope はクラスメソッドのような形で呼び出すことができる puts User.actives.to_sql # => SELECT "users".* FROM "users" WHERE "users"."active" = 1 # また User. だけではなくて scope をチェーンすることもできる puts User.actives.owners.to_sql # => SELECT "users".* FROM "users" WHERE "users"."active" = 1 AND "users"."owner" = 1 puts User.order(:created_at).owners.to_sql # => SELECT "users".* FROM "users" WHERE "users"."owner" = 1 ORDER BY "created_at" ASC
  19. 02. その機能(メソッド)の定義位置を調べる 02. その機能(メソッド)の定義位置を調べる 実際にどこでメソッドが定義されているのか Object#method で確認 する Ruby の場合は動的にメソッドが定義されていたりすることがあるの

    で Object#method を使うのが⼀番確実 class User < ActiveRecord::Base # 実際にメソッドがどこで定義されるのか調べる # これは Ruby の標準の機能かつ実⾏時に値を取得するので⼀番確実 pp method(:scope).source_location # => ["/home/worker/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activerecord- 6.0.3/lib/active_record/scoping/named.rb", # 170] # ちなみに Ruby 2.7 以降だと p や pp でそのまま出⼒しても表⽰される pp method(:scope) # => #<Method: #<Class:ActiveRecord::Base> (ActiveRecord::Scoping::Named::ClassMethods)#scope(name, body, &block) /home/worker/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activerecord- 6.0.3/lib/active_record/scoping/named.rb:170> end
  20. 03. 実装を読む 03. 実装を読む Object#method で取得した情報を元にファイルを開いて実際に定義 されているメソッドの中⾝を読んでいく このあたり def scope(name,

    body, &block) unless body.respond_to?(:call) raise ArgumentError, "The scope body needs to be callable." end if dangerous_class_method?(name) raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ "on the model \"#{self.name}\", but Active Record already defined " \ "a class method with the same name." end if method_defined_within?(name, Relation) raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \ "an instance method with the same name." end
  21. valid_scope_name?(name) extension = Module.new(&block) if block if body.respond_to?(:to_proc) singleton_class.define_method(name) do

    |*args| scope = all._exec_scope(name, *args, &body) scope = scope.extending(extension) if extension scope end else singleton_class.define_method(name) do |*args| scope = body call(*args) || all
  22. 実装を読んでいくと以下のような事がわかってくる # body で call メソッドが呼び出せないとエラー unless body.respond_to?(:call) raise ArgumentError,

    "The scope body needs to be callable." end # dangerous_class_method? とは… ? if dangerous_class_method?(name) raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ "on the model \"#{self.name}\", but Active Record already defined " \ "a class method with the same name." end # method_defined_within? とは… ? if method_defined_within?(name, Relation) raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \ "an instance method with the same name." end # 省略
  23. 04. 気になった部分を掘り下げていく 04. 気になった部分を掘り下げていく 先程の実装でよく知らないメソッドが出てきました dangerous_class_method? と method_defined_within? このメソッドが何をやっているのか気になるので次はこの実装を… という感じで気になったメソッドをどんどん掘り下げていきコード

    を読んでく また #method で掘り下げていくこともあればそのファイルで検索 したり gem 全体で grep することもある もちろん関係なさそうな処理をすっ⾶ばして次のコードを読んでい くのも OK コードリーディングは⾃由
  24. まとめ まとめ まずは Ruby の気持ちを理解するところからはじめる!! 実際 Ruby がわかっていれば(処理は複雑だが)読み解くのはそ こまで難しくはない 複雑なコードを⾒てもパニックにならず冷静に読んでみるのが⼤

    事 コードを読むときは⽬的を持って読んでいく! まずは⼩さな⽬的を⾒つけて読んでいくと⼊り⼝が明確になって 読みやすい Ruby には便利なデバッグ機能がたくさんあるので使い分けてコード を読んでいこう 記述したデバッグコードはちゃんと消そう… 他の⼈がコードを読んだり書いたりするのを⾒るのは楽しいのでみ んなどんどんやっていこう!!
  25. Let's enjoy Ruby reading!! Let's enjoy Ruby reading!! ご清聴 ご清聴

    ありがとうございました ありがとうございました