includesの挙動模索

普段、N+1問題を防ぐためにとりあえずincludesをしてしまうのですが、includesがどんな挙動をしているのか理解していなかったので調べることにしました!

(昨日からActiveRecord読みたい気持ちが高まっていたため、includesのソースコードを読むことに挑戦したのですが良く分からず、動かしながら挙動を理解する方針に変更しましたw)

 

 

定義元のコード

includesメソッドは以下のように定義されておりました。

引数があるかチェックし、クローンしたオブジェクトにincludes!を呼んでいます。

active_record/relation/query_methods.rb

def includes(*args)
check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
end

 

※check_if_method_has_arguments!メソッドは引数がないときにArgumentErrorを起こすメソッドのようです。

def check_if_method_has_arguments!(method_name, args)
if args.blank?
raise ArgumentError, "The method .#{method_name}() must contain arguments."
end
end

 

※spawnはオブジェクト?をクローンしているようです。

module ActiveRecord
module SpawnMethods
# This is overridden by Associations::CollectionProxy
def spawn #:nodoc:
clone
end

 

※includes!は配列からnilと空文字を取り除き、配列の入子を平坦化し、self.includes_valuesに引数を代入しています。

def includes!(*args) # :nodoc:
args.reject!(&:blank?)
args.flatten!

self.includes_values |= args
self
end

 

includesが結局何しているのか分からない。。。w

 

 

挙動を調べる

reviewモデルとcommentモデルがあり、以下のような関係とします。

has_many :comments

belongs_to :review

 

 

 

  • 子モデルをincludes

preloadした時のように、reviewの全レコードをselect文で取得し、取得したreviewのidと一致するreview_idカラムを持っているcommentモデルのレコードを取得しています。

irb(main):095:0> reviews = Review.includes(:comments)
Review Load (0.5ms) SELECT `reviews`.* FROM `reviews` LIMIT 11
Comment Load (0.3ms) SELECT `comments`.* FROM `comments` WHERE `comments`.`review_id` IN (1, 2, 3, 4, 5)
=> #<ActiveRecord::Relation [#<Review id: 1, rate: 5, body: "Good">, #<Review id: 2, rate: 3, body: "So-so">, #<Review id: 3, rate: 5, body: "Great">, #<Review id: 4, rate: 1, body: "Bad">, #<Review id: 5, rate: 2, body: "I do not like it">]> 

 

  •  親モデルをincludes

こちらもpreloadのような挙動です。

irb(main):002:0> comments = Comment.includes(:review)
Comment Load (0.4ms) SELECT `comments`.* FROM `comments` LIMIT 11
Review Load (45.6ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` IN (1, 2)
=> #<ActiveRecord::Relation [#<Comment id: 1, body: "hoge", review_id: 1, created_at: "2019-11-16 08:07:32", updated_at: "2019-11-16 08:07:32">, #<Comment id: 2, body: "fuga", review_id: 2, created_at: "2019-11-16 08:07:41", updated_at: "2019-11-16 08:07:41">]>

 

 

  • 子レコードをincludesして親レコードで絞り込み

こちらもpreloadのような挙動です。

irb(main):101:0> reviews = Review.includes(:comments).where(id: [1.2])
Review Load (0.9ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` = 1 LIMIT 11
Comment Load (0.4ms) SELECT `comments`.* FROM `comments` WHERE `comments`.`review_id` = 1
=> #<ActiveRecord::Relation [#<Review id: 1, rate: 5, body: "Good">]>

 

  • 子レコードをincludesして子レコードで絞り込み

この場合、eager_loadのようにreviewsテーブルに対してcommentsテーブルをleft outer joinしています。

その後、最初のselect文で取得したcommentsテーブルのレコードのreview_idと一致するidを持つReviewのレコードを取得しています。

irb(main):103:0> reviews = Review.includes(:comments).where(comments: {id: [1,2]})
SQL (2.7ms) SELECT DISTINCT `reviews`.`id` FROM `reviews` LEFT OUTER JOIN `comments` ON `comments`.`review_id` = `reviews`.`id` WHERE `comments`.`id` IN (1, 2) LIMIT 11
SQL (0.3ms) SELECT `reviews`.`id` AS t0_r0, `reviews`.`rate` AS t0_r1, `reviews`.`body` AS t0_r2, `comments`.`id` AS t1_r0, `comments`.`body` AS t1_r1, `comments`.`review_id` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4 FROM `reviews` LEFT OUTER JOIN `comments` ON `comments`.`review_id` = `reviews`.`id` WHERE `comments`.`id` IN (1, 2) AND `reviews`.`id` IN (1, 2)
=> #<ActiveRecord::Relation [#<Review id: 1, rate: 5, body: "Good">, #<Review id: 2, rate: 3, body: "So-so">]>

 

  • 親レコードをincludesして子レコードで絞り込み

こちらはpreloadのような挙動でした。

irb(main):106:0> comments = Comment.includes(:review).where(id: [1,2])
Comment Load (1.3ms) SELECT `comments`.* FROM `comments` WHERE `comments`.`id` IN (1, 2) LIMIT 11
Review Load (0.4ms) SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`id` IN (1, 2)

=> #<ActiveRecord::Relation [#<Comment id: 1, body: "hoge", review_id: 1, created_at: "2019-11-16 08:07:32", updated_at: "2019-11-16 08:07:32">, #<Comment id: 2, body: "fuga", review_id: 2, created_at: "2019-11-16 08:07:41", updated_at: "2019-11-16 08:07:41">]>

 

  • 親レコードをincludesして親レコードで絞り込み

こちらは不可でした。

irb(main):107:0> comments = Comment.includes(:review).where(review: {id: [1,2,3]})
SQL (1.5ms) SELECT `comments`.`id` AS t0_r0, `comments`.`body` AS t0_r1, `comments`.`review_id` AS t0_r2, `comments`.`created_at` AS t0_r3, `comments`.`updated_at` AS t0_r4, `reviews`.`id` AS t1_r0, `reviews`.`rate` AS t1_r1, `reviews`.`body` AS t1_r2 FROM `comments` LEFT OUTER JOIN `reviews` ON `reviews`.`id` = `comments`.`review_id` WHERE `review`.`id` IN (1, 2, 3) LIMIT 11
Traceback (most recent call last):
ActiveRecord::StatementInvalid (Mysql2::Error: Unknown column 'review.id' in 'where clause': SELECT `comments`.`id` AS t0_r0, `comments`.`body` AS t0_r1, `comments`.`review_id` AS t0_r2, `comments`.`created_at` AS t0_r3, `comments`.`updated_at` AS t0_r4, `reviews`.`id` AS t1_r0, `reviews`.`rate` AS t1_r1, `reviews`.`body` AS t1_r2 FROM `comments` LEFT OUTER JOIN `reviews` ON `reviews`.`id` = `comments`.`review_id` WHERE `review`.`id` IN (1, 2, 3) LIMIT 11)

 

 

 

感想その他

includeの使い分け模索&コードリーディングは引き続き行おうと思います。

gemのコードリーディングする時はruby mineが便利ですね!久々にruby mine使って思いました!