68日目
今日の学習
Ruby on Rails
Railsチュートリアル 第14章
今日でおそらく最後になるチュートリアル。
最後に学ぶのはステータスフィードというもの。
ステータスフィード
このときに作成した原型をさらに改良し、フィードを汎用化してフォローしているユーザーの投稿を表示して、さらに自分の投稿もそこへ表示させたりするようだ。
Railsチュートリアルのモックアップは、ものすごい初期のTwitterUIを彷彿とさせる。
フィードの計画
目的は、現在のユーザー(current_user
)によってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出す。
Railsチュートリアルでは、最初にテストを書くところから始まっている。
テストで満たすべき条件は以下の三つ。
- フォローしているユーザーのマイクロポストがフィードに含まれている
- 自分自身のマイクロポストもフィードに含まれている
- フォローしていないユーザーのマイクロポストがフィードに含まれていない
fixture
ファイルでテストユーザーのフォロー関係を作っておき、feed
にフォローしているユーザーの投稿があるかどうかを確認するようなテストを作っていく。
test/models/user_test.rb
test "feed should have the right posts" do user1 = users(:user1) user2 = users(:user2) user3 = users(:user3) # user1の投稿が、フォローしているuser2のフィードに含まれているか確認 user1.microposts.each do |post_following| assert user2.feed.include?(post_following) end # 自分自身の投稿を確認 user1.microposts.each do |post_self| assert user1.feed.include?(post_self) end # フォローしていないユーザーの投稿を確認 user3.microposts.each do |post_unfollowed| assert_not user1.feed.include?(post_unfollowed) end end
まだフィードの実装ができていないので、このテストをパスすることはできない。
次は、このテストをパスするようにフィードの実装を行なっていく。
フィードの実装
最初に、フィードに必要なクエリについて考える。
ここで必要なクエリは、microposts
テーブルから、あるユーザー(current_user)がフォローしているユーザーに対応するidを持つマイクロポストを全て選択(select
)すること。
SELECT * FROM micropost WHERE user_id IN (<list of ids>) OR user_id = <user id>
IN
を使うことで、idの集合の内包(set inclusion)に対してテストを行えるらしい。
二行目のクエリは、取得条件をuser_id
に<list of ids>
か、user_id = <user_id>
を含むものとしている。
<>
は単純に強調しているだけで、コードではないらしい。
SQLでIN句を使おう!基本からサブクエリ活用方法まで一覧紹介 | 侍エンジニアブログ
【SQL】IN句まとめ(複数条件や否定)~where in~|sampling2x
IN
句は、WHERE
内で使用されるもので、OR
を省略することができるようだ。
しかし、先ほどの文章ではIN
の後ろにOR
がまた出てきているが...。
自分のポストのみを選択する場合は、以下のように単純だった。現在のユーザーに対応するユーザーidを持つマイクロポストを選択している。
Micropost.where("user_id = ?", id)
これが複雑になり、フォローされているユーザーに対応するidの配列が必要になる。
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
クエリを実行するために、フォローされているユーザーに対応するidの配列が必要なので、map
メソッドを使ってその配列を用意する。
map
メソッドの復習...今回の目的である集合を文字列としてカンマ区切りで繋げる場合のコード
[1, 2, 3, 4].map(&:to_s).join(", ") => "1, 2, 3, 4"
このコードを使えば、user.following
にある各要素のid
を呼び出して、フォローしているユーザーのidを配列として扱える。
User.first.following.map(&:id) => User.firstがフォロー中のユーザーが配列として出力される => もしもidが2, 3, 5, 10のユーザーをフォローしていた場合は以下のようになる => [2, 3, 5, 10]
Active Recordはfollowing_ids
(先程のクエリに含まれていたもの)というメソッドを既に用意してくれている。このメソッドを使うことで、上のコードと全く同じ結果を取得できる。
User.first.following_ids => [2, 3, 5, 10]
このfollowing_ids
メソッドは、has_many :following
の関連付けをするとActiveRecordが自動生成してくれる。
user.following
コレクションに対応するidを取得する際には、関連付けの名前の末尾に_ids
を付け足すだけで良くなった。
実際にSQL文に挿入する際は、このように記述する必要はなく、?
を内挿すると自動的に処理をしてくれるそうだ。さらにデータベースに依存する一部の非互換性も解消してくれるらしい。
User.first.following_ids.join(", ")
とせずとも、following_ids
メソッドをそのまま使うだけでよい。
では、ユーザーのステータスフィードを返すfeed
メソッドを、app/models/user.rb
に追加する。
def feed Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id) end
これで、先ほどのテストの条件を満たすことができた。
サブセレクト
先ほどの実装には問題があり、投稿されたマイクロポストの数が膨大になったとき、うまくスケールしない。(機能しなくなってしまうという意味か)。
例えばフォローしているユーザーが5000人のような大規模な数になると、Webサービス全体が遅くなる可能性がある。
そこで、フォローしているユーザー数に応じてスケールするように、ステータスフィードを改善していく。
先ほどのコードの問題点は、following_ids
でフォローしている全てのユーザーをデータベースに問い合わせ、さらにフォローしているユーザーの完全な配列を作るために再度データベースに問い合わせている部分。
この問題は、SQLのサブセレクト(subselect)というものを使うと解決できるらしい。
# 先ほどのコード 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)
疑問符?
の部分が置き換わっている。
同じ変数を複数の場所に挿入したい場合は、置き換え後の文法を使う方が便利だそうだ。
このSQLクエリに、もう一つのuser_id
を追加する。
following_ids
というRubyコードは、以下のようなSQLに置き換えることができる。
following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id"
このコードを、SQLのサブセレクトとして使う。 ユーザー1がフォローしているユーザー全てを選択するというSQLを、既存のSQLに内包させる。
SELECT * FROM microposts WHERE user_id IN (SELECT followed_id FROM relationships WHERE follower_id = 1) OR user_id = 1
このサブセレクトは集合のロジックを、Railsではなくデータベースに保存するのでより効率的にデータを取得できる。
最終的に、以下のような形になる。RailsとRubyとSQLのコードが合体している。
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
これで目的のステータスフィードの実装が完了した。
自分のポスト以外に、フォローしているユーザーのポストもフィードに表示されている。また、フォローを解除すると、そのユーザーのポストがちゃんと表示されなくなることも確認した。
デプロイをして、完了。正直SQL文のところは全然理解できなかったので、SQLの勉強をもっとしっかりするべきだと感じた。
https://mighty-reef-20863.herokuapp.com/
↑実際にherokuにデプロイをしたサイト。
Railsチュートリアル最後の演習では、feed
メソッドのコードの書き換えを紹介している。
現在のコードはSQLのLEFT JOIN
、すなわちleft_outer_joins
メソッドを使えば、Railsdで直接表現できるそうだ。
distinct
メソッドを使い、コレクション内の重複を削除している。
def feed part_of_feed = "relationships.follower_id = :id or microposts.user_id = :id" Micropost.left_outer_joins(user: :followers) .where(part_of_feed, { id: id }).distinct .includes(:user, image_attachment: :blob) end
Railsチュートリアルについて調べていたら、もっと短くて簡潔な形を発見した。
らくだ🐫にもできるRailsチュートリアル|14.3 | らくだ🐫のさいと
def feed Micropost.where(user: following).or(Micropost.where(user_id: id)) end
たったの一行で済んでしまった。テストもしっかりパスしている。
就職活動用のポートフォリオを作成したいため、このまま追加機能などを練習で付与していくかどうか迷う。どちらもすればよいか。
SQL
サブクエリ
クエリの部分がさっぱりだったので、サブセレクトについてちゃんと調べておこうと思い、サブセレクトで検索をしてみると、引っかかるのは主に「サブクエリ」。
ProgateでSQLを学んだときに、かなり苦戦していたのでおそらく自分はSQL文が苦手なのだろう。
実際にSQL文を使うことは多いとよく聞くので、ちゃんとできるようにしておきたい。
とりあえずサブクエリについての理解を深めるために、サブクエリの意味を調べてみた。
https://wa3.i-3-i.info/word17573.html
それを踏まえて、サブセレクトについて見てみる。
つまり、SELECT
を利用しているサブクエリ(入れ子になって書かれているSQL文)ということでいいだろうか。
意味は分かったが、それでもRailsチュートリアルに出てきたSQL文があまりうまく飲み込めなかった...。
Railsのクエリインタフェース select、where、or、mergeメソッドの使い方 - Qiita
明日はSQLの基礎的なことを学び直しながら、こちらの記事も参考に学習していきたい。
チュートリアルの感想
感想というほどのことでもないが、とりあえず最後まで進められたのでよかったという話をしたい。
チュートリアルを始める前に、実際にチュートリアルをやっておられた方から「あまり楽しくないですよ」と言った風にお聞きしていたのだが、自分の場合はちょっとずつ前進している感じがあって終始楽しく進めることができた。
コツコツする作業が好きな人は多分楽しめるだろうと思う。
ただ、実装していく中であまりつまづくことはなかったが、演習は自分にとってけっこうハードルが高かったし、何度も答えをネットから探したりしていた。
テストを書く経験が0から学べたのはよかったと思うが、おそらく自分で「これを実装するにはこういったテストを書けばいいはずだ」と判断してテストを書いていくのは難しいと思う。
あとは、CSSなどをRailsチュートリアル側が全て用意してくれていたからよかったものの、本来であればデザイン面でも苦戦する部分がたくさんあるんだろうなと思う。
学ぶものがまだまだ多いと感じるが、総じて「やってよかった」と思った。1000円でこれはかなり安いと思う。知らない知識がたくさん手に入った。
学習メモ
Railsの redirect_to
redirect_to @userが何を省略しているかわかりますか?〜挫折しないRailsチュートリアル7章〜 - Qiita
検索中に見かけた。とても分かりやすかったので貼っておく。
Railsでは、基本は相対パスすなわちpathヘルパーを利用しつつ、redirect_toメソッドでは絶対パスすなわちurlヘルパーを利用するのが慣例となっています。
これは頭から抜けていた...。