256
202

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 5 years have passed since last update.

Ruby on Rails いいね機能の実装と解説(Rails Tutorial 14章 演習の機能拡張)

Last updated at Posted at 2018-01-19

目的

Rails Tutorial 14章の最難関、【サンプルアプリケーションの機能を拡張する】の注釈にある、
いいね機能を実装したので、Rails Tutorial風 に解説してみたいと思います。

環境

  • Rails Tutorialと同じ環境

第14α章 マイクロポストをいいねする

この章では、サンプルアプリケーションの中にあればいいかな?という部分を完成させます。
具体的には、他のユーザのマイクロポストをいいね(およびいいね解除)できる機能を追加します。
また、1人のユーザが同じマイクロポストに複数回いいねすることもできないようにします。
本書の中で学んだ内容を思い出せば、この最終章(仮)もそれほど苦労はしないでしょう。

では、実装に入る前の事前準備として、以下にモック(完成版)を示します。
そして、まずはモデルから考え、その後コントローラ、ビュー、その他アクションの順で実装して
いきたいと思います。

スクリーンショット 2018-01-12 13.31.20.png 図 14α-1: いいねボタン追加後のプロフィールページ

14α.1 Likeモデル

マイクロポストをいいねする機能を実装する第一歩は、データモデルを構成することです。
前章を読み込んでいる読者であれば、has_many を使用すれば、実装できることが分かるでしょう。

では、まずはこれまで同様新しいトピックブランチを作成してください。

$ git checkout -b iine

14α.1.1 データモデルの構造

データモデルの構造は、ユーザをフォローする機能と少し似ています。簡単に言うと、Likeという”誰”が
”どのマイクロポスト”にいいねしたかを管理するモデルを追加すればよいのです。
このデータモデルを実装するために、マイグレーションを生成します。

$ rails generate model Like user_id:integer micropost_id:integer

また、追加したカラムでの検索が頻繁に行われるため、インデックスも追加します。

db/migrate/[timestamp]_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.1]
  def change
    create_table :likes do |t|
      t.integer :user_id, null: false
      t.integer :micropost_id, null: false

      t.timestamps

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

relationshipsでも使用した、複合キーインデックスもあります。これにより、user_idとmicropost_idの
組み合わせが必ずユニークであることを保証し、1ユーザが同じマイクロポストに複数回いいねすることを防ぎます。

では、likesテーブルを作成するために、いつものようにデータベースのマイグレーションを行います。

$ rails db:migrate

14α.1.2 User/Like、Micropost/Likeの関連付け

1つのマイクロポストには1対多(has_many)のいいねがあり、1人のユーザには1対多(has_many)の
いいねがあります。いいねはその両方に属します(belongs_to)。
前章では、関連名をactive_relationshipsとしていたため、class_nameの記載が必要でしたが、
本章では余計なことをしないため、不要となります。

UserとLike、MicropostとLikeの関係をコードにまとめると、以下のようになります。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  has_many :likes, dependent: :destroy
  .
  .
  .
end
app/models/micropost.rb
class Micropost < ApplicationRecord
  .
  .
  .
  has_many :likes, dependent: :destroy
  .
  .
  .
end
app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :micropost
  validates :user_id, presence: true
  validates :micropost_id, presence: true
end

User、Micropostには、それぞれが削除されたらLikeも削除する、
Likeにはuser_id、micropost_idが必須である、という記載もしています。

14α.2 いいね機能を実装する

モデルの作成が完了したので、いよいよいいね機能に取りかかります。
ここでは、コントローラ、ビューについて作成していきましょう。

14α.2.1 ルーティング

まずはルーティングから考えましょう。Likesコントローラには、Maicroposts、Relationshipsと同様で、
createとdestoryがあれば十分です。コントローラを作成し、ルーティングを修正しましょう。
その際、コントローラ作成時に自動的に追加されるルーティングの削除を忘れないようにしてください。

$ rails generate controller Likes create destroy
config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  resources :likes, only: [:create, :destroy]
  .
  .
  .
