目的
railsにて、オリジナルのSNSアプリ(「Kuishare」 URL:https://kuishare.herokuapp.com) に「いいね」機能を追加します。ここでいう「いいね」機能はtwitterの「いいね」と同じ意味合いです。
環境
rails 5.1.6「いいね」機能に関与するモデルの現在の構造(今回関与しない属性は省略します)
User(ユーザー情報用モデル)
→name
Post(投稿用モデル)
→content
1.データモデルの構造
データモデルの**構造**は、ユーザをフォローする機能と少し似ています。簡単に言うと、**「いいね」**をする**”誰か”**が **”どのマイクロポスト”**に**「いいね」**したかを管理するモデルを追加すればいいんです。 このデータモデルを実装するために、マイグレーションを生成します。$ rails generate model Like user_id:integer post_id:integer
また、追加したカラムでの検索が頻繁に行われるため、インデックスも追加します。
class CreateLikes < ActiveRecord::Migration[5.1]
def change
create_table :likes do |t|
t.integer :user_id, null: false
t.integer :post_id, null: false
t.timestamps
t.index :user_id
t.index :post_id
t.index [:user_id, :post_id], unique: true
end
end
end
複合キーインデックスがあります。これにより、user_idとpost_idの
組み合わせが必ずユニークであることを保証し、1ユーザが同じマイクロポストに複数回いいねすることを防ぎます。
データベースのマイグレーションを行います。
$ rails db:migrate
#2. User/Like、Post/Likeの関連付け
1つのポストには1対多(has_many)のいいねがあり、1人のユーザには1対多(has_many)のいいねがあります。
いいねはその両方に属します(belongs_to)。
UserとLike、PostとLikeの関係をコードにまとめると、以下のようになります。
class User < ApplicationRecord
.
.
.
has_many :likes, dependent: :destroy
.
.
.
end
class Post < ApplicationRecord
.
.
.
has_many :likes, dependent: :destroy
.
.
.
end
class Like < ApplicationRecord
belongs_to :user
belongs_to :post
validates :user_id, presence: true
validates :post_id, presence: true
end
User、Postには、それぞれが削除されたらLikeも削除する、
Likeにはuser_id、post_idが必須である、という記載もしています。
#3.ルーティング#
Likesコントローラには、
createとdestoryがあれば十分です。コントローラを作成し、ルーティングを修正しましょう。
その際、コントローラ作成時に自動的に追加されるルーティングの削除を忘れないようにしてください。
Rails.application.routes.draw do
.
.
.
resources :likes, only: [:create, :destroy]
.
.
.
end
#4.いいね機能(仮)を実装する#
ルーティングができたので、機能について考えましょう。
考え方のヒントは「”誰”が”どのポスト”にいいねしたか」です。
”誰”がいいねをするかは、ログインしているユーザです。
”どのポスト”にいいねするかは、いいねが配置さているポスト。
いいねは各ポストに対して配置していますので、マイクロポストのいいね機能をPostモデルに記載します。
これで、likesテーブルのレコードを作る情報が全て出揃いましたので、いいね機能(仮)を実装できるはずです。
class Post < ApplicationRecord
.
.
.
# マイクロポストをいいねする
def iine(user)
likes.create(user_id: user.id)
end
# マイクロポストのいいねを解除する(ネーミングセンスに対するクレームは受け付けません)
def uniine(user)
likes.find_by(user_id: user.id).destroy
end
.
.
.
end
これらのメソッドをコントローラから呼び出せば(仮)の完成です。
class LikesController < ApplicationController
before_action :logged_in_user
def create
@post = Post.find(params[:post_id])
@post.iine(current_user)
end
def destroy
@post = Like.find(params[:id]).post
@post.uniine(current_user)
end
end
#5.いいねボタンを実装する#
いいねボタンはポストの下に配置するので、PostビューにLikeビューを配置することが分かります。
<li id="post-<%= post.id %>" data-post-id="<%= post.id %>">
.
.
.
<span class="timestamp">
.
.
.
</span>
<%= render "likes/like", post: post %>
</li>
render時に見慣れない記述があります。
<%= render "likes/like", post: post %>
Likesコントローラではポストのidが必要になるため、Likeビューをパーシャル化する際に、
Postをビューに渡してあげる必要があります。
次に、上記で呼んでいるLikeのパーシャルは以下のようになります。
<% if !current_user?(post.user) %>
<span class="like">
<% if post.iine?(current_user) %>
<%= form_for(post.likes.find_by(user_id: current_user.id), method: :delete, remote: true) do |f| %>
<%= button_tag(class: "btn btn-xs") do %>
<%= content_tag :span, "#", class: "glyphicon glyphicon-heart" %>
<% end %>
<% end %>
<% else %>
<%= form_for(post.likes.build, remote: true) do |f| %>
<div><%= hidden_field_tag :post_id, post.id %></div>
<%= button_tag(class: "btn 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 post.iine?(current_user) %>
このメソッドをPostモデルに作成します。その際に、モデルの関連も追加したいと思います。
どのような関連かというと、「ポストにいいねをしたユーザーの一覧」という関連です。
これを追加すれば、あとはそのユーザ一覧にcurrent_userがいれば、既にいいねしている、
いなければ、まだいいねしていないということが分かります。
class Post < ApplicationRecord
.
.
.
has_many :iine_users, through: :likes, source: :user
.
.
.
# 現在のユーザーがいいねしてたらtrueを返す
def iine?(user)
iine_users.include?(user)
end
.
.
.
end
#6.いいねボタンAjax化#
上記までの実装で、基幹部分の実装ができていますが、いいねボタン押下時の描画の実装をしていません。
その描画をAjaxを使用して実装していきます。
まずはコントローラをAjaxリクエストに対応させます。
class LikesController < ApplicationController
def create
@post = Post.find(params[:post_id])
unless @post.iine?(current_user)
@post.iine(current_user)
@post.reload
respond_to do |format|
format.html { redirect_to request.referrer || root_url }
format.js
end
end
end
def destroy
@post = Like.find(params[:id]).post
if @post.iine?(current_user)
@post.uniine(current_user)
@post.reload
respond_to do |format|
format.html { redirect_to request.referrer || root_url }
format.js
end
end
end
end
ここで、既にいいねされているかどうかの確認を追加していることにも注意してください。likeテーブルは
user_id、post_idがPKとなっているため、2つのブラウザで同時にいいねされてもエラーと
ならないようにしています。
次に、js用のerbを作成します。
$("#post-<%= @post.id %> .like").html("<%= escape_javascript(render "likes/like", post: @post) %>");
$("#post-<%= @post.id %> .like").html("<%= escape_javascript(render "likes/like", post: @post) %>");
これで、いいね機能が正しく動作するようになりました。しかし、まだ終わりではありません。
いいねの数をいいねボタンの横に表示する必要があります。
#7.いいね数のカウントとcounter_culture#
いいね数のカウント方法ですが、railsには counter_cultureというgemがあります。
これを利用したいと思います。
source 'https://rubygems.org'
.
.
.
gem 'counter_culture', '~> 1.8'
group :development, :test do
.
.
.
bundle installを実行して、counter_cultureをインストールします。
$ bundle install
次に、必要なテーブルにカウント数を格納するカラムを追加します。今回は、Postテーブルに
いいね数を格納したいので、Postテーブルに追加します。
$ rails generate migration add_likes_count_to_microposts likes_count:integer
生成されたマイグレーションファイルを編集します。
class AddLikesCountToPosts < ActiveRecord::Migration[5.1]
class MigrationUser < ApplicationRecord
self.table_name = :posts
end
def up
_up
rescue => e
_down
raise e
end
def down
_down
end
private
def _up
MigrationUser.reset_column_information
add_column :posts, :likes_count, :integer, null: false, default: 0 unless column_exists? :posts, :likes_count
end
def _down
MigrationUser.reset_column_information
remove_column :posts, :likes_count if column_exists? :posts, :likes_count
end
end
ここでは私の練習のため、changeメソッドは使用せず、up、downメソッドを使用しています。
なぜ分割しているかというと、migrationファイルは冪等性(べきとうせい)を担保する必要があり、
upとdownで処理を分ける場合があるからみたいです。(むずかしい。。)
冪等性が担保されなかった場合、rollbackが正しく行われず、中途半端なDB状態となる恐れがあります。
(私たちのプロジェクトでも過去のmigrationファイルの冪等性が担保されていなかったために、
開発環境のDBが壊れ、バックアップから復元する羽目になりました。)
では、いつものようにデータベースのマイグレーションを行います。
$ rails db:migrate
そして関連付けを行います。
class Like < ApplicationRecord
belongs_to :user
belongs_to :post
counter_culture :post
validates :user_id, presence: true
validates :post_id, presence: true
end
これでいいねのカウントを自動的に集計し、Postテーブルに持つことが出来るようになりました。
後はそれを表示するだけとなります。Bootstrapのiconを使用する際に、#としていた箇所を書き換えます。
<% if !current_user?(post.user) %>
<span class="like">
<% if post.iine?(current_user) %>
<%= form_for(post.likes.find_by(user_id: current_user.id), method: :delete, remote: true) do |f| %>
<%= button_tag(class: "btn btn-xs") do %>
<%= content_tag :span, "#{post.likes_count}", class: "glyphicon glyphicon-heart" %>
<% end %>
<% end %>
<% else %>
<%= form_for(post.likes.build, remote: true) do |f| %>
<div><%= hidden_field_tag :post_id, post.id %></div>
<%= button_tag(class: "btn btn-xs") do %>
<%= content_tag :span, "#{post.likes_count}", class: "glyphicon glyphicon-heart-empty" %>
<% end %>
<% end %>
<% end %>
</span>
<% end %>
これで、いいね機能完成です。
#感想#
bootstrap-icon便利だと思いました。
ajaxを利用した箇所の理解が少し深まってよかったです!