らくだ🐫にもできるRailsチュートリアル|14.1

14章では他のユーザーをフォロー及びフォロー解除できる仕組みと
フォローしているユーザーの投稿をステータスフィールドに表示する機能を追加していく。

  1. ユーザー間の関係をどうモデリングするか学ぶ
  2. モデリングに対応するWebインターフェイスを実装
  3. ステータスフィールドの完成版を実装

という流れ。

チュートリアルの中で最も難易度が高い手法をいくつか使っているとの事でガク🐫ブルですねぇ!

ページ操作の全体的なフローは次のようになります。あるユーザー (John Calvin) は自分のプロフィールページを最初に表示し (図 14.1)、フォローするユーザーを選択するためにUsersページ (図 14.2) に移動します。Calvinは2番目のユーザーThomas Hobbes (図 14.3) を表示し、[Follow] ボタンを押してフォローします。これにより、[Follow] ボタンが [Unfollow] に変わり、Hobbes の [followers] カウントが1人増えます (図 14.4)。CalvinがHomeページに戻ると、[following] カウントが1人増え、Hobbesのマイクロポストがステータスフィードに表示されるようになっていることがわかります (図 14.5)。

図解はRoRT本文参照にて

14.1 Relationshipモデル

データモデルの構成を考える

素朴に考えれば、has_many (1対多) の関連付けを用いて「1人のユーザーが複数のユーザーをhas_manyとしてフォローし、1人のユーザーに複数のフォロワーがいることをhas_manyで表す」といった方法でも実装できそうです。

後述ありとの事だけども、この方法ではすぐに壁に突き当たるとの事
→has_many throughというものを使う

ひとまずブランチを作って開始ー

データモデルの問題 (および解決策)

具体例で考えてみる
🐫目線→🐫は🐹をフォローしている
🐹目線→🐹は🐫にフォローされている
🐫は🐹から見ればフォロワー (follower) であり、🐫が🐹をフォロー (followed) した形

Railsにおけるデフォルトの複数形の慣習に従えば、あるユーザーをフォローしているすべてのユーザーの集合はfollowersとなり、user.followersはそれらのユーザーの配列を表すことになります。しかし残念なことに、この名前付けは逆向きではうまくいきません (Railsというより英語の都合ですが)。

これに従うと🐫がフォローしているユーザーの集合はfollowedsとなって英語の文法にも合わないし見苦しい!
よって、ここではTwitterの慣習に倣ってfollowingという呼称を使っていく

呼称が決まったところでモデリングをしていく

user.followingはユーザーの集合でなければならないため、followingテーブルのそれぞれの行は、followed_idで識別可能なユーザーでなければなりません (これはfollower_idの関連付けについても同様です)2 。さらに、それぞれの行はユーザーなので、これらのユーザーに名前やパスワードなどの属性も追加する必要があるでしょう。

図解はRoRT本文参照
上記のデータモデルでは何かと無駄が多い(followingモデルにも名前やメールアドレスがありusersテーブルと被っている)
さらにfollowersの方をモデリングするときにも、同じぐらい無駄の多いテーブルを別に作成しなければならなくなってしまいます
→メンテナンスの観点から見て最悪です!
現状ではユーザー名を変更するたびにusersテーブルだけでなく
followingとfollowersの両テーブルもそのユーザーが含まれるすべての行を更新しなければならない!!

必要な抽象化

この問題の根本は、必要な抽象化を行なっていないことです。正しいモデルを見つけ出す方法の1つは、Webアプリケーションにおけるfollowingの動作をどのように実装するかをじっくり考えることです。

  1. あるユーザーが別のユーザーをフォローするとき何が作成されるか
  2. あるユーザーが別のユーザーをフォロー解除するとき何が削除されるか

→それぞれあるユーザーと別のユーザーの「関係 (リレーションシップ)」が作成されたり削除されたりしている
一人のユーザーは1対多の関係を持てるし、
更にユーザーはリレーションシップを経由することにより、多くのfollowing (またはfollowers) と関係を持てるという事

左右非対称な関係性

Facebookなどでは左右対称なデータモデルが成り立ちますが(Facebook知らないからわかんないけどw)
Twitterのようなフォロー関係では左右非対称の性質がある。
🐫は🐹をフォローしていても、🐹は🐫をフォローしていないといった関係が成り立っている
→それぞれを能動的関係 (Active Relationship)と受動的関係 (Passive Relationship)と呼ぶ
上記の例で言えば🐫は🐹に対して「能動的関係」を持っている。逆に、🐹は🐫に対して「受動的関係」を持っている。と、いう事。

能動的関係

フォローしているユーザーを生成するために能動的関係に焦点を当てていく!
follower_idとfollowed_idを持つテーブルで
followed_idを通して、usersテーブルのフォローされているユーザーを見つけるようにする
RoRT本文参照