end
HTTPリクエスト URL アクション 名前付きルート
POST /likes create likes_path
DELETE /likes/1 destroy like_path(like)
表 14α-1: Likesリソースが提供するRESTfulルート

14α.2.2 いいね機能(仮)を実装する

ルーティングができたので、機能について考えましょう。
考え方のヒントは「”誰”が”どのマイクロポスト”にいいねしたか」となります。

”誰”がいいねをするかというと、ログインしているユーザとなるので、current_userです。

”どのマイクロポスト”にいいねするかというと、いいねボタンは、図 14α-1 を見て分かるとおり、
各マイクロポストに対して配置していますので、マイクロポストのいいね機能をMicropostモデルに
記載することが分かります。ユーザーのフォローの際は、フォローボタンをユーザーページに配置し、
フォロー機能をUserモデルに記載していましたが、それと同様です。

これで、likesテーブルのレコードを作る情報が全て出揃いましたので、いいね機能(仮)を実装できるはずです。

app/models/micropost.rb
class Micropost < ApplicationRecord
  .
  .
  .
  # マイクロポストをいいねする
  def iine(user)
    likes.create(user_id: user.id)
  end

  # マイクロポストのいいねを解除する(ネーミングセンスに対するクレームは受け付けません)
  def uniine(user)
    likes.find_by(user_id: user.id).destroy
  end
  .
  .
  .
end

これらのメソッドをコントローラから呼び出せば(仮)の完成です。

app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :logged_in_user

  def create
    @micropost = Micropost.find(params[:micropost_id])
    @micropost.iine(current_user)
  end

  def destroy
    @micropost = Like.find(params[:id]).micropost
    @micropost.uniine(current_user)
  end
end

14α.3 いいねボタンを実装する

コントローラでの処理ができましたので、次はいいねボタンを実装していきます。

Bootstrapには便利なiconがありますので、それを利用していきたいと思います。
また、フォローボタンと同じように、いいねボタンを押した時にページの再ロードが起こらないように、
Ajaxを使用して非同期でリクエストを飛ばすようにします。

14α.3-1 いいねボタンの配置とデザイン

いいねボタンはマイクロポストの下に配置するので、MicropostビューにLikeビューを配置することが分かります。

app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>" data-micropost-id="<%= micropost.id %>">
.
.
.
  <span class="timestamp">
    .
    .
    .
  </span>
  <%= render "likes/like", micropost: micropost %>
</li>

render時に見慣れない記述がありますね。

<%= render "likes/like", micropost: micropost %>

Likesコントローラではマイクロポストのidが必要になるため、Likeビューをパーシャル化する際に、
Micropostをビューに渡してあげる必要があります。

次に、上記で呼んでいるLikeのパーシャルは以下のようになります。

app/views/likes/_like.html.erb
<% if !current_user?(micropost.user) %>
    <span class="like">
    <% if micropost.iine?(current_user) %>
      <%= form_for(micropost.likes.find_by(user_id: current_user.id), method: :delete, remote: true) do |f| %>
        <%= button_tag(class: "btn btn-default btn-xs") do %>
          <%= content_tag :span, "#", class: "glyphicon glyphicon-heart" %>
        <% end %>
      <% end %>
    <% else %>
      <%= form_for(micropost.likes.build, remote: true) do |f| %>
        <div><%= hidden_field_tag :micropost_id, micropost.id %></div>
        <%= button_tag(class: "btn btn-default btn-xs") do %>
          <%= content_tag :span, "#", class: "glyphicon glyphicon-heart-empty" %>
        <% end %>
      <% end %>
    <% end %>
  </span>
<% end %>

ざっくり説明すると以下の通りになります。

  • いいねボタンは自分のマイクロポスト以外の場合に表示
  • 既にいいねしている場合は、いいねボタンを塗りつぶされたハートとし、押下時にdestroyを呼ぶ
  • いいねしていない場合は、いいねボタンを塗りつぶされていないハートとし、押下時にcreateを呼ぶ

