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

ActiveRecord の歩み方

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for osyo osyo
October 24, 2020
620

ActiveRecord の歩み方

Avatar for osyo

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!! ご清聴 ご清聴

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