Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - サブセレクト

# 前準備 - `where`メソッド内の変数に、キーと値のペアを使うようにする

`where`メソッドの第1引数であるSQL文において、Rails側の変数の内容を使う部分は、これまで`?`（疑問符）として与えてきました。以下のようなメソッド呼び出しがその例です。

```ruby:
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
```

`where`メソッドは、実は「上記`?`の部分に、`?`ではなくRubyのシンボルを与える」という使い方ができます。以下のようなメソッド呼び出しがその例です。

```ruby:
Micropost.where(
  "user_id IN (:following_ids) OR user_id = :user_id",
  following_ids: following_ids,
  user_id: id
)
```

結果、`app/models/user.rb`内の`feed`メソッドは以下のように変更できます。

```diff:app/models/user.rb#feed
  def feed
-   Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
+   Micropost.where(
+     "user_id IN (:following_ids) OR user_id = :user_id",
+     following_ids: following_ids,
+     user_id: id
+   )
  end
```

このような変更をするからには、「`following_ids`もしくは`user_id`を複数箇所で使う」という実装が発生するということなのでしょう。

# 「フィードを初めて実装する」の実装の問題点

「フィードを初めて実装する」の実装では、「投稿されたマイクロポストの数が膨大になった際にうまくスケールしない」という問題点があります。Railsチュートリアル本文には以下のようにあります。

> フォローしているユーザーが5,000人程度になるとWebサービス全体が遅くなる可能性があります

現状の実装は、一体どのような点でスケールしないのでしょうか。「よりスケールする実装」というのは、一体どのような実装なのでしょうか。

# 現状の実装ではどういう処理がされているのか、現状の実装は何が問題なのか

「フィードを初めて実装する」の実装における`feed`メソッドの実装内容は、以下のようなものでした。

```ruby:
def feed
  Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
end
```

上記のコードは、最終的に以下のような動作をすることになります。

1. `following_ids`メソッドにより、現在フォローしている全てのユーザーを得るためにRDBに問い合わせを行う
2. 全ての`Micropost`オブジェクトを得るため、前述`following_ids`メソッドの戻り値を条件に、RDBのmicropostsテーブル全体を対象として問い合わせを行う

今回開発しているアプリケーションのユースケースでは、idが上記1.の集合に内包されているかどうかだけをチェックするため、RDBに2回問い合わせを行う現状の動作はどうにもまどろっこしいです。また、Railsが介入する必要がないであろうところにRailsが介入しているのはよろしくありません。

より効率的な方法はないのでしょうか。いや、こうした集合計算に特化した言語であるSQLなら、より効率的な方法はきっとあるはずです。

# どういう処理だとよりいいのか - サブクエリ（サブセレクト）を用いたクエリの使用

今回行おうとしている処理の場合、「`SELECT`文の結果そのものを、（Railsに渡すことなく）次段の`SELECT`文の評価の対象とする」という形にすることによって、全てをRDBMS内で完結させることができます。RDBMSはこうした処理に最適化されているので、全てをRDBMS内で完結させることができれば、途中でRailsが介在する実装より高速な処理が実現できます。

例えば現在のユーザーのidが1である場合、このような処理を実現するためのSQL文は以下のようになります。

```sql:
SELECT * FROM microposts
WHERE user_id IN (
  SELECT followed_id FROM relationships
  WHERE follower_id = 1
) OR user_id = 1
```

上記SQL文のように、「`SELECT`文の結果そのものを、次段のSQL文の評価の対象とする」処理は「サブクエリを用いたクエリ」と呼ばれます。`()`の内側のSQL文は「サブクエリ（もしくはサブセレクト）」と呼ばれます。

このようなサブクエリを用いたクエリにおいては、集合を組み立てるロジックはRDBMS内で完結します。

# サブクエリを用いたクエリを`where`メソッドで使用する

```ruby:
Micropost.where(
  "user_id IN (:following_ids) OR user_id = :user_id",
  following_ids: following_ids,
  user_id: id
)
```

上記`Micropost.where`の引数`:following_ids`は、前述「サブクエリを用いたクエリの使用」を踏まえて、以下のように書き換えることができます。

```ruby:
following_ids =
  "SELECT followed_id FROM relationships
  WHERE follower_id = :user_id"
```

結果、`User#feed`メソッドの実装は以下のように書き換えられる、ということになるわけです。

```ruby:User#feed
def feed
  following_ids =
    "SELECT followed_id FROM relationships
    WHERE follower_id = :user_id"
  Micropost.where(
    "user_id IN (#{following_ids}) OR user_id = :user_id",
    user_id: id
  )
end
```

# フィードの最終的な実装

ここまでの内容を踏まえると、`app/models/user.rb`に対する変更の内容は、以下のようになります。

```diff:app/models/user.rb#feed
  def feed
-   Micropost.where(
-     "user_id IN :following_ids, OR user_id = :user_id",
-     following_ids: following_ids,
-     user_id: id
-   )
+   following_ids =
+     "SELECT followed_id FROM relationships
+     WHERE follower_id = :user_id"
+   Micropost.where(
+     "user_id IN (#{following_ids}) OR user_id = :user_id",
+     user_id: id
  end
```

`test/models/user_test.rb`を対象としたテストも実行しておきましょう、

```
# rails test test/models/user_test.rb
Running via Spring preloader in process 428
Started with run options --seed 8307

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.29007s
15 tests, 64 assertions, 0 failures, 0 errors, 0 skips
```

無事テストが通りました。

開発環境において、Homeページにフィードが表示されている様子は以下のようになります。

<img width="1161" alt="スクリーンショット 2020-02-26 18.25.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/851a4f92-b34b-98ca-1795-95baba5f29ee.png">

本番環境において、Homeページにフィードが表示されている様子は以下のようになります。

<img width="1161" alt="スクリーンショット 2020-02-26 18.32.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/62c1dff9-fba1-a97f-84b3-10896ed8f1ce.png">