この記事について
こちらもどうぞ
前回までのあらすじ
- 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)
Postモデル
- 各モデルとの関係性は2つ
- 1件のpostは複数のlikesを持つ(色々なユーザーが、1件のブログ記事にいいね! をする)
- [こっちがポイント]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
について
-
source
はhas_many ... through
を使うときにどのモデルと関連させるかを表現するときに使う - 例によって、Railsのパターンどおりであれば、この記述は不要
- しかし、今回はpostからlikesを介してuserにアクセスするときのメソッドを
liked_users
にしているのでこの記述は必要
sourceのメリット
- メリット1: 自由なメソッド名を付けられるのでメソッド名から取得内容を判断しやすい
- ブログ記事(post)目線で、いいね(like)を経由したユーザー(user)との関係性を考えると、これらのユーザーはいいね!をした人(liked_users)にあたるので
liked_users
を採用した(別に好きなようにすればよい)
- ブログ記事(post)目線で、いいね(like)を経由したユーザー(user)との関係性を考えると、これらのユーザーはいいね!をした人(liked_users)にあたるので
- メリット2: 例えば、参考リンク先のような状況でも対応できる(ユーザー情報はUserモデルで管理しているが、user同士に商品の買い手と売り手といった関係性が存在する場合など)
- 参考: has_many :through の関連に同一モデルを含む場合【rails4】
- 自己結合とか自己関連と呼ぶらしい
Userモデル
- 考え方はPostのときと同様
- Like絡みの関係性は2つ
- [簡単]1人のuserは複数のlikesを持つ(1人が複数の投票できる)
- dependent: :destroyはつけておく
-
[重要]1人のuserがいいねした複数のlikesから、postsを辿れる(自分がいいねをした全ての記事)
- その際のメソッドを
liked_posts
にしている
- その際のメソッドを
- [簡単]1人のuserは複数のlikesを持つ(1人が複数の投票できる)
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_id
はparams[: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