Railsでincludes(eager_load)した値を、where句を使用せずに絞り込む

調べたけど意外に出てこなかったので書いてみます。
ActiveRecord の join, includes, preload, eager_load の違いについては、この記事では言及しません。

## ユースケース

N + 1 問題回避のために includes を使用したいけれど、絞り込みには where 句を使いたくない。
例えば includes で取得した値に対して、同じ処理内で異なった条件での絞り込みをしたいとき。

## 結論
[ActiveRecord::QueryMethods#where](https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-where) じゃなくて、 [Array#select](https://docs.ruby-lang.org/ja/latest/method/Array/i/filter.html) を使用して絞り込む。

[追記]
別のやり方として、association を応用する方法を教えていただきました。詳細は[コメント参照](https://qiita.com/t0yohei/items/53941dac0d523066b62f#comment-90d7effcc662958022c0)

## サンプルコード
https://github.com/t0yohei/rails-ar-filter-without-where

## 本題

例えば以下のようなテーブル構造に対して、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152779/c9e0214a-7fd7-905a-7e7c-fe76c33c6799.png)


次のようなレコードが用意されているとする。

```ruby
author_one = Author.find_or_create_by!(name: 'author_one')
author_two = Author.find_or_create_by!(name: 'author_two')

book_one = Book.find_or_create_by!(name: 'book_one', author_id: author_one.id, book_type: 'magazine')
book_two = Book.find_or_create_by!(name: 'book_two', author_id: author_one.id, book_type: 'comic')
book_three = Book.find_or_create_by!(name: 'book_three', author_id: author_two.id, book_type: 'magazine')
book_four = Book.find_or_create_by!(name: 'book_four', author_id: author_two.id, book_type: 'comic')
```

## eager_load(includes)などを使わない場合(N + 1 問題が発生する場合)

eager_load(includes)などを使わずに、下記のコードを実行すると N + 1 問題が発生する。

```ruby
authors = Author.all
authors.each do |author|
  author.books.each_with_index do |book, index|
    pp "--book_#{index}--", book
  end
end

# 出力結果
"--book_0--"
#<Book:0x00007f83b6adf9a8
 id: 1,
 name: "book_one",
 author_id: 1,
 book_type: "magazine">
"--book_1--"
#<Book:0x00007f83ba996fc0
 id: 2,
 name: "book_two",
 author_id: 1,
 book_type: "comic">
"--book_0--"
#<Book:0x00007f83b7544628
 id: 3,
 name: "book_three",
 author_id: 2,
 book_type: "magazine">
"--book_1--"
#<Book:0x00007f83b7544538
 id: 4,
 name: "book_four",
 author_id: 2,
 book_type: "comic">
```

実行ログを見て発行される SQL を確認すると、以下のように N + 1 問題が発生している。

```ruby
  Author Load (0.6ms)  SELECT "authors".* FROM "authors"
  Book Load (0.5ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ?  [["author_id", 1]]
  Book Load (0.1ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ?  [["author_id", 2]]
```

## eager_load(includes) + where を使って絞り込みを行う

eager_load(includes) + where を使って絞り込みを行うと、N + 1 問題を回避した上で次のように book_type が magazine のものだけが取得できる。

```ruby
authors = Author.all.eager_load(:books).where(books: { book_type: 'magazine' })
authors.each do |author|
  author.books.each_with_index do |book, index|
    pp "--magazine_#{index}--", book
  end
end

# 出力結果
"--magazine_0--"
#<Book:0x00007f83b74b9a78
 id: 1,
 name: "book_one",
 author_id: 1,
 book_type: "magazine">
"--magazine_0--"
#<Book:0x00007f83ba97d2f0
 id: 3,
 name: "book_three",
 author_id: 2,
 book_type: "magazine">
```

 SQL を確認すると、以下のように N + 1 問題が回避されたものとなる。

```ruby
  SQL (0.1ms)  SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, "books"."id" AS t1_r0, "books"."name" AS t1_r1, "books"."author_id" AS t1_r2, "books"."book_type" AS t1_r3 FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id" WHERE "books"."book_type" = ?  [["book_type", "magazine"]]
```
  
しかし、追加で同じ処理内で異なる book_type のものを取得しようした場合、

```diff
authors = Author.all.eager_load(:books).where(books: { book_type: 'magazine' })
authors.each do |author|
  author.books.each_with_index do |book, index|
    pp "--magazine_#{index}--", book
  end
end


+ authors.each do |author|
+   author.books.where(book_type: 'comic').each_with_index do |book, index|
+     pp "--comic_#{index}--", book
+   end
+ end

# 出力結果
"--magazine_0--"
#<Book:0x00007f83b74b9a78
 id: 1,
 name: "book_one",
 author_id: 1,
 book_type: "magazine">
"--magazine_0--"
#<Book:0x00007f83ba97d2f0
 id: 3,
 name: "book_three",
 author_id: 2,
 book_type: "magazine">
"--comic_0--"
#<Book:0x00007f83b74f4010
 id: 2,
 name: "book_two",
 author_id: 1,
 book_type: "comic">
"--comic_0--"
#<Book:0x00007f83b74e7bd0
 id: 4,
 name: "book_four",
 author_id: 2,
 book_type: "comic">
```

追加で SQL が発行されてしまう。

```ruby
  SQL (0.8ms)  SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, "books"."id" AS t1_r0, "books"."name" AS t1_r1, "books"."author_id" AS t1_r2, "books"."book_type" AS t1_r3 FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id" WHERE "books"."book_type" = ?  [["book_type", "magazine"]]
  Book Load (0.2ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ? AND "books"."book_type" = ?  [["author_id", 1], ["book_type", "comic"]]
  Book Load (0.1ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ? AND "books"."book_type" = ?  [["author_id", 2], ["book_type", "comic"]]
```

## eager_load(includes) + select を使って絞り込みを行う

eager_load(includes) + select を使って絞り込みを行うと、同じ処理内で異なる book_type のものを取得しようした場合でも、N + 1 問題を回避することができる。

```ruby
authors = Author.all.eager_load(:books)
authors.each do |author|
  magazines = author.books.select{ |book| book.book_type == 'magazine' }
  magazines.each_with_index do |magazine, index|
    pp "--magazine_#{index}--", magazine
  end
end

authors.each do |author|
  comics = author.books.select{ |book| book.book_type == 'comic' }
  comics.each_with_index do |comic, index|
    pp "--comic_#{index}--", comic
  end
end

# 実行結果
"--magazine_0--"
#<Book:0x00007f83b74dd428
 id: 1,
 name: "book_one",
 author_id: 1,
 book_type: "magazine">
"--magazine_0--"
#<Book:0x00007f83b74f49c0
 id: 3,
 name: "book_three",
 author_id: 2,
 book_type: "magazine">
"--comic_0--"
#<Book:0x00007f83b74f50a0
 id: 2,
 name: "book_two",
 author_id: 1,
 book_type: "comic">
"--comic_0--"
#<Book:0x00007f83b74f4c40
 id: 4,
 name: "book_four",
 author_id: 2,
 book_type: "comic">
```

発行される SQL は一つだけとなっている。

```ruby
  SQL (0.2ms)  SELECT "authors"."id" AS t0_r0, "authors"."name" AS t0_r1, "books"."id" AS t1_r0, "books"."name" AS t1_r1, "books"."author_id" AS t1_r2, "books"."book_type" AS t1_r3 FROM "authors" LEFT OUTER JOIN "books" ON "books"."author_id" = "authors"."id"
```

## おまけ
N + 1 問題を回避するために includes などを使うと LEFT OUTER JOIN が発生するので、 JOIN するテーブルの規模がとってもとっても大きかったりすると、実は N + 1 を発生させたままにしておいた方が早いってことがあったりするらしい。

実際に色々試して計測して、一番早くなる方法を採用するのが良いですね。おわり。