Railsで簡単なLike機能(いいね)を実装する

こんにちは、わったんです。

Twitterで定番のLike機能の実装を勉強しましたので、ここで整理しておきます。基本は下のブックマーク機能実装の記事を参考にしてます。

①モデルの作成・編集

Like機能は「一人のuser→複数のpostにLikeする」、「一つのpost→複数のuserからLikeされる」特徴を持つのでuserとpostが「多対多」の関係になります。このような場合、中間テーブルを作成します。中間テーブル作成のメリットなどはこちら

$ rails g model Like user:references post:references

中間テーブルはuserテーブルもpostテーブルも参照するので、外部キー制約を両方のカラムにつけます。そしてマイグレーションファイルの編集。

202202211214222_create_likes.html.erb

class CreateLikes < ActiveRecord::Migration[5.2]
 def change
   create_table :likes do |t|
     t.references :user, null: false, foreign_key: true
     t.references :post, null: false, foreign_key: true
     #null: falseを付与
     
     t.timestamps
   end
   add_index :likes, [:user_id, :post_id], unique: true
   #user_idとpost_idの組み合わせが一意になるように(複合キー)設定
 end
end
rails db:migrate

これでマイグレーションが完了しました。次はLikeモデルの編集です。

Like.rb


class Like < ApplicationRecord
 belongs_to :user
 belongs_to :post

 validates :user_id, uniqueness: { scope: :post_id} 
end

ユニーク制約にscopeをつけることで一つのpostに同じuserが重複してlikeできないようにします。もしscopeなしだと、userがあるpostにlikeしたら、そのuserは他のpostにはlikeできなくなる機能になってしまいます。

 Post.rb
 
 
 belongs_to :user
 has_many :likes, dependent: :destroy
 has_many :users, through: :likes

 validates :title, presence: true, length: { maximum: 255 }
 validates :content, presence: true, length: { maximum: 65_535 }
end

has_many :likesにはthroughオプションでlikesを指定することで、中間テーブルを経由するようになります。

User.rb


class User < ApplicationRecord
 authenticates_with_sorcery!

 has_many :posts, dependent: :destroy
 has_many :likes, dependent: :destroy
 has_many :likes_posts, through: :likes, source: :post

 validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
 validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
 validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

 validates :email, uniqueness: true
 validates :email, presence: true
 validates :first_name, presence: true, length: { maximum: 255 }
 validates :last_name, presence: true, length: { maximum: 255 }

has_many :likes_posts, through: :likes, source: :postで中間テーブルを経由してpostモデルを参照する架空のモデルを設定することができます。参考にしたぺージはこちら。少しrails consoleで確認してみます。

pry(main)> puts user3.likes_posts
 Post Load (0.1ms)  SELECT "posts".* FROM "posts" INNER JOIN "likes" ON "posts"."id" = "likes"."post_id" WHERE "likes"."user_id" = ?  [["user_id", 4]]
#<Post:0x00007feea4d6e1e8>

user3にはidが4のオブジェクトが入っています(少しややこしくて、ごめんなさい)。userオブジェクト.likes_postsを実行してみると、likesテーブルからuser3のidが登録されているレコードを選び出し、次にpostテーブルのidとlikesテーブルのpost_idが等しいpostレコードを取り出していることがわかります。through, sourceを設定することでこのようなSQL文を作ることができるのですね。

またuser.rbに以下も追加します。

user.rb


#userオブジェクトのidとpostやlikeオブジェクトのuser_idが同じかどうかを判断
  def mine?(object)
   object.user_id == id
 end
 
#likes_postsテーブルにpostオブジェクトを追加する。
#いいねを押したときに、いいねしたユーザーといいねされた投稿の情報が保存される。
 def like(post)
   likes_posts << post
 end

#likes_postsテーブルから引数のpostオブジェクトに該当するレコードを削除する。
 def unlike(post)
   likes_posts.destroy(post)
 end

#likes_postsテーブルに引数のpostオブジェクトに該当するレコードがあるかを判断する。
 def like?(post)
   likes_posts.include?(post)
 end

 def full_name
   "#{last_name} #{first_name}"
 end
end

likes_posts << postのところでcreate!メソッドを使わないのは、create!メソッドを使ってしまうとlikesテーブルにレコードが追加されずにlikeが生成されてしまうからみたいです(詳しくはよく分かりません、、)。

これでモデルの編集は終わりました。

②コントローラーの作成

$ rails g controller Likes create destroy

