railsのアソシエーションを学ぶ その2(多対多のアソシエーションと中間テーブル)
こんにちは!
前回の記事の続きです。
前回の記事はこちら↓
kojiprg.hatenablog.com
この記事では、ツイッターもどきを作成しています。
前回は、どのユーザが投稿したツイートかわかるように、ユーザとツイートのテーブルに1対多のアソシエーションを実装しました。
1対多の関係とは、以下のような関係でした。
- ユーザ一人が、複数のツイートをできる
- ツイートは、一人のユーザに属する
今回は、多対多と呼ばれるアソシエーションを使用して、いいね機能の実装をしていきます。
いいね機能の概要
まず、今回実装するいいね機能について定義します。
- 各ツイートにいいねボタンを表示する
- いいねボタンの横にそのツイートにいいねをしたユーザの数を表示する
- いいねボタンの色で、ログインしているユーザがどのツイートにいいねをしたかがわかる
- まだいいねをしていないツイートのいいねボタンを押すことで、そのツイートにいいねをすることができる
- いいね済みのツイートのいいねボタンを押すことで、そのツイートへのいいねを取り消すことができる
本家Twitterで、ハートをクリックすると、ハートがついたり消えたりする動作をイメージしています。
ただし、今回のアプリではJavaScriptによる非同期での更新機能は実装せずに、ボタンをクリックするたびに画面を更新させる仕様にします。
いいねボタンをクリックすると、色が変化するのと、右隣にある数字が増減します。
テーブルの設計
上記機能を実現するためには、ユーザがどのツイートにいいねをしたかが分かるようなデータを保存する必要があります。
ここでは、どうやってテーブルに保存するかを考えます。
まず、ユーザテーブルに保存することを考えます。
ユーザは複数のツイートにいいねできるため、どのツイートにいいねしたかを記録するためには、ユーザテーブルにツイートの数だけカラムを用意しておく必要があります。
ツイートの数が、5個や10個くらいに制限されていればこれでもいいかもしれませんが、拡張性がありません。
次に、ツイートテーブルに保存することを考えます。
この方法でも、どのユーザがいいねをしたかを記録しておくために、ユーザごとにカラムを準備する必要があるので、拡張性がありません。
ユーザやツイートの数に合わせて、カラムを増やしていくのは無理があるので、レコードを増やす方向で考えます。
新しくテーブルを作成すればレコードは簡単に増やすことができます。
そこで、いいねテーブルを作成します。
このテーブルに、いいねしたユーザと、いいねされたツイートのidを保存していくことで、実現できそうです。
今回の状況をまとめます。
- ユーザが複数のツイートにいいねができる
- ツイートが複数のユーザにいいねされる
つまり、ユーザから見た時も、ツイートから見た時も相手が複数いる状況です。
このような関係を多対多の関係とよび、新しくテーブルを作ることで、テーブル同士の関係性を保存することができます。
このテーブルを中間テーブルと呼びます。
いいねテーブルを作成した後の、テーブル構成は以下のようになります。
references型でuserと、tweetのidを保存します。
機能の実装は以下のようにすればできそうです。
- ツイートにいいねをしたユーザの数を表示する
→いいねテーブルで、調べたいツイートのtweet_idがいくつ保存されているかを調べる。
- ログインしているユーザがどのツイートにいいねをしたかがわかる
→いいねテーブルで、調べたいツイートのtweet_idで検索をかけ、その中にログイン中のuser_idを含むものがあるかどうを調べる。
アソシエーションの実装
実装はポイントに絞って解説し、rails db:migrateなんかは端折ります。(読み直したら、もっと端折ってました。)
まず、モデルへのアソシエーションの記述です。
基本的には、1対多のアソシエーションと同じで、has_manyとbelongs_toを使用します。
- favorite.rb
class Favorite < ApplicationRecord belongs_to :user belongs_to :tweet end
- user.rb
(省略) has_many :favorites, dependent: :destroy (省略)
- tweet.rb
(省略) has_many :favorites has_many :users, through: :favorites (省略)
ポイントはtweetモデルの記述です。
has_many :users, through: :favorites
throughオプションを使用することで、ツイートにいいねをしたユーザの一覧をtweet.usersのような形で取得することができます。
流れにすると以下のようになります。
tweetのidを取得
→favoritesテーブルでtweet_idと一致するいいねの一覧を取得。これで、ツイートにいいねしたユーザのidが分かる
→usersテーブルからtweetにいいねしたユーザの情報が取得できる
サンプルコード
ビューとコントローラのコードです。
- index.html.erb
(省略、以下メッセージ部分) <div class="messages"> <% @tweets.each do |tweet| %> <div class="message"> <div class="message_head"> <div class="message_user_name"> 投稿者:<%= tweet.user.user_name %> </div> <div class="message_created_at"> <%= time_ago_in_words(tweet.created_at) %> </div> <% if tweet.users.ids.include?(current_user.id) %> <%= button_to "いいね", favorite_path(tweet.id), method: :delete, style: "background-color: pink; border-radius: 50vh;" %> <% else %> <%= button_to "いいね", favorites_path, params: {tweet_id: tweet.id}, style: "background-color: white; border-radius: 50vh;" %> <% end %> <div class="favorite_count"> <%= tweet.favorites.size %> </div> </div> <div class="message_content"> <%= simple_format(tweet.message) %> </div> </div> <% end %> </div>
- favorites_controller.rb
class FavoritesController < ApplicationController def create @favorite = Favorite.new(favorite_params) @favorite.save redirect_to root_path end def destroy @favorite = Favorite.find_by(user_id: current_user.id, tweet_id: params[:id]) @favorite.destroy redirect_to root_path end private def favorite_params params.permit(:tweet_id).merge(user_id: current_user.id) end end
- tweets_controller.rb(indexアクション)
class TweetsController < ApplicationController def index @tweet = Tweet.new @tweets = Tweet.includes(:user, :users).reverse_order end (省略)
ビューのいいねボタンの実装部分は以下です。
<% if tweet.users.ids.include?(current_user.id) %> <%= button_to "いいね", favorite_path(tweet.id), method: :delete, style: "background-color: pink; border-radius: 50vh;" %> <% else %> <%= button_to "いいね", favorites_path, params: {tweet_id: tweet.id}, style: "background-color: white; border-radius: 50vh;" %> <% end %>
一行目のif文の条件文で、アソシエーションの記述を使用して、ツイートにいいねをしたユーザのidを取得しています。
ポイントは、idも複数形のidsになっている点と、
whereやfind_byを使用せずに、簡潔にかけている点です。
あとは、include?メソッドを使用して、その中にログイン中のユーザのidが含まれているかどうかを判定しています。
また、button_toのデフォルトのメソッドはpostのため、いいねを取り消すdestroyアクションを作動させる場合は、method: :deleteを指定しています。
微妙な点としては、直接favoriteのidを取得することが難しかったため、tweet.idをパスに放り込んでいます。
いいねコントローラの記述を見るとわかりますが、直接favoriteのidで検索するのではなく、find_byでuser_idとtweet_idを検索して削除しています。
@favorite = Favorite.find_by(user_id: current_user.id, tweet_id: params[:id])
続いて、いいねされた数を表示する機能の実装部分です。
<%= tweet.favorites.size %>
アソシエーションとsizeメソッドを使用することで、レコードの数を表示しています。
countでも同様のことができますが、SQL文が毎回発行されてしまいます。
sizeメソッドではすでにロード済みの場合、その結果を使用するためSQL文が発行されません。
今回は、ツイートコントローラで以下のように:usersをincludesで呼び出しています。
@tweets = Tweet.includes(:user, :users).reverse_order
この時に、throughオプションによってfavoritesも同時にロードしているため、sizeメソッドを使用することで、SQL文の発行を回避できています。
まとめ
今回は多対多のアソシエーションと中間テーブルでした。
アソシエーションを使用することで、異なるテーブルの情報を簡単に取得することができました。
後半の実装部分はややパフォーマンスも意識してみたので、わかりにくくなっていたらすみません。
最後までありがとうございました!