💛

【Rails】いいね機能

2023/06/27に公開
1

本の投稿サイトにいいね機能を追加します。

実装機能の要件

実装機能の要件は以下の通りです。

コントローラ アクション 用途
favorites create いいねを作成する
favorites destroy いいねを削除する
モデル 用途 備考
favorite ユーザーと投稿のセットでいいねをしている状態とする ユーザーは一つの投稿に一つしかいいねできないこと
ビュー 画面 要素
投稿一覧画面 いいね数表示
いいね(する, 外す)ボタン
投稿詳細画面 いいね数表示
いいね(する, 外す)ボタン
いいね作成ボタン いいねされていない投稿に対して表示
いいね削除ボタン いいねされている投稿に対して表示

いいね機能のテーブル設計

以下のような、テーブル設計にします。

カラム名 データ型 カラムの説明
id integer 「いいね」ごとのID
user_id integer 「いいね」したユーザのID
book_id integer 「いいね」された投稿のID

Model作成

ターミナルでの入力方法は以下の通りです。

  1. Favoriteモデルの生成
ターミナル
rails g model Favorite user_id:integer book_id:integer

上記のコマンドを実行すると、db/migrateディレクトリにマイグレーションファイルが生成されます。

  1. データベースへのマイグレーション実行
ターミナル
rails db:migrate

上記のコマンドを実行することで、マイグレーションファイルに基づいてデータベースにテーブルが作成されます。

以上の手順により、Favoriteモデルが作成され、user_idbook_idのカラムが含まれるテーブルがデータベースに作成されます。

関連付けの設定

Favoriteモデルと他のモデルの関連付けを設定します。

モデルの関係性

図表で表すと以下の通りです。

  • User(ユーザー)とFavorite(いいね)の関係は、1対多の関係です。1人のユーザーが複数のいいねを持つことができます。
  • Book(投稿)とFavorite(いいね)の関係も、1対多の関係です。1つの投稿が複数のいいねを持つことができます。

つまり、ユーザーといいねの関係は「1:N」であり、投稿といいねの関係も「1:N」です。

関連付けの実装

以下のようにそれぞれのモデルに関連付けを記述します。

  • Favoriteモデル
app/models/favorite.rb
class Favorite < ApplicationRecord
  belongs_to :user
  belongs_to :book
end

上記のコードでは、FavoriteモデルがUserモデルとBookモデルに対してそれぞれ1対多の関連付けを持つことを表しています。

  • Userモデル
app/models/user.rb
class User < ApplicationRecord
+ has_many :favorites, dependent: :destroy
end

上記のコードでは、Userモデルにhas_many :favoritesの関連付けを定義しています。これによりUserモデルのインスタンスは複数のFavoriteモデルのインスタンスと関連付けられます。
dependen: :destroyオプションでUserが削除された場合に関連するFavoriteも同時に削除できます。

  • Bookモデル
app/models/book.rb
class Book < ApplicationRecord
+ has_many :favorites, dependent: :destroy
  :
+ def favorited_by?(user)
+   favorites.exists?(user_id: user.id)
+ end
end

上記のコードでは、Bookモデルにも同様にhas_many :favoritesの関連付けを定義しています。これにより、Bookモデルのインスタンスは複数のFavoriteモデルのインスタンスと関連付けられます。また、dependent: :destroyオプションによって、Bookが削除された場合に関連するFavoriteも同時に削除されるように設定されています。

このような関連付けの設定により、UserモデルとBookモデルはそれぞれ複数のFavoriteモデルと関連付けられ、1対多の関係が成立します。つまり、1つのユーザは複数のお気に入りを持つことができ、また1つの投稿は複数のお気に入りを受けることができるという関係性が構築されます。

  • def favorited_by?(user)メソッド
    このコードは、Bookモデルにfavorited_by?(user)メソッドを追加しています。このメソッドは、指定されたユーザが特定の投稿(Bookインスタンス)をいいねしているかどうかを判定します。

以下に、メソッドの内容を解説します。

  1. def favorited_by?(user):
  • メソッドの定義です。favorited_by?はメソッド名であり、userは引数として渡されるユーザオブジェクトです。
  1. favorites.exists?(user_id: user.id):
  • favoritesは、BookモデルとFavoriteモデルの関連性によって定義されたアソシエーションです。has_many :favoritesによって、Bookは複数のFavoriteを持つ関連性が設定されています。
  • exists?メソッドは、与えられた条件に合致するレコードが存在するかどうかをチェックします。
  • user_id: user.idという条件は、favoritesテーブルのuser_idカラムがuser.id(引数で渡されたユーザのID)と一致するかどうかを検証します。
  • favoritesテーブルに、user_iduser.idと一致するレコードが存在する場合は、exists?メソッドはtrueを返します。それ以外の場合はfalseを返します。