能動的関係も受動的関係も、最終的にはデータベースの同じテーブルを使うことになります。したがって、テーブル名にはこの「関係」を表す「relationships」を使いましょう。モデル名はRailsの慣習にならって、Relationshipとします。

更にRoRT本文参照

Relationshipデータモデルを実装

Relationshipモデルはfollower_idとfollowed_idで頻繁に検索することになるので
それぞれのカラムにインデックスを追加する

複合キーインデックスとは何ぞ

上記のコードで言えばfollower_idとfollowed_idの組み合わせが必ず一意であることを保証する仕組み
これにより同じユーザーを2回以上フォローすることを防ぐ

もちろん、このような重複 (2回以上フォローすること) が起きないよう、インターフェイス側の実装でも注意を払います(14.1.4)。しかし、ユーザーが何らかの方法で (例えばcurlなどのコマンドラインツールを使って) Relationshipのデータを操作するようなことも起こり得ます。そのような場合でも、一意なインデックスを追加していれば、エラーを発生させて重複を防ぐことができます。

演習

  1. 図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。
  2. 図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。

RoRT本文・図14.7

  1. id=1のユーザーが持つfollewd_idが配列で返る
    [2,7,10,8]
    • id=2のユーザーが持つfollewd_idと関連付いたidのユーザーデータが返る
      [User id:1, name:Michael Hartl, email:mhartl@example.com]
    • id=2のユーザーが持つfollewd_idが配列で返る
      [1]

14.1.2 User/Relationshipの関連付け

UserとRelationshipの関連付けを行う
1人のユーザーにはhas_many(1対多)のリレーションシップがある
→このリレーションシップは2人のユーザー間の関係なので、フォローしているユーザーとフォロワーの両方に属する(belongs_to)
次のようなユーザー関連付けのコードを使って新しいリレーションシップを作成したいと考える

マイクロポストと関連付けをした際のコードとの違い

ユーザーとマイクロポストを関連付けした際、Userモデルでは下記のようなコードを書いた

Railsは引数の:micropostsシンボルに対応するMicropostモデルを探し出すことが出来たけど
今回も同じように書いてしまうと

となり、RelationshipモデルではなくActiveRelationshipモデルを探してしまう
→探してほしいモデルのクラス名を明示的に伝える必要がある!

反対にMicropostモデルでは下記のように書いた

Railsはデフォルトでは外部キーの名前を<class>_idといったパターンとして理解し、 <class>に当たる部分からクラス名 (正確には小文字に変換されたクラス名) を推測します

よって、micropostテーブルにあるuser_idをたどって所有者 (ユーザー) を特定することが出来る
今回の場合はfollower_idという外部キーを使ってフォロワーを特定したいけどfollowerと言うクラスは存在しない為
Railsに正しいクラス名を伝える必要がある!

これらを踏まえるとUserとRelationshipの関連付けはこのようになる

※マイクロポストの時と同様に、ユーザーを削除したらユーザーのリレーションシップも同時に削除される必要があるため
関連付けにdependent: :destroyも追加している

突然出てきたように見えるactive_relationshipsとは何ぞ

4.1.1で触れられているように能動的関係 (Active Relationship)を表している
フォローした時、フォローされている時を同じモデルを使ってあれこれしたいので
使うモデルはRelationshipモデルだけど
関係性はモデル名ではなく別途定義している→明示的に伝えるモデル名はRelationship
と、いう事。
この章では「フォローする(能動的関係)」を扱っているためactive_relationshipsという関係性を結んでいる
あとで「フォローされている(受動的関係)」を扱う際にpassive_relationshipsという関係性も定義する
なんで別々の名前を使うのか→それぞれの名前を使ったメソッドや外部キーが使えるようになる
参考↓


上記の関連付けを行ったことによりRoRT本文参照なメソッドが使えるようになっている

演習

  1. コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください
  2. 先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう

1.

2.

表現がちょっとややこしいねぇ🐫

14.1.3 Relationshipのバリデーション

Relationshipモデルのテストとバリデーションを追加する

User用のfixtureファイル (リスト 6.30) と同じように、生成されたRelationship用のfixtureでは、マイグレーション (リスト 14.1) で制約させた一意性を満たすことができません。ということで、ユーザーのときと同じで (リスト 6.31でfixtureの内容を削除したように)、今の時点では生成されたRelationship用のfixtureファイルも空にしておきましょう (リスト 14.6)。

テストはGREEN!

演習

  1. リスト 14.5のバリデーションをコメントアウトしても、テストが成功したままになっていることを確認してみましょう。(以前のRailsのバージョンでは、このバリデーションが必須でしたが、Rails 5から必須ではなくなりました。今回はフォロー機能の実装を優先しますが、この手のバリデーションが省略されている可能性があることを頭の片隅で覚えておくと良いでしょう。)

