LoginSignup
38
35

More than 5 years have passed since last update.

【Rails】いいねボタンを作ろう part2/2

Posted at

この記事について

こちらもどうぞ

前回までのあらすじ

  • UserとPostあたりを実装した

今回の目標

  • いよいよ「いいねボタン」をつくる

改めて設計の確認

  • [これが本丸]ログインユーザーは各ブログ記事に1回だけいいね! することができる
    • いいね! は1ユーザー1ブログ記事につき1回の制限
    • 自分のブログ記事に対してもいいね! することはできる
  • 既に、いいね! をしているブログ記事に対して、いいね! を解除することができる
    • ボタンは1つで実装し、いいね! の状態によって切り替える
  • ブログ記事詳細画面で、いいね! 件数および、どのユーザーがいいね! しているかが表示される

Likeモデルをつくる

  • post_id と user_id をもつだけ
    • どの記事がいいねしたかという情報
$ rails g model Like post:references user:references

MigrationスクリプトにはNull制約だけ追加

db/migrate/yyyymmddhhiiss_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.0]
  def change
    create_table :likes do |t|
      t.references :post, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end
$ rails db:migrate

seedファイル追記

  • アソシエーションやデータ取得方法が重要なので、データはseedで作る
  • ユーザーは3人
  • 各ユーザーが各記事にいいね!している状態
  • idを振り直したいので、rails db:reset
db/seeds.rb
3.times do |i|
  i += 1
  user = User.create(
    email: "user#{i}@example.com",
    password: 'password'
  )

  3.times do |j|
    j += 1
    Post.create(
      title: "#{user.email}の記事 その#{j}",
      body: "body#{j} by #{user.email}",
      user_id: user.id
    )

    Like.create(post_id: i, user_id: j)
  end
end
$ rails db:reset

Like絡みのアソシエーション

  • Likeモデル登場により生まれるモデル間の関係性を考える

参考図(手書き)

  • 中間テーブルを含むイメージ画像を書きました
  • PostはLikeを介してUserにアクセスできる(メソッド名はliked_users)
  • UserはLikeを介してPostにアクセスできる(メソッド名はliked_posts)

ink-image.png

Postモデル

  • 各モデルとの関係性は2つ
    1. 1件のpostは複数のlikesを持つ(色々なユーザーが、1件のブログ記事にいいね! をする)
    2. [こっちがポイント]1件のpostが持つ複数のlikesを介して、複数のusersとつながっている(この記事にいいねした全てのユーザー)
app/models/post.rb
class Post < ApplicationRecord
  # ...

  # 1の関係
  has_many :likes

  # 2の関係
  has_many :liked_users, through: :likes, source: :user

end

source: :userについて

  • sourcehas_many ... throughを使うときにどのモデルと関連させるかを表現するときに使う
  • 例によって、Railsのパターンどおりであれば、この記述は不要
  • しかし、今回はpostからlikesを介してuserにアクセスするときのメソッドをliked_usersにしているのでこの記述は必要
sourceのメリット
  • メリット1: 自由なメソッド名を付けられるのでメソッド名から取得内容を判断しやすい
    • ブログ記事(post)目線で、いいね(like)を経由したユーザー(user)との関係性を考えると、これらのユーザーはいいね!をした人(liked_users)にあたるのでliked_usersを採用した(別に好きなようにすればよい)
  • メリット2: 例えば、参考リンク先のような状況でも対応できる(ユーザー情報はUserモデルで管理しているが、user同士に商品の買い手と売り手といった関係性が存在する場合など)

Userモデル

  • 考え方はPostのときと同様
  • Like絡みの関係性は2つ
    1. [簡単]1人のuserは複数のlikesを持つ(1人が複数の投票できる)
      • dependent: :destroyはつけておく
    2. [重要]1人のuserがいいねした複数のlikesから、postsを辿れる(自分がいいねをした全ての記事)
      • その際のメソッドをliked_postsにしている
app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :likes, dependent: :destroy
  has_many :liked_posts, through: :likes, source: :post

  # ...
end

コンソールで試す

  • アソシエーションを設定したのでコンソールで確かめる
  • 少々見づらいのでppを利用し、なおかつ余計な出力は省略している

Post

