Railsの中身をチョロっとのぞいちゃえ!!!の段(find_by_XXX)
まえがき
こんにちは、machidaです。
この記事は KMC Advent Calendar 2019 - Adventar の18日目の記事です。
2があります
お絵かきもあります
前日の記事は id:PrimeNumber さんのオンラインBrainf*ckインタプリタ・デバッガを作った【Brainf*ck Advent Calendar 2019とその他のAdCの17日目】 - prime's diaryでした。
今年の夏ごろからRuby、Ruby on Railsのお勉強をしています。
Ruby on Rails(以下Rails)はRubyで作られたWebアプリケーションフレームワークです。
Railsは、少ないコード量で多くの機能を実現することができますが、その分前提となっている知識や概念はどちらかというと多めです。そのため、Railsの持っている文脈を理解するまでは、「そういうもの」と割り切って学習を進めなければならない場面によく遭遇します。
この記事の趣旨は、Railsの中身を少しだけのぞいてみることで、「そういうもの」としての理解からすこしだけでも踏み出そうというものです(俺が)。
なにをするの
Railsは非常に多くのライブラリやメソッドから構成されていますが、今回は、その中でも Active Record というライブラリに含まれる find_by_XXX
メソッドについて調べてみます。
Active Recordは、Rubyのオブジェクトとデータベース上のデータを結びつけ、Rubyとデータベースの仲立ちをしてくれているライブラリです。
find_by_XXX
は、Active Recordに含まれるメソッドです。
モデルをレシーバ、XXXを列名、引数をフィールドの値として呼び出すと、該当するレコードを取り出して、Active Recordオブジェクトとして返してく れます。
例えば、Memberテーブル(モデル)でkmc_idフィールドの値が"machida"のレコードを取り出したい場合は、以下のようにします。
Member.find_by_kmc_id "machida"
実際にコードを見て理解するまでは、自分で定義したわけでもないのに、カラムの名前を使ったメソッドが実行できてしまうこのメソッドは魔法のように感じられました。
Railsの中身をのぞいて、未定義のはずのfind_by_XXX
メソッドが呼び出され、実行されるまでの流れを見てみましょう(実際にレコードを取得する部分については割愛します、ごめんなさい)。
準備
Railsの本体をローカルに持ってくる(じっくり読んでみたい場合)
GitHubにあるRailsのレポジトリをForkし、cloneしてローカルに落とします。
Railsプロジェクトの準備
new
コマンドでRailsプロジェクトを作成します。
rails new PeekingInside
処理の流れを追うために必要なgemをGemfileに追加します。
#Gemfile ... gem gem 'pry-rails' gem 'pry-byebug'
必要なgemを取得します。
bundle install
モデルの作成
id、first_name、last_name、created_at、updated_atのカラムを持つシンプルなUserモデルを作成します。
rails generate model User first_name:string last_name:string
いくつかレコードを追加します。
bin/rails console User.create(first_name: "Mito", last_name: "Tsukino") User.create(first_name: "Kaede", last_name: "Higuchi") User.create(first_name: "Rin", last_name: "Shizuka")
以下のようなUserモデルが作成できました。
id | first_name | last_name | created_at | updated_at |
---|---|---|---|---|
1 | Mito | Tsukino | "2019-12-17 13:24:09" | "2019-12-17 13:24:09" |
2 | Kaede | Higuchi | "2019-12-17 13:24:16" | "2019-12-17 13:24:16" |
3 | Rin | Shizuka | "2019-12-17 13:24:25" | "2019-12-17 13:24:25" |
実際に処理をのぞいてみる
pryによるステップ実行
Railsのコンソールコマンドでpryを起動します。
bin/rails console
find_by_first_name
メソッドを呼び出すtest
メソッドを作ります。
[1] pry(main)> def test [1] pry(main)* User.find_by_first_name "Mito" [1] pry(main)* end => :test
まずは通常通り実行してみます。
[2] pry(main)> test User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."first_name" = ? LIMIT ? [["first_name", "Mito"], ["LIMIT", 1]] => #<User:0x000055cfd9daf2d8 id: 1, first_name: "Mito", last_name: "Tsukino", created_at: Tue, 17 Dec 2019 13:24:09 UTC +00:00, updated_at: Tue, 17 Dec 2019 13:24:09 UTC +00:00>
SELECT文が発行され、first_nameが "Mito" になっているレコードが読み込まれました。
次は、先ほどのtest
メソッドに1行加えたtest2
メソッドを作ります。
[3] pry(main)> def test2 [3] pry(main)* binding.pry [3] pry(main)* User.find_by_first_name "Mito" [3] pry(main)* end => :test2
コード中でbinding.pry
を呼び出すと、ブレークポイントを設け、その場所からステップ実行することができます。
ステップ実行中には以下のコマンドが使えます。
コマンド | 操作 |
---|---|
step | メソッドの内部に入る |
next | 現在のメソッド内で1行進める |
finish | 現在のメソッドを抜ける |
exit! | pryを抜ける |
test2
メソッドを実行すると、以下のようにbinding.pry
を呼び出した場所で処理が止まります。
[4] pry(main)> test2 From: (pry) @ line 7 Object#test2: 5: def test2 6: binding.pry => 7: User.find_by_first_name "Mito" 8: end [1] pry(main)>
step
コマンドでメソッドの内部に入り、処理をみてみます。
[1] pry(main)> step From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activesupport-5.2.4/lib/active_support/dependencies.rb @ line 194 ActiveSupport::Dependencies::ModuleConstMissing#const_missing: 193: def const_missing(const_name) => 194: from_mod = anonymous? ? guess_for_anonymous(const_name) : self 195: Dependencies.load_missing_constant(from_mod, const_name) 196: end [1] pry(Object)>
内部に入ることができました。
ここは、RailsのAutoloadという機能を実現している部分です。
Autoloadは、Railsには命名規則に則ったクラス(のファイル)を自動的に読み込む機能です。コントローラやモデルをrequire
する必要がないのは、この仕組みのためです。
今回調べたいのはメソッド呼び出しの仕組みなので、この部分は飛ばします。
finish
コマンドで現在のメソッドを抜け、次の処理に進みます。
method_missing
[1] pry(Object)> finish From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 16 ActiveRecord::DynamicMatchers#method_missing: 15: def method_missing(name, *arguments, &block) => 16: match = Method.match(self, name) 17: 18: if match && match.valid? 19: match.define 20: send(name, *arguments, &block) 21: else 22: super 23: end 24: end
method_missing
という奇妙な名前のメソッドにたどり着きました。
ここからが本題のメソッド呼び出しに関わる部分です。
Rubyでは、オブジェクトのメソッドが呼び出されると、まずそのオブジェクトのクラスのインスタンスメソッド中を探索します。そこでメソッドが見つからないと、次はそのクラスが継承している親クラス中を探索します。
これを繰り返し、最終的に最上位のBasicObjectクラスにたどり着いてもメソッドが見つからないとき、Rubyは、メソッドが存在しないことを伝えるために、元のオブジェクトのmethod_missing
メソッドを呼び出します。
method_missing
メソッドが呼び出されると、Rubyは通常のメソッド呼び出しの場合と同様に、呼び出されたmethod_missing
メソッドの在りかを継承チェーンを登り探索しはじめます。
通常、最上位のBasicObjectに定義されているmethod_missing
メソッドが呼び出され、メソッドが存在しないことを告げるエラーが出力されます。
下記は、存在しないメソッドを呼び出す例です。文字列変数strに対し、存在しないメソッドhogehoge
を呼び出しています。
[1] pry(main)> str = "hello" => "hello" [2] pry(main)> str.hogehoge NoMethodError: undefined method `hogehoge' for "hello":String from (pry):2:in `<main>'
実は、Rubyでは、通常のメソッドと同様に、method_missing
メソッドもサブクラスで再定義(オーバーライド)することができます。
method_missing
を再定義することによって、未定義のメソッドが呼び出された際に所定の処理を行うことが可能です。
[1] pry(Object)> finish From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 16 ActiveRecord::DynamicMatchers#method_missing: 15: def method_missing(name, *arguments, &block) => 16: match = Method.match(self, name) 17: 18: if match && match.valid? 19: match.define 20: send(name, *arguments, &block) 21: else 22: super 23: end 24: end
Active Recordは、まさにこの部分でmethod_missing
を再定義しています。
method_missing
を再定義することで、単にエラーを出力して終了する前に、何らかの意義のあるメソッド呼び出しでないかを判定し、処理を行うことを可能にしているのです。
name, *arguments, &block
引き続きpryを使ってmethod_missing
メソッドに与えられた引数name, arguments, blockの中身をのぞいてみましょう。
Rubyは見つからなかったメソッドの名前(シンボル)、与えられた引数、与えられたブロックを引数としてmethod_missing
を呼び出します。
From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 16 ActiveRecord::DynamicMatchers#method_missing: 15: def method_missing(name, *arguments, &block) => 16: match = Method.match(self, name) 17: 18: if match && match.valid? 19: match.define 20: send(name, *arguments, &block) 21: else 22: super 23: end 24: end [1] pry(User)> name => :find_by_first_name [2] pry(User)> arguments => ["Mito"] [3] pry(User)> block => nil
実際に、name
には呼び出した :find_by_first_name、arguments
には与えた引数 "Mito" が格納されていることが確認できます。今回はブロックは与えていないので、block
にはなにも入っていません。
Methodクラスのmatchメソッド
次は、match = Method.match(self, name)
について調べましょう。
From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 16 ActiveRecord::DynamicMatchers#method_missing: 15: def method_missing(name, *arguments, &block) => 16: match = Method.match(self, name) 17: 18: if match && match.valid? 19: match.define 20: send(name, *arguments, &block) 21: else 22: super 23: end 24: end [1] pry(User)> self => User (call 'User.connection' to establish a connection) [2] pry(User)> name => :find_by_first_name
ActiveRecord::DynamicMatchers::Methodのクラスメソッドmatch
が呼び出されています。
引数のself
にはUserモデル(クラス)自身、name
には呼び出したメソッド名 :find_by_first_nameが格納されています。
step
コマンドでmatch
メソッドの内部に入ります。
From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 33 ActiveRecord::DynamicMatchers::Method.match: 32: def match(model, name) => 33: klass = matchers.find { |k| k.pattern.match?(name) } 34: klass.new(model, name) if klass 35: end
find
は、ブロック引数に要素を入れてブロックを繰り返し、ブロックの返り値が真だったときにその要素を返すメソッドです。
matchersという変数には何が入っているのでしょうか。のぞいてみます。
[3] pry(ActiveRecord::DynamicMatchers::Method)> matchers => [ActiveRecord::DynamicMatchers::FindBy, ActiveRecord::DynamicMatchers::FindByBang]
matchersにはActiveRecord::DynamicMatchers::FindBy
とActiveRecord::DynamicMatchers::FindByBang
のクラスオブジェクトが格納されています。
FindByクラスとFindByBangクラスは、それぞれ以下のようなクラスです。
class FindBy < Method Method.matchers << self def self.prefix "find_by" end def finder "find_by" end end class FindByBang < Method Method.matchers << self def self.prefix "find_by" end def self.suffix "!" end def finder "find_by!" end end
また、Methodクラスには以下のようなメソッドpattern
が定義されています。
def pattern @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/ end
pattern
は、文字列変数prefix
、suffix
に挟まれた文字列にマッチするRegexpオブジェクトを返します。
FindByクラスとFindByBangクラスはどちらもMethodクラスを継承しているので、pattern
メソッドを使用することができます。
以上から、klass = matchers.find { |k| k.pattern.match?(name) }
は、find_by
系かfind_by!
系の処理であるかをパターンマッチで判定し、マッチした場合は、対応するクラスオブジェクトを変数klassに格納するという処理を行っている、ということが分かりました。
余談ですが、変数名にclass
でなくklass
が用いられているのは、class
がRubyの予約語で、変数名として使うことができないためです。
クラスオブジェクトの生成
次行のklass.new(model, name) if klass
に進みます。
[1] pry(ActiveRecord::DynamicMatchers::Method)> next From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 34 ActiveRecord::DynamicMatchers::Method.match: 32: def match(model, name) 33: klass = matchers.find { |k| k.pattern.match?(name) } => 34: klass.new(model, name) if klass 35: end
model
にはUserモデル(クラス)自身、name
には呼び出したメソッド名 :find_by_first_nameが入っているのでした。
step
コマンドで内部に入ります。
[1] pry(ActiveRecord::DynamicMatchers::Method)> step From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 53 ActiveRecord::DynamicMatchers::Method#initialize: 52: def initialize(model, name) => 53: @model = model 54: @name = name.to_s 55: @attribute_names = @name.match(self.class.pattern)[1].split("_and_") 56: @attribute_names.map! { |n| @model.attribute_aliases[n] || n } 57: end
渡されたモデルやメソッド名の情報をもとに初期化を行い、オブジェクトを生成しています。
match
メソッドは渡された情報をもとに、先ほどのパターンマッチに合致したFindByクラスまたはFindByBangクラスのオブジェクトを生成し、返り値として返すメソッドだということが分かりました。
コンソールで確かめてみます。finish
コマンドでmethod_missing
メソッドの内部に戻りましょう。
[1] pry(#<ActiveRecord::DynamicMatchers::FindBy>)> finish From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 18 ActiveRecord::DynamicMatchers#method_missing: 15: def method_missing(name, *arguments, &block) 16: match = Method.match(self, name) 17: => 18: if match && match.valid? 19: match.define 20: send(name, *arguments, &block) 21: else 22: super 23: end 24: end [1] pry(User)> match => #<ActiveRecord::DynamicMatchers::FindBy:0x000055cfd8fa6970 @attribute_names=["first_name"], @model=User (call 'User.connection' to establish a connection), @name="find_by_first_name">
実際に、Method.match(self, name)
の返り値が格納されている変数match
には、FindByクラスオブジェクトが格納されていることが確認できました。
また、インスタンス変数には、モデルや探索しようとしている属性の名前、メソッドの名前などの情報が記録されています。
match && match.valid?
From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 18 ActiveRecord::DynamicMatchers#method_missing: 15: def method_missing(name, *arguments, &block) 16: match = Method.match(self, name) 17: => 18: if match && match.valid? 19: match.define 20: send(name, *arguments, &block) 21: else 22: super 23: end 24: end
マッチが完了すると、if文によってマッチが成功したかどうかの判定が行われます。
今回はマッチに成功していますが、マッチに失敗した場合は、super
メソッドによって処理がスーパークラスのmethod_missing
メソッドに引き渡され、通常通りメソッドが存在しないことを告げるエラーが出力されます。
defineメソッドとsendによる実行
[2] pry(User)> next From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 19 ActiveRecord::DynamicMatchers#method_missing: 15: def method_missing(name, *arguments, &block) 16: match = Method.match(self, name) 17: 18: if match && match.valid? => 19: match.define 20: send(name, *arguments, &block) 21: else 22: super 23: end 24: end
マッチに成功したことを確認すると、Methodクラスのdefine
メソッドが呼び出されます。
step
で内部に入ります。
[2] pry(User)> step From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 64 ActiveRecord::DynamicMatchers::Method#define: 63: def define => 64: model.class_eval <<-CODE, __FILE__, __LINE__ + 1 65: def self.#{name}(#{signature}) 66: #{body} 67: end 68: CODE 69: end
class_eval
メソッドで、クラスオブジェクトmatch
が持っている情報をもとに、match
に対して新たなメソッドfind_by_first_name
を定義しています。
class_eval
は、レシーバのクラスのコンテキストで与えられたコードを評価するメソッドです。
これで、最初は未定義だったfind_by_first_name
メソッドがれっきとしたメソッドとして定義されました。
From: /home/vagrant/.rbenv/versions/2.6.4/lib/ruby/gems/2.6.0/gems/activerecord-5.2.4/lib/active_record/dynamic_matchers.rb @ line 20 ActiveRecord::DynamicMatchers#method_missing: 15: def method_missing(name, *arguments, &block) 16: match = Method.match(self, name) 17: 18: if match && match.valid? 19: match.define => 20: send(name, *arguments, &block) 21: else 22: super 23: end 24: end
最後にsend
メソッドによって実際にfind_by_first_name
メソッドが呼び出され、実際にSQL文を発行し、レコードを取り出す処理が行われます(ここから先の流れは割愛します)。
あとがき
少しだけのぞいてみると言ったわりには、ずいぶん長くなってしまいました、ごめんなさい。
Railsの魅力が、Rubyの柔軟な言語仕様の賜物であることを改めて実感しました。
何はさておき、コード読んでみるといろいろな発見があって楽しいよ!ということだけでも伝わったら嬉しいです。
次の記事は id:Pasta-K さんのウェブページの表示を遅くなくしたい時の道標 - ぱすたけ日記です。