createメソッドとdestroyメソッドだけ追加します。この時、ビューでcreate.html.erbとdestroy.html.erbも生成されますが不要なので削除しておきます。

次はlikes.controller.rbの編集です。

likes.controller.rb


class LikesController < ApplicationController
 def create
   post = Post.find(params[:post_id])
   current_user.like(post)
   redirect_back fallback_location: posts_path
 end

 def destroy
   post = current_user.likes_posts.find(params[:post_id])
   current_user.unlike(post)
   redirect_back fallback_location: posts_path
 end
end

createメソッドとdestroyメソッドでは、それぞれモデルで設定したlikeメソッドとunlikeメソッドを使用しています。redirect_backは直前のページにリダイレクトするメソッドです。fallback_locationオプションを設定することで、直前のページが見つからない場合のリダイレクト先を指定できます。

③ルーティングの設定

routes.rb

Rails.application.routes.draw do
 get 'login', to: 'user_sessions#new'
 post 'login', to: 'user_sessions#create'
 delete 'logout', to: 'user_sessions#destroy'
 resources :posts do
   resources :likes, only: %i[create destroy]
   collection do
     get :likes
   end
 end

 resources :users, only: %i[new create]
end

collectionを使うことで/posts/likesのURLを生成することができます。

posts_controller.rb


  def likes
   @posts = current_user.likes_posts
 end

posts#likesのルーティングを設定したので、likesメソッドを設定します。@postsにcurrent_userがlikeした投稿のオブジェクトを入れるようにします。これでlikeしたpostの一覧を表示するためのアクションを設定できました。

④Viewの編集・作成

最後はviewの編集と作成です。まずはそれぞれのpostにlikeボタンを表示させる設定をしていきます。

/posts/index.html.erb

<省略>

<table>
 <thead>
   <tr>
     <th>Title</th>
     <th>Content</th>
     <th colspan="3"></th>
   </tr>
 </thead>

 <tbody>
   <% @posts.each do |post| %>
     <tr>
       <td><%= post.title %></td>
       <td><%= post.content %></td>
       <td><%= link_to 'Show', post %></td>

       <% if current_user.mine?(post) %>
         <td><%= link_to 'Edit', edit_post_path(post) %></td>
         <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
       <% else %>
        #ボタンのパーシャルを呼び出す
         <%= render 'like_button', {post: post} %>
       <% end %>
     </tr>
   <% end %>
 </tbody>
</table>

Likeボタンはパーシャルに記載します。パーシャルにはpostにpostオブジェクトを入れて渡すように設定します。

posts/_like_buttun.html.erb


#userモデルの設定したlike?メソッドを使って、postオブジェクトに対してcurrent_userがlikeしているかを確認。
<% if current_user.like?(post) %>
 <%= render 'unlike', { post: post } %>
<% else %>
 <%= render 'like', { post: post } %>
<% end %>

まずcurrent_user.like?(post)で引数に渡されたpostオブジェクトにcurrent_userがlikeしているかどうかを確認します。likeしていればunlikeパーシャルを呼び出し、likeしてなければlikeパーシャルを呼び出します。

/posts/_like.html.erb


<td><%= link_to 'Like', post_likes_path(post_id: post.id), method: :post %></td>
/posts/_unlike.html.erb


<td><%= link_to 'UnLike', post_like_path(post_id: post.id, id: current_user.likes.find_by(post_id: post.id)), method: :delete %></td>

likeとunlikeのパーシャルを作成します。ここは、もっとシンプルに書けるのかもしれません。

/posts/likes.html.erb


<h1>Liked Posts</h1>

<table>
   <thead>
       <tr>
           <th> Auther</th>>
           <th>Title</th>
           <th>Content</th>
       <th colspan="3"></th>
   </tr>
   </thead>

   <tbody>
       <% @posts.each do |post| %>
       <tr>
           <td><%= post.user.full_name %></td>
           <td><%= post.title %></td>
           <td><%= post.content %></td>
           <td><%= link_to 'Show', post %></td>
       </tr>
       <% end %>
   </tbody>
</table>
<%= link_to 'Back', posts_path %>​

最後にlike一覧のビューを作成します。full_nameはuserの性と名を連結させたメソッドです。userモデルに定義してあるメソッドになります。

 user.rb
 
 
def full_name
  "#{last_name} #{first_name}"
end


これでlike機能が使えるようになりました。まだまだ冗長なコードになってる部分もあると思うので課題はありそうです。最後まで読んでくださりありがとうございました〜。少しでも参考になれば幸いです!!

いいなと思ったら応援しよう!