9
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Railsで「いいね!」機能を作る - ②「いいね!」のcreateアクション

Last updated at Posted at 2020-08-31

背景

Twitterのようなアプリを作っていて、投稿に「いいね!」ボタンを付けようと思います。
作ろうとしているもののデータベース構造はこんな感じです。

▼ER図
Image from Gyazo

作ろうとしている画面の参考画像はこちらです
▼画像イメージ
Image from Gyazo

この「いいね!」ボタンを押すと、ユーザーが投稿に「いいね!」でき、もう一度ボタンを押すと「いいね!」が解除できるようにしたいです。

モデル間のアソシエーションの設定は、過去にこの記事で行ってきました。

▼アソシエーションの設定
Railsで「いいね!」機能を作る - ①アソシエーションに別名をつける

今回は、controllerの記述を中心に紹介し、ボタンを押すと「いいね!」ができるところまでを目標にしてみたいと思います。なお、「いいね!」の解除機能は、この次の記事で実装します。

▼この記事の続編はこちら
Railsで「いいね!」機能を作る - ③「いいね!」を解除できるようにする

ゴール

ゴールは至ってシンプルで、userspostsの中間テーブルであるlikesに適切なデータが入れば良いだけです。

er.png
入れるデータはログインしているユーザーのidと、「いいね!」ボタンを押した投稿のidです。

ただし、**ユーザーがすでに「いいね!」を押した投稿に対し、重複して「いいね!」を押すことはできません。**←この条件が結構難しくてハマりどころではないのかと思います。

とりあえず動かす① - 中間テーブルにデータを保存できるようにする

controller

とりあえず、中間テーブルにデータを保存するところからスタートしてみます。likesテーブルにシンプルにuser_idpost_idを保存するようにしてみました。

controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    like = current_user.likes.new(post_id: clicked_post.id)
    if like.save
      flash[:success] = '投稿に「いいね!」しました。'
      redirect_back(fallback_location: root_path)
    else
      flash[:alert] = '「いいね!」に失敗しました。'
      redirect_back(fallback_location: root_path)
    end
  end

  private
  
  def clicked_post
    Post.find(params[:post_id])
  end
end

まず、privateメソッドでclicked_post(ユーザーがクリックした投稿)を定義します。

そして、current_user.likes.new(post_id: clicked_post.id)で、ボタンを押した投稿の「いいね!」をイニシャライズし、その次の行で保存しています。

redirect_back(fallback_location: root_path)は「元のページに戻る」コードです。詳細はこちらの記事をご覧いただくのがわかり易いと思います。

view

ビューは、下図のように投稿のパーシャルの中に「いいね!」ボタンをつける想定です。私は以下のように書きました。

views/posts/_post.rb
<!-- postのインスタンスは、パーシャルに渡されている前提です。 -->
    <div>
      <!-- 投稿者名・投稿日時・投稿内容 -->
      <div>
        <%= post.user.name %> <%= post.created_at %>
      </div>
      <div>
        <%= post.content %>
      </div>

      <!-- いいね!ボタン -->
      <div>
        <%= form_with(model: @like, url: likes_path, local: true) do |f| %>
          <%= f.hidden_field :post_id, value: post.id %>
          <%= f.submit 'いいね!' %>
        <% end %>   
      </div>
    </div>

上記のコードでは、form_with以下からLikeモデルに向かって、hidden_fieldpost_idを送信しています。

これで、likesテーブルに欲しいデータは入ります。ただ、入るには入るのですが…。Railsコンソールで確かめると下記のようになりました。

