フィードに必要となるSQLクエリと、対応するwhere
メソッドの引数
要件は以下です。
- 対象ユーザーがフォローしているユーザーのユーザーidを持つマイクロポストを全て選択する
- かつ、対象ユーザー自身のマイクロポストも全て選択する
以上の要件を満たす最小限のSQLクエリを模式的に書くと、以下のようになります。
SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>
「ログインしているユーザーがフォローしているユーザーのid」というのは、多くの場合複数となります。ゆえに、それらをクエリ問い合わせに使う場合、単一の値ではなく集合として与えなければなりません。そのような場合に使うSQLのキーワードがIN
となります。
Active Recordでは、where
メソッドを使うことによって、SQL文のWHERE
以下に相当する検索条件を与えることができます。上記のSQL文に対応するwhere
メソッドの引数の与え方は以下のようになります。
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
following_ids
の内容は、「ユーザーidをカンマ区切りで列挙したもの」となります。となると、次に問題となるのは、「カンマ区切りでユーザーidを列挙したものを文字列で得るためにはどうすればいいか」ということになりますね。
参考…自身のマイクロポスト全てを取得するためのSQL文と、where
メソッドの引数の与え方
「対象ユーザー自身のマイクロポストを全て選択する」という要件を満たす最小限のクエリは、以下のような内容になります。
SELECT * FROM microposts
WHERE user_id = <user id>
上記のSQL文に対応するwhere
メソッドの引数の与え方は以下のようになります。
Micropost.where("user_id = ?", id)
ユーザーidの列挙を、カンマ区切りの文字列として得る方法
Rubyにおいては、「オブジェクトの列挙から、各オブジェクトの特定の属性値を文字列化し、その結果の列挙を配列として得る」という操作は、「当該オブジェクトの列挙に対して、ブロック内でto_s
メソッドを呼び出す形でmap
メソッドを実行する」という処理を行うことによって実現できます。
>> [1, 2, 3, 4].map { |i| i.to_s}
=> ["1", "2", "3", "4"]
上記のmap
メソッドの呼び出しでは、各要素に対してto_s
メソッドが実行されています。map
メソッド(やeach
メソッド等)に与えるブロックにおいて、内部の処理が「単一のメソッドを実行する」というものである場合、「map
メソッド(やeach
メソッド等)の引数として、ブロック中で実行するメソッドのシンボルを&
に続けて記述したものを与える」という省略表記を使うことが可能です。
>> [1, 2, 3, 4].map(&:to_s)
=> ["1", "2", "3", "4"]
上記結果に対してjoin
メソッドを使うと、以下のように、idの集合をカンマ区切りで繋げた文字列を得ることができます。
>> [1, 2, 3, 4].map(&:to_s).join(", ")
=> "1, 2, 3, 4"
実際に、User
オブジェクトの列挙からidの集合をカンマ区切りの文字列として得てみる
データベースの最初のユーザーを対象として、フォローしている全ユーザーのidを配列として得てみます。
User.first.following.map(&:id)
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
このような呼び出しは、実際のアプリケーションでも頻繁に行われます。そのため、Active Recordでは次のようなメソッドも用意されています1。
>> User.first.following_ids
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
ここまでで得られたのはあくまで配列です。最終的には、得られた配列を「カンマ区切りの文字列」に変換しなければなりません。お目当ての出力を得るためには、join(', ')
をメソッドチェーンの最後につないでやる必要がある、ということですね。
>> User.first.following_ids.join(', ')
=> "3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"
これで「カンマ区切りでユーザーidを列挙したものを文字列で得る」という出力結果を得ることができました。
where
メソッドの引数内でfollowing_ids
メソッドを使う
user.following_ids.join(', ')
というのは、あくまで説明用のコードであり、実際のRailsアプリケーションでこのようなコードを使うことはありません。実際のRailsアプリケーションにおいて、where
メソッドの引数内でfollowing_ids
メソッドを使う場合、特に引数を取ることなくfollowing_ids
と書けばいいのです。where
の第一引数で正しく?
が使われていれば、それで「カンマ区切りでユーザーidを列挙したものを含めたSQLクエリを文字列で得る」ことができるのです。RDBMS依存の非互換性の一部も含め、全てはRailsがよしなにしてくれます。
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
app/models/user.rb
の実装を変更する
ここまでの内容を踏まえ、実際にapp/models/user.rb
に加える変更の内容は以下のようになります。
class User < ApplicationRecord
...略
# 試作feedの定義
# 完全な実装は次章の「ユーザーをフォローする」を参照
def feed
- Micropost.where("user_id = ?", id)
+ Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
end
...略
end
test/models/user_test.rb
に対し、改めてテストを実行する
ここまでの実装を反映すれば、test/models/user_test.rb
に対するテストが通るようになります。
# rails test test/models/user_test.rb
Running via Spring preloader in process 178
Started with run options --seed 54454
15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01
Finished in 1.41146s
15 tests, 64 assertions, 0 failures, 0 errors, 0 skips
- ログインユーザー自身のマイクロポストがステータスフィードに含まれていること
- フォローしているユーザーのマイクロポストがステータスフィードに含まれていること
- フォローしていないユーザーのマイクロポストがステータスフィードに含まれていないこと
以上の機能が実現されている、ということになりますね!
-
当該メソッド名は、Active Recordで使用しているモデル名そのものが含まれています。ライブラリ作成時にモデル名がわかるはずがないので、「黒魔術」ことメタプログラミングの手法を使わなければ実現できないタイプの実装といえますね。 ↩