#前説
いいね機能の実装コードの全貌を載せています。少し、情報量が多くて大変です。コードのわかりにくいところに補足をつけておきました。何か間違いやご指摘などございましたら、ご連絡ください。
#いいね機能の実装
Twitterでタイムラインのツイートに対していいね!をしますよね!
あの機能を一応実装しました。非同期通信で行う予定でしたが、今回はRails部のみの実装を行いました。
#前提
MVCモデルをある程度理解してらっしゃる方向け
Ruby言語をある程度触ってらっしゃる方向け
Rails 4.2です。
#環境整備
Likesのコントローラー、モデル、データベーステーブルを用意します。
bundle exec rake db:create
bundle exec rails g model like
bundle exec rails g controller likes
#データベース設定
以下はmigrateファイルです。データベーステーブルは以下のように作成しました。
class Likes < ActiveRecord::Migration
def change
create_table :likes do |t|
t.integer :user_id, null: false
t.integer :product_id, null: false
t.timestamps
t.index :user_id
t.index :product_id
t.index [:user_id, :product_id] , unique: true
end
end
end
bundle exec rake db:migrate
#Model
Modelファイルです。固有のツイートに対し、一人のユーザーのいいね!は一意です。
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :product
validates :user_id, presence: true
validates :product_id, presence: true
# validates_uniqueness_of :product_id, scope: :user_idでも可能
end
product.rbのところでは、データベースのテーブルのアソシエーションの関係性が問題です。
一人のユーザーがいくつものツイート(products)を投稿。→ user:products = 1 : 多
一つのツイート(product)に対し、複数人のいいね。→ (users:likes):product = 多 : 1
userとproductの関係が重複するので、以下のように記述している。
class Product < ActiveRecord::Base
belongs_to :user
has_many :likes , dependent: :destroy #削除されると自動的にいいねは消える
has_many :like_users, through: :likes, source: :user #多ー多関係であるので区別化してる
def user_exist?(user)
like_users.include?(user)
# いいねしたユーザーのなかにuser(引数)がいるかどうか
end
end
#ルーティング
ルーティングは以下の通りです。productにネストしています。
resources :products do
resources :likes, only: [:create, :destroy]
end
product_likes POST /products/:product_id/likes(.:format) likes#create
product_like DELETE /products/:product_id/likes/:id(.:format) likes#destroy
#View
Viewファイルでは以下のように編集します。
index.html(タイムライン)やshow.html(Tweet詳細ページ)、show.html(User詳細ページ)から参照できるようにします。それぞれのページからrenderで参照させなくてはいけませんが、index.html(タイムライン)アクセス時ではproduct_id(Tweetのid)を取得できません。
<div class = "product">
<% products.each do |a_product| %>
<div class="card col s3">
<%= render 'products/product' , a_product: a_product %>
</ul>
</div>
<% end %>
</div>
<ul class="collection">
・ ・ ・
<li id="product-<%= a_product.id %>" class="collection-item" >
<span class="timestamp">
</span>
<%= render 'products/likes' , a_product: a_product %>
</li>
・ ・ ・
</ul>
正直ここが一番苦戦しました。
<% if current_user %>
<% if a_product.user_exist?(current_user) %>
<!--現在のユーザーがすでにいいね!を押してるかどうかの判定-->
<%= form_for [a_product,@like],url: product_like_path(a_product.id,@like.id),method: :delete do |f| %>
<!--すでにいいね!を押していると、deleteメソッドで削除-->
<!--ここのform_forの引数がわかりにくいですが、ルーティングのDELETEのパスの引数に:product_idと:id(like)を当てはめています。-->
<% button_tag(type: "submit",class: "waves-effect waves-light btn") do %>
<i class="fa fa-arrow-left"></i>
<%= f.submit "いいね解除" %>
<% end %>
<% end %>
<% else %>
<div class = "action">
<%= form_for [a_product, @likes], methos: :post do |f| %>
<%= button_tag type: "submit", class: "waves-effect waves-light btn" do %>
<!--いいね!を押していないと、postメソッドで追加-->
<i class="fa fa-arrow-left"></i>
<%= f.submit "いいね" %>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
#Controller
次はコントローラーです。いいね!をデータベースに保存するcreateアクションとdestroyアクションを作成しています。
def index
@likes = Like.new(user_id: current_user.id, product_id: params[:product_id])
#create時に生成するインスタンス
@like = Like.find_by(user_id: current_user.id)
#destroy時にテーブルから探し出す
@products = Product.order("created_at DESC").page(params[:page]).per(10)
end
class LikesController < ApplicationController
def create
Like.create(like_params)
redirect_to controller: :products, action: :index
# データベースにuserのidを含んだ情報を保存
# current_userの情報を引数に入れて、Likeしたuserとしてデータベースに保存
end
def destroy
@like_product = Like.find(params[:id]).product
# いいねされたツイートのidを取得してのそツイートの情報を取得
Like.find_by(user_id: current_user.id, product_id: params[:product_id]).destroy
# すでにLike!されているツイートのidを取得してその現在のユーザーがしたいいね!を削除
redirect_to controller: :products, action: :index
end
private
def like_params
params.permit(:product_id).merge(user_id: current_user.id)
end
end
これで一応、いいね機能は実装できました。あとは非同期通信で画面遷移なしでいいね!できるようにします。
続きはまた次回!