button_tag配下にcontent_tagを指定し、classにBootstrapのiconのクラスを指定すると、
ボタンにクラスに応じた可愛いiconが表示されます。content_tagにある"#"の箇所に文字列を入れれば、
そのiconの後ろに該当の文字列が表示されます。ここには後ほど、いいね数を表示するようにします。

ここで、既にいいねされているかどうかを判断する必要が出てきました。

<% if micropost.iine?(current_user) %>

このメソッドをMicropostモデルに作成します。その際に、モデルの関連も追加したいと思います。
どのような関連かというと、「マイクロポストにいいねをしたユーザーの一覧」という関連です。
これを追加すれば、あとはそのユーザ一覧にcurrent_userがいれば、既にいいねしている、
いなければ、まだいいねしていないということが分かります。

app/models/micropost.rb
class Micropost < ApplicationRecord
  .
  .
  .
  has_many :iine_users, through: :likes, source: :user
  .
  .
  .
  # 現在のユーザーがいいねしてたらtrueを返す
  def iine?(user)
    iine_users.include?(user)
  end
  .
  .
  .
end

14α.3-2 いいねボタン(Ajax化)

上記までの実装で、基幹部分の実装ができていますが、いいねボタン押下時の描画の実装をしていません。
その描画をAjaxを使用して実装していきます。これはフォローボタンのAjax化とほとんど同じです。

まずはコントローラをAjaxリクエストに対応させます。

app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :logged_in_user

  def create
    @micropost = Micropost.find(params[:micropost_id])
    unless @micropost.iine?(current_user)
      @micropost.iine(current_user)
      respond_to do |format|
        format.html { redirect_to request.referrer || root_url }
        format.js
      end
    end
  end

  def destroy
    @micropost = Like.find(params[:id]).micropost
    if @micropost.iine?(current_user)
      @micropost.uniine(current_user)
      respond_to do |format|
        format.html { redirect_to request.referrer || root_url }
        format.js
      end
    end
  end
end

ここで、既にいいねされているかどうかの確認を追加していることにも注意してください。likeテーブルは
user_id、micropost_idがPKとなっているため、2つのブラウザで同時にいいねされてもエラーと
ならないようにしています。

次に、js用のerbを作成します。

app/views/likes/create.js.erb
$("#micropost-<%= @micropost.id %> .like").html("<%= escape_javascript(render "likes/like", micropost: @micropost) %>");
app/views/likes/destroy.js.erb
$("#micropost-<%= @micropost.id %> .like").html("<%= escape_javascript(render "likes/like", micropost: @micropost) %>");

...どちらも実施していることは一緒ですね。
フォローの場合は、フォロー用パーシャルとアンフォロー用パーシャルが分かれていたかと思いますが、
いいねの場合は、パーシャルを分けていません。その理由は忘れましたが、分けて実装しようとした際に
出来なかった気がします。もし出来るようでしたらご教授いただきますよう、よろしくお願いいたします。

これで、いいね機能が正しく動作するようになりました。しかし、まだ終わりではありません。
いいねの数をいいねボタンの横に表示する必要があります。

14α.3-3 いいね数のカウントとcounter_culture

いいね数のカウント方法ですが、railsには counter_cache という機能があります。
しかし、どうやらより高機能である、 counter_culture というgemがあるみたいですので、
そちらを利用したいと思います。

counter_cultureのREADMEを読みながら実装していきます。

Gemfile
source 'https://rubygems.org'
.
.
.
gem 'counter_culture', '~> 1.8'

group :development, :test do
.
.
.

いつものようにbundle installを実行して、counter_cultureをインストールします。

$ bundle install

次に、必要なテーブルにカウント数を格納するカラムを追加します。今回は、Micropostテーブルに
いいね数を格納したいので、Micropostテーブルに追加します。

$ rails generate migration add_likes_count_to_microposts likes_count:integer

生成されたマイグレーションファイルを編集します。

db/migrate/[timestamp]_add_likes_count_to_microposts.rb
class AddLikesCountToMicroposts < ActiveRecord::Migration[5.1]
  class MigrationUser < ApplicationRecord
    self.table_name = :microposts
  end

  def up
    _up
  rescue => e
    _down
    raise e
  end

  def down
    _down
  end

  private

  def _up
    MigrationUser.reset_column_information

    add_column :microposts, :likes_count, :integer, null: false, default: 0 unless column_exists? :microposts, :likes_count
  end

  def _down
    MigrationUser.reset_column_information

    remove_column :microposts, :likes_count if column_exists? :microposts, :likes_count
  end
