プログラミング備忘録

プログラミングの学習状況をメモしています

68日目

今日の学習

Ruby on Rails

Railsチュートリアル 第14章

今日でおそらく最後になるチュートリアル

最後に学ぶのはステータスフィードというもの。

railstutorial.jp

ステータスフィード

このときに作成した原型をさらに改良し、フィードを汎用化してフォローしているユーザーの投稿を表示して、さらに自分の投稿もそこへ表示させたりするようだ。

Railsチュートリアルモックアップは、ものすごい初期のTwitterUIを彷彿とさせる。

フィードの計画

目的は、現在のユーザー(current_user)によってフォローされているユーザーに対応するユーザーidを持つマイクロポストを取り出し、同時に現在のユーザー自身のマイクロポストも一緒に取り出す。

Railsチュートリアルでは、最初にテストを書くところから始まっている。

テストで満たすべき条件は以下の三つ。

  1. フォローしているユーザーのマイクロポストがフィードに含まれている
  2. 自分自身のマイクロポストもフィードに含まれている
  3. フォローしていないユーザーのマイクロポストがフィードに含まれていない

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ではなくデータベースに保存するのでより効率的にデータを取得できる。

最終的に、以下のような形になる。RailsRubySQLのコードが合体している。

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

これで目的のステータスフィードの実装が完了した。

f:id:hasegawa_note:20210715144605p:plain

自分のポスト以外に、フォローしているユーザーのポストもフィードに表示されている。また、フォローを解除すると、そのユーザーのポストがちゃんと表示されなくなることも確認した。

デプロイをして、完了。正直SQL文のところは全然理解できなかったので、SQLの勉強をもっとしっかりするべきだと感じた。

https://mighty-reef-20863.herokuapp.com/

↑実際にherokuにデプロイをしたサイト。

Railsチュートリアル最後の演習では、feedメソッドのコードの書き換えを紹介している。

現在のコードはSQLLEFT 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

入れ子になって書かれているSQL文における中に書いてある方のSQL文のこと

それを踏まえて、サブセレクトについて見てみる。

クエリ内のサブセレクト | Microsoft Docs

つまり、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ヘルパーを利用するのが慣例となっています。

これは頭から抜けていた...。