0
0

More than 3 years have passed since last update.

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

Posted at

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

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

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

whereメソッドは、実は「上記?の部分に、?ではなく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メソッドは以下のように変更できます。

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メソッドの実装内容は、以下のようなものでした。

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文は以下のようになります。

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メソッドで使用する

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

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

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

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

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に対する変更の内容は、以下のようになります。

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ページにフィードが表示されている様子は以下のようになります。

スクリーンショット 2020-02-26 18.25.04.png

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

スクリーンショット 2020-02-26 18.32.27.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0