このメソッドを使うと、ある投稿(Bookインスタンス)が特定のユーザによっていいねされているかどうかを簡潔に判定できます。もし指定されたユーザがいいねしている場合はtrueを、していない場合はfalseを返します。

Routing

以下の通りルーティングの設定をします。

config/routes.rb
Rails.application.routes.draw do
:
+ resources :books, only: [:index, :show, :edit, :create, :destroy, :update] do
+   resource :favorites, only: [:create, :destroy]
+ end
  resources :users, only: [:index, :show, :edit, :update]
end

resources vs resource

  • resources :booksresources :usersなどのように複数形のリソース名を指定すると、URLには:idが含まれます。これは、それぞれのリソースの特定の要素(レコード)を操作するために識別子(ID)が必要な場合に使用します。

  • 一方、resource :favoritesのように単数形のリソース名を指定すると、URLには:idが含まれません。これは、そのリソースが他のリソースにネスト(関連付け)されており、ネストされたリソースによって特定できる場合に使用します。

  • ネスト(関連付け)は、異なるリソース間の親子関係を表現するための仕組みです。例えば、ユーザーが投稿に対して1つのコメントしかできない場合、コメントはユーザーと投稿に関連付けられます。この関連付けにより、どのユーザーがどの投稿にコメントしたのかを特定することができます。

  • いいね機能の場合、1人のユーザーは1つの投稿に対して1回しかいいねできないという制約があります。そのため、いいねの削除を行う際には、ユーザーIDと投稿IDがわかれば、どのいいねを削除するかを特定できます。したがって、いいねのIDをURLに含める必要はありません。

  • resourcesresourceの違いは、URLに:idを含めるかどうかです。resourcesを使用すると:idが含まれ、特定の要素を操作するためのIDが必要です。一方、resourceを使用すると:idが含まれず、他のリソースとの関連付けによって特定できます。

  • resourceは、自身のIDが分からなくても他のリソースのIDから特定できる場合に使用されます。例えば、いいねの削除の場合、ユーザーIDと投稿IDを指定することで特定できるため、いいねのIDをURLに含める必要がありません。

Controller作成

まず、ターミナルで以下のコマンドを実行してコントローラを作成します。

ターミナル
rails g controller favorites

次に、favorites_controller.rbファイルに以下のコードを記述します。

app/controllers/favorites_controller.rb
class FavoritesController < ApplicationController
  def create
    book = Book.find(params[:book_id])
    favorite = current_user.favorites.new(book_id: book.id)
    favorite.save
    redirect_to request.referer
  end
  
  def destroy
    book = Book.find(params[:book_id])
    favorite = current_user.favorites.find_by(book_id: book.id)
    favorite.destroy
    redirect_to request.referer
  end
end

createメソッドはいいねを作成し、destroyメソッドはいいねを削除します。
コードについて詳細を説明します。

  1. favorite = current_user.favorites.new(book_id: book.id):

    • current_userは現在ログインしているユーザーを表します。
    • current_user.favoritesは、current_userに関連付けられたFavoriteモデルのコレクション(複数のいいね)を表します。
    • favorites.newは、Favoriteモデルの新しいインスタンスを作成します。このインスタンスは、まだデータベースに保存されていない状態です。
    • book_id: book.idは、新しいFavoriteインスタンスのbook_id属性に、指定されたbook.idをセットしています。これにより、作成されるいいねが特定のBookオブジェクトに関連付けられます。
    • favoriteには、新しく作成されたFavoriteインスタンスが代入されます。
  2. favorite = current_user.favorites.find_by(book_id: book.id):

    • favorites.find_by(book_id: book.id)は、book_id属性が指定されたbook.idと一致するFavoriteインスタンスを検索します。このメソッドは、最初に見つかったインスタンスを返します。
    • favoriteには、見つかったFavoriteインスタンスが代入されます。もし一致するインスタンスが見つからなかった場合は、nilが代入されます。
  3. redirect_to request.referer:

    • request.refererは、リクエストを送信した元のページのURLを表します。つまり、ユーザーがいいねの作成または削除を行った元のページに戻ることができます。

Viewページ

  • 今回は部分テンプレートファイルにするので、ファイルを作成し記述します。
app/views/favorites/_btn.html.erb
<% if book.favorited_by?(current_user) %>
  <%= link_to book_favorites_path(book), method: :delete do %>
    <i class="fas fa-heart" aria-hidden="true" style="color: red;"></i>
    <%= book.favorites.count %> いいね
  <% end %>