$ rails c
Running via Spring preloader in process 46772
Loading development environment (Rails 5.0.1)
irb > require 'pp'
irb > p1 = Post.first
irb > pp p1.likes
[#<Like:0x007fe688b25e78
  id: 1,
  post_id: 1,
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Like:0x007fe68a085408
  id: 2,
  post_id: 1,
  user_id: 2,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Like:0x007fe68a0849b8
  id: 3,
  post_id: 1,
  user_id: 3,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>]
irb > pp p1.liked_users
[#<User id: 1, email: "user1@example.com", created_at: "2017-01-26 07:43:16", updated_at: "2017-01-26 07:43:16">,
 #<User id: 2, email: "user2@example.com", created_at: "2017-01-26 07:43:16", updated_at: "2017-01-26 07:43:16">,
 #<User id: 3, email: "user3@example.com", created_at: "2017-01-26 07:43:16", updated_at: "2017-01-26 07:43:16">]

User

irb > u1 = User.first
irb > pp u1.likes
[#<Like:0x007fe68c2aae20
  id: 1,
  post_id: 1,
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Like:0x007fe68c2aa880
  id: 4,
  post_id: 2,
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Like:0x007fe68c2aa5b0
  id: 7,
  post_id: 3,
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>]
irb > pp u1.liked_posts
[#<Post:0x007fe68c281ae8
  id: 1,
  title: "user1@example.comの記事 その1",
  body: "body1 by user1@example.com",
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Post:0x007fe68c2819a8
  id: 2,
  title: "user1@example.comの記事 その2",
  body: "body2 by user1@example.com",
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Post:0x007fe68c281868
  id: 3,
  title: "user1@example.comの記事 その3",
  body: "body3 by user1@example.com",
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>]

1人1記事1いいねValidation

  • 次に、いいね回数に制限をかける
  • 具体的には「1人1記事1いいね」
    • 今回は自分の投稿にもいいねをつけられることにする(Twitterど同じ)
  • 色々な方法がありそうだが、モデルのバリデーションだけ紹介
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post

  validates_uniqueness_of :post_id, scope: :user_id
end

いいねボタン

  • いよいよ「いいね!」ボタンをつくる

ログイン/ログアウトをやりやすくするためのheader作成

  • いいねはログインが必要であり、色々なユーザーで試すにはログアウト作業もしなくてはならない
  • その作業をスムーズにするために、共通レイアウトにheaderを追加する
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>LikeBtn</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <header>
      <% if current_user %>
        <%= current_user.email %>さんとしてログインしています
        <%= link_to 'ログアウトする', destroy_user_session_path, method: :delete %>
      <% else %>
        <%= link_to 'ログインはこちら', user_session_path %>
      <% end %>
    </header>
    <%= yield %>
  </body>
</html>

Likesコントローラ生成

  • 素のコントローラ生成
$ rails g controller Likes

いいね周りのルーティング

  • いいねをする → likes#create
  • いいねを取り消す → likes#destroy
    • 論理削除ではなく、そのままデータを削除する
config/routes.rb
Rails.application.routes.draw do

  # ...

  resources :posts, only: [:index, :show] do
    resources :likes, only: [:create, :destroy]
  end

  # ...
end

Likes#create

  • @postを記述しておかないとバリデーションに引っかかったときに@post(元々はposts#showで宣言していた)の情報を失う
  • post_idparams[:post_id]で取れることを意識する
  • Deviseの効果でログインユーザーの情報はcurrent_userで取れる
app/controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    @post = Post.find(params[:post_id])

    @like = Like.new(
      post_id: params[:post_id],
      user_id: current_user.id
    )

    if @like.save
      redirect_to post_path(@post)
    else
      render template: 'posts/show'
    end
  end

  private
    def like_params
      params.require(:like).permit(:post_id, :user_id)
    end
end

いいねボタン

  • posts#show で 空のオブジェクト(@like)を生成
  • form_forの第1引数がポイント
    • [親モデル, 子モデル]という記述になる
    • この場合、親:Post 子:Like
app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    # ...
  end

  def show
    @post = Post.find(params[:id])
    @like = Like.new() # 追記
  end
end
app/view/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>

<h3>本文</h3>
<p><%= @post.body %></p>

<%= form_for [@post, @like] do |f| %>
  <%= f.submit 'いいね!' %>
<% end %>

実際に試してみる

  • まずはLikeモデルのデータをすべて消しておく(バリデーションなどはあとで)
$ rails c
irb > Like.destroy_all
  • ログインした状態で/posts/1にアクセスして「いいね」を押すと登録されるはず(DBを見て欲しい)
  • ただし、2件以上は登録されない(Likeモデルのvalidates_uniqueness_ofの働き)

それぞれの記事にいいね!しているユーザー一覧の表示

  • has_many ... throughというアソシエーションを設定しているの簡単に取り出せる
app/view/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>

<h3>本文</h3>
<p><%= @post.body %></p>

<%= form_for [@post, @like] do |f| %>
  <%= f.submit 'いいね!' %>
<% end %>

<h2>この記事にいいねしたユーザー</h2>
<% @post.liked_users.each do |user| %>
  <li><%= user.email %></li>
<% end %>

いいねに対するバリデーションメッセージを表示する

  • せっかくなのでバリデーションメッセージも表示する
  • 赤文字で表示するだけ
app/view/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>

<h3>本文</h3>
<p><%= @post.body %></p>

<%= form_for [@post, @like] do |f| %>
  <% if @like.errors.any? %>
    <% @like.errors.full_messages.each do |msg| %>
      <li style="color: red;"><%= msg %></li>
    <% end %>
  <% end %>
  <%= f.submit 'いいね!' %>
<% end %>

<h2>この記事にいいねしたユーザー</h2>
<% @post.liked_users.each do |user| %>
  <li><%= user.email %></li>
<% end %>

いいね取り消し機能

  • いろいろな書き方があると思う
  • 細かいところはさておき、ひとまずの機能を実装
  • 既にいいねしているか? でボタン表記を制御(いいね可能かどうか? ではない!)
    • User.already_liked?を実装
  • いいね取り消しは論理削除ではなく物理削除を採択
app/models/user.rb
class User < ApplicationRecord
  # ...

  def already_liked?(post)
    self.likes.exists?(post_id: post.id)
  end
end
app/controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    # ...
  end

  def destroy
    @like = Like.find_by(post_id: params[:post_id], user_id: current_user.id)
    @like.destroy
    redirect_to post_path(params[:post_id])
  end

  private
    # ...
end
app/view/posts/show.html.erb
<!-- ...  -->

<% if current_user.already_liked?(@post) %>
  <%= button_to 'いいねを取り消す', post_like_path(@post), method: :delete %>
<% else %>
  <%= form_for [@post, @like] do |f| %>
    <% if @like.errors.any? %>
      <% @like.errors.full_messages.each do |msg| %>
        <li style="color: red;"><%= msg %></li>
      <% end %>
    <% end %>
    <%= f.submit 'いいね!' %>
  <% end %>
<% end %>

<!-- ... -->

いいね件数を表示

app/view/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>

<h3>本文</h3>
<p><%= @post.body %></p>

<!-- ココ追記 -->
<h3>いいね件数: <%= @post.likes.count %></h3>

<% if current_user.already_liked?(@post) %>
  <%= button_to 'いいねを取り消す', post_like_path(@post), method: :delete %>
<% else %>
  <%= form_for [@post, @like] do |f| %>
    <% if @like.errors.any? %>
      <% @like.errors.full_messages.each do |msg| %>
        <li style="color: red;"><%= msg %></li>
      <% end %>
    <% end %>
    <%= f.submit 'いいね!' %>
  <% end %>
<% end %>

<h2>この記事にいいねしたユーザー</h2>
<% @post.liked_users.each do |user| %>
  <li><%= user.email %></li>
<% end %>

ユーザー権限の制御

  • posts#index(ブログ記事一覧あるいは各ユーザーの記事一覧)以外はログインユーザーのみ
app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index]

  def index
    # ...
  end

  def show
    # ...
  end
end
app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :authenticate_user!, only: [:create, :destroy]

  def create
    # ...
  end

  def destroy
    # ...
  end

  private
    # ...

end

まとめ

  • 無事にいいねボタンを実装した
  • やはり重要なのはモデル同士がどう関連しているか、ということ
  • Ajax化などもあると思うが、今回はここまで
  • ここまでの実装にはGitHub上でv2というタグをつけていますmohira/like-btn v2
38
35
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
38
35