$ rails c
> user = User.first
> user.favorites
=> #<ActiveRecord::Associations::CollectionProxy 
[#<Post id: 2, content: "テスト投稿です。", user_id: 2, created_at: "2020-08-27 12:36:45", updated_at: "2020-08-27 12:36:45">, 
#<Post id: 2, content: "テスト投稿です。", user_id: 2, created_at: "2020-08-27 12:36:45", updated_at: "2020-08-27 12:36:45">]> 

...同じユーザーが同じ投稿に対して持っている「いいね!」が2つになっています。。。これではいけませんね。。。

とりあえず動かす② - 同じ投稿に「いいね!」が2回できないようにする

そこで、ユーザーが「いいね!」をした投稿(favorites)の中に、たった今、画面上で「いいね!」をされた投稿(clicked_post)がないか探し、なければlikesテーブルにデータを保存するようにしたいと思います。

controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    unless current_user.favorites.include?(clicked_post)
      like = current_user.like.new(post_id: clicked_post.id)

      if like.save
        flash[:success] = '投稿に「いいね!」しました。'
        redirect_back(fallback_location: root_path)
      end

    else
      flash[:alert] = 'すでに「いいね!」しています。'
      redirect_back(fallback_location: root_path)
    end
  end
  
  private
  
  def clicked_post
    Post.find(params[:post_id])
  end
end

unless current_user.favorites.include?(clicked_post)の部分で、ユーザーのクリックした投稿が、ユーザーがこれまでに「いいね!」した投稿(favorites)に含まれていないことを確認し、

その場合に、「いいね!」を保存できるようにcontrollerを書き換えました。favoritesの定義は、前回のこちらの記事を見てください。

これで、同じ投稿に2回「いいね!」できないようにするという条件はクリアしました。

...が、コードが汚いです:sweat_smile:
コントローラーに色々なものを書きすぎています。
コントローラーはできるだけ薄くするのがRailsによるアプリ作りの原則ですので、リファクタリング(コードを簡素化し、きれいにする)をしたいと思います。

リファクタリング

結論から言うと、リファクタリングの結果作成したのは、以下のコードです。

controllers/likes_controller.rb
class LikesController < ApplicationController

  def create
    if current_user.like_this(clicked_post)
      flash[:success] = '投稿に「いいね!」しました。'
      redirect_back(fallback_location: root_path)
    else
      flash[:alert] = 'すでに「いいね!」しています'
      redirect_back(fallback_location: root_path)
      # 「いいね!」削除ボタンを実装すると、↑上記の2行は不要になります
    end
  end
  
  private
  
  def clicked_post
    Post.find(params[:post_id])
  end
end
models/user.rb
class User < ApplicationRecord
  has_many :likes
  has_many :favorites, through: :likes, source: :post

  def like_this(clicked_post)
    self.likes.find_or_create_by(post: clicked_post)
  end
end

まず、like_thisメソッドを定義します。ユーザーであるselflikes内に、clicked_postがあるかどうかを探して、なければ作成します。

find_or_create_byの仕様はこちらの記事がわかりやすかったです。

このメソッドで、コントローラーにゴニョゴニョあったif文は全て消えてしまいました。わー。。。。:sweat_smile:リファクタリング、大事です。

あとは、地味ですがこちらの部分もリファクタリングしました。

# これを
current_user.like.new(post_id: clicked_post.id)

# こうした
current_user.like.new(post: clicked_post)

*_id 同士を比べて、、、、という書き方は、基本的に全てインスタンス同士を比較する書き方に書き換えられそうです。

ここまでやったら、かなりRailsに詳しくなれそうです。。。。

次は、一度「いいね!」を押したボタンをクリックすると、「いいね!」を取りやめる、「いいね!」の削除機能について調べていきます。

▼こちらの記事です
Railsで「いいね!」機能を作る - ③「いいね!」を解除できるようにする

補足:マイグレーションを作るときに安全性をあげる

なお、順番が前後してしまいましたが、「同じ投稿に2回『いいね!』することを避ける」ために、DB側に以下のような設定もできます。

class CreateLikes < ActiveRecord::Migration[5.2]
  def change
    create_table :likes do |t|
      t.references :user, foreign_key: true
      t.references :post, foreign_key: true

      t.timestamps

      t.index [:user_id, :post_id], unique: true
    end
  end
end

t.index [:user_id, :post_id], unique: trueの部分で、重複したuser_idpost_idのペアが登録されようとしたら、例外を発生させることができます。

同じ投稿に「いいね!」ができないようにする安全性を高めるためにも、やってみてもいいと思います^^

### 追記
2020.09.29 一部のコードをリファクタリングしました。

9
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?