<% else %>
  <%= link_to book_favorites_path(book), method: :post do %>
    <i class="fas fa-heart" aria-hidden="true"></i>
    <%= book.favorites.count %> いいね
  <% end %>
<% end %>
コードの解説

このコードは、条件分岐に基づいて表示内容を切り替えています。

  1. <% if book.favorited_by?(current_user) %>:

    • book.favorited_by?(current_user)は、bookオブジェクトが現在のユーザー (current_user) によっていいねされているかどうかを確認するメソッドです。
    • もし現在のユーザーによっていいねされている場合は、if文の中のブロックが実行されます。
  2. <%= link_to book_favorites_path(book), method: :delete do %>:

    • link_toは、指定されたURLへのリンクを生成するためのRailsのヘルパーメソッドです。
    • book_favorites_path(book)は、bookオブジェクトに対していいねを作成・削除するためのURLを生成します。
    • method: :deleteは、リンクをクリックした際にHTTPのDELETEメソッドが使用されるように指定しています。つまり、いいねを削除するアクションに対応します。
  3. <i class="fas fa-heart" aria-hidden="true" style="color: red;"></i>:

    • <i>タグは、Font Awesomeアイコンを表示するためのHTML要素です。
    • fas fa-heartは、Font Awesomeのハートアイコンを指定しています。
    • style="color: red;"は、アイコンの色を赤に指定しています。この部分はいいねされている場合に適用されます。
  4. <%= book.favorites.count %> いいね:

    • book.favorites.countは、bookオブジェクトに紐づくいいねの数を取得します。
  5. <% else %>:

    • if文の条件に合致しない場合に実行されるブロックです。
  6. <%= link_to book_favorites_path(book), method: :post do %>:

    • book_favorites_path(book)は、bookオブジェクトに対していいねを作成するためのURLを生成します。
    • method: :postは、リンクをクリックした際にHTTPのPOSTメソッドが使用されるように指定しています。つまり、いいねを作成するアクションに対応します。
  7. <i class="fas fa-heart" aria-hidden="true"></i>:

    • こちらも先ほどと同様に、ハートアイコンを表示するためのHTML要素です。
  8. <%= book.favorites.count %> いいね:

    • 先ほどと同様に、いいねの数を表示します。
  • 部分テンプレートファイルを①投稿一覧画面と②投稿詳細画面から呼び出します。
投稿一覧画面
app/views/books/_index.html.erb
 <table class="table table-hover table-inverse">
   <thread>
     <tr>
       <th></th>
       <th>Title</th>
       <th>Opinion</th>
       <th colspan="3"></th>
     </tr>
   </thread>
   <tbody>
     <% books.each do |book| %>
     <tr>
       <td><%= link_to book.user do %>
           <%= image_tag book.user.get_profile_image, size:'50x50' %>
           <% end %>
       </td>
       <td><%= link_to book.title,book %></td>
       <td><%= book.body %></td>
+      <td><%= render "favorites/btn", book: book %></td>
     </tr>
   <% end %>
   </tbody>
 </table>
投稿詳細画面
app/views/books/show.html.erb
 <div class="container">
   <div class="row">
     <div class="col-md-3">
       <h2>User info</h2>
       <%= render "users/info", user: @user %>
       <h2 class="mt-3">New book</h2>
       <%= render "form", book: @books %>
     </div>
     <div class='col-md-8 offset-md-1'>
       <h2>Book detail</h2>
       <table class="table">
         <tr>
           <td><%= link_to @book.user do %>
               <%= image_tag @book.user.get_profile_image, size:"100x100" %><br>
               <%= @book.user.name %>
               <% end %>
           </td>
           <td><%= link_to @book.title, @book %></td>
           <td><%= @book.body %></td>
+          <td><%= render "favorites/btn", book: @book %></td>
          
           <% if @book.user == current_user %>
           <td><%= link_to "Edit", edit_book_path(@book), class: "btn btn-sm btn-success" %></td>
           <td><%= link_to "Destroy", @book, method: :delete, data: { confirm: "本当に消しますか?" }, class: "btn btn-sm btn-danger" %></td>
           <% end %>
         </tr>
       </table>
     </div>
   </div>
 </div>

以上、いいね機能の実装でした。

息子が今とても流行っているヘルパンギーナというウイルスに感染していることがわかり、40度の高熱がでてしまいました💦
これが2~3日続くみたい、、、小さな身体で一生懸命治そうとがんばっている姿を見ると涙がでます。
絶対元気になるから大丈夫だよ。
今週は妻と協力しながら息子の看病をします。

Discussion

ちいかぶちいかぶ

もし宜しければ、railsをAPIモードで、フロントエンドをVueかReactにしたバージョンの解説をしていただけると幸いです。