end

ここでは、筆者の練習のため、changeメソッドは使用せず、up、downメソッドを使用しています。
なぜ分割しているかというと、migrationファイルは冪等性(べきとうせい)を担保する必要があり、
upとdownで処理を分ける場合があるからです。
冪等性が担保されなかった場合、rollbackが正しく行われず、中途半端なDB状態となる恐れがあります。
(私たちのプロジェクトでも過去のmigrationファイルの冪等性が担保されていなかったために、
開発環境のDBが壊れ、バックアップから復元する羽目になりました。)
では、いつものようにデータベースのマイグレーションを行います。

$ rails db:migrate

ここで、冪等性が担保されているか確認するため、再度マイグレーションを行います。

$ rails db:migrate:redo

そして関連付けを行います。

app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :micropost
  counter_culture :micropost
  validates :user_id, presence: true
  validates :micropost_id, presence: true
end

これでいいねのカウントを自動的に集計し、Micropostテーブルに持つことが出来るようになりました。
後はそれを表示するだけとなります。Bootstrapのiconを使用する際に、#としていた箇所を書き換えます。

app/views/likes/_like.html.erb
<% if !current_user?(micropost.user) %>
    <span class="like">
    <% if micropost.iine?(current_user) %>
      <%= form_for(micropost.likes.find_by(user_id: current_user.id), method: :delete, remote: true) do |f| %>
        <%= button_tag(class: "btn btn-default btn-xs") do %>
          <%= content_tag :span, "#{micropost.likes_count}", class: "glyphicon glyphicon-heart" %>
        <% end %>
      <% end %>
    <% else %>
      <%= form_for(micropost.likes.build, remote: true) do |f| %>
        <div><%= hidden_field_tag :micropost_id, micropost.id %></div>
        <%= button_tag(class: "btn btn-default btn-xs") do %>
          <%= content_tag :span, "#{micropost.likes_count}", class: "glyphicon glyphicon-heart-empty" %>
        <% end %>
      <% end %>
    <% end %>
  </span>
<% end %>

これで、完成となります。実際に動かしてみましょう。

.
.
.

動かしてみましたか?

筆者は完成したと思って動かしてみたら、カウント数が正しくないことに気づきました。
「うぉぉおおおきたぁぁああああ」って喜んて恥ずかしい思いをしました。
原因はビューに渡しているMicropostが、いいねする前の状態となっているからです。
これは、コントローラでiine後にreloadをすれば解決します。

app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :logged_in_user

  def create
    @micropost = Micropost.find(params[:micropost_id])
    unless @micropost.iine?(current_user)
      @micropost.iine(current_user)
      @micropost.reload
      respond_to do |format|
        format.html { redirect_to request.referrer || root_url }
        format.js
      end
    end
  end

  def destroy
    @micropost = Like.find(params[:id]).micropost
    if @micropost.iine?(current_user)
      @micropost.uniine(current_user)
      @micropost.reload
      respond_to do |format|
        format.html { redirect_to request.referrer || root_url }
        format.js
      end
    end
  end
end

これで本当に完成となります。実際に動かして確認してみましょう。
※testについては、本項では記載いたしません。

14α.4 本番環境へデプロイする

testが完了したら、masterブランチに取り込みましょう。

$ rails test
$ git add -A
$ git commit -m "Add micropost iine"
$ git checkout master
$ git merge iine

コードをリポジトリにpushして、本番環境にデプロイしてみましょう。

$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed

感想

railsを勉強してから2ヶ月で、ほぼ自分一人で初めて1つの機能を作りましたが、
先人の知恵(gem)の探し方がとても難しいと感じました。
これを期に、どんな先人の知恵があるのか、トレンドなどを見るようにしたいと思います。

256
202
12

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
256
202

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?