動作確認のみにて省略

14.1.4 フォローしているユーザー

いよいよRelationshipの関連付けの核心、followingとfollowersに取りかかります。今回はhas_many throughを使います

ユーザーには複数の「フォローする」「フォローされる」といった関係性がある→多対多の関係性
デフォルトのhas_many throughという関連付けではモデル名 (単数形) に対応する外部キーを探す

上記のコードは
「followeds」というシンボル名を「followed」という単数形に変えて、 relationshipsテーブルのfollowed_idを使って対象のユーザーを取得する働きを持つ
しかし!
「user.followeds」は英語として不適切なので「user.following」という名前を使いたい!

そのためには、Railsのデフォルトを上書きする必要があります。ここでは:sourceパラメーター (リスト 14.8) を使って、「following配列の元はfollowed idの集合である」ということを明示的にRailsに伝えます。

これによりフォローしているユーザーを配列の様に扱えるようにる!
例えばこんなことが出来るようになっている↓

また、followingで取得したオブジェクトは、配列と同じく要素を追加したり削除したりすることも可能

followingメソッドで配列のように扱えるだけでも便利ですが、Railsは単純な配列ではなく、もっと賢くこの集合を扱っています

follow・unfollowメソッドを追加

followingで取得した集合を簡単に扱えるようにするために
followやunfollowといった便利なメソッドを追加する!
さらにfollowing?論理値メソッドも追加し、特定のユーザーが誰かをフォローしているか確認できるようにする。
まずはテスト!

というのも、Webインターフェイスなどで便利メソッドを使うのはまだ先なので、すぐに使える場面がなく、実装した手応えを得にくいからです。一方で、Userモデルに対するテストを書くのは簡単かつ今すぐできます。そのテストの中で、これらのメソッドを使っていきます。

  1. following?メソッドであるユーザーをまだフォローしていないことを確認
  2. followメソッドを使ってそのユーザーをフォロー
  3. following?メソッドを使ってフォロー中になったことを確認
  4. nfollowメソッドでフォロー解除できたことを確認

メソッドを作ってないからREDになるけど上記の手順で作ったテスト↓

さらに、表 14.1のメソッドを参考にしながら、followingによる関連付けを使ってfollow・unfollow・following?メソッドを実装
この時、可能な限りselfを省略する!
→model内ではselfが省略できるので省略できるものはなるべく省略するのだ!

これにてGREEN!

演習

  1. コンソールを開き、リスト 14.9のコードを順々に実行してみましょう。
  2. 先ほどの演習の各コマンド実行時の結果を見返してみて、実際にはどんなSQLが出力されたのか確認してみましょう。

1.

2.
SQL文を感じましょう🐫(雰囲気は読み取れるよね)

14.1.5 フォロワー

user.followingメソッドと対になるuser.followersメソッドを追加する
RoRT本文の図解参照だけどactive_relationshipsテーブルのfollower_idとfollowed_idを入れ替える形で活用できるっぽいので
実装はこのようになる↓

source: :followerが省略可能なのは
「followers」属性の場合、followedと違いRailsが「followers」を単数形にして自動的にfollower_idを探すことが出来るから!
しかし類似性を強調するためにあえて残しているとの事。

followersに対するテスト

followers.include?メソッドを使ってデータモデルをテストしていく

リスト 14.13ではリスト 14.9に1行だけ追加していますが、実際には多くの処理が正しく動いていなければパスしません。つまり、リスト 14.12の実装に対するテストは、実装の影響を受けやすいテストだといえます。

ともあれテストはGREEN!

演習

  1. コンソールを開き、何人かのユーザーが最初のユーザーをフォローしている状況を作ってみてください。最初のユーザーをuserとすると、user.followers.map(&:id)の値はどのようになっているでしょうか?
  2. 上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。
  3. user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。

  1. userをフォローしたユーザーのidを順に取り出したものになっている
  2. 一致
  3. to_aメソッドが入るとDBのデータを直接ではなく配列を生成してからカウントしているので時間が掛かるしDBに負担が掛かる

まとめとか感想

最難関というだけあって、難しいっていうかややこしいっていうか難しかったでした!
けど、多対多って言うのは活用場面多そうなので頑張ってついていきたい🐫

らくだ🐫にもできるRailsチュートリアルとは

「ド」が付く素人のらくだ🐫が勉強するRailsチュートリアルの学習記録です。
自分用に記録していますが、お役に立つことがあれば幸いです。

調べたとはいえらくだ🐫なりの解釈や説明が含まれます。間違っている部分もあるかと思います。そんな所は教えて頂けますと幸いなのですが、このブログにはコメント機能がありません💧お手数おかけしますがTwitterなどでご連絡いただければ幸いです