この記事について
- いわゆる「いいねボタン」をRailsで実装する記事です
- 少々長いので記事は2分割されています
- 自分で書いていたのですが、せっかくなので解説などを付け足してQiitaの記事にしました
対象読者(≒この記事を読むことによって幸せになれる可能性がある人)
- いいね機能を作りたい人
- 多対多の関係がいまいちピンと来ていない人
- 愛に溢れるマサカリを投げてくれる奇特なエンジニアさん
こちらもどうぞ
-
【Rails】いいねボタンを作ろう part2/2
- この記事の続き
-
【Rails】ミニアプリを作りながら多対多のアソシエーションとネストされたルーティングを学ぶ
- 多対多とネストされたルーティングによりフォーカスした記事
設計
ざっくり要件
- ユーザー認証機能(ログイン機能)
- ログインユーザーはブログ記事を作成できる
- [これが本丸]ログインユーザーは各ブログ記事に1回だけいいね! することができる
- いいね! は1ユーザー1ブログ記事につき1回の制限
- 自分のブログ記事に対してもいいね! することはできる
- 既に、いいね! をしているブログ記事に対して、いいね! を解除することができる
- ボタンは1つで実装し、いいね! の状態によって切り替える
- ユーザーのブログ記事一覧が表示される
- ブログ記事詳細画面で、いいね! 件数および、どのユーザーがいいね! しているかが表示される
テーブル
- データ型はRails的な書き方
Users
- ユーザーを管理するテーブル
- Deviseによって生成される構造に準拠する
カラム | データ型 | 役割 |
---|---|---|
string | メールアドレス | |
password | string | パスワード |
その他いろいろ | いろいろ |
Postモデル
カラム | データ型 | 役割 |
---|---|---|
user_id | integer | 投稿したuserのid |
title | string | ブログ記事のタイトル |
body | text | ブログ記事の本文 |
Likeモデル
カラム | データ型 | 役割 |
---|---|---|
user_id | integer | いいね! したuserのid |
post_id | integer | いいね! されたpostのid |
GitHub
開発環境
- Ruby 2.3.0
- Rails 5.0.1
- おそらくRails 4.x でも動作すると思います(未確認)
- 4.x での
rake
コマンドは、5.xではrails
になっているので注意
ここから実装
プロジェクト作成
$ rails new like-btn
$ cd like-btn
Devise導入とUserモデル生成
- Deviseを使って生成する
Gemfile
gem 'devise'
$ bundle install
$ rails g devise:install
$ rails g devise User
$ rails db:migrate
config/routes.rb
root 'posts#index'
Postモデル生成
$ rails g model Post title:string body:text user:references
Migrationスクリプト修正
null制約だけ追加
db/migrate/yyyymmddhhiiss_create_post.rb
class CreatePosts < ActiveRecord::Migration[5.0]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :body, null: false
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
$ rails db:migrate
Seedデータ作成
- 3人のユーザーが3件ずつ記事を持つ
db/seeds.rb
User.delete_all
Post.delete_all
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
)
end
end
UserとPostのアソシエーション(1対多)
- 基本的な1対多の関係(多対多の話はしばらく先)
- ポイントはuserを削除したときに関連する(≒そのuserが作成した)ブログ記事を削除するところ
User
-
dependent: :destroy
をつける-
dependent
は「依存」という意味。やはり英単語を抑えておくとイメージしやすい - かの有名な「依存関係」の「依存」である
- 「依存」の反対を意味する
independence
(独立)の方が有名かもしれない
-
app/models/user.rb
class User < ApplicationRecord
has_many :posts, dependent: :destroy
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
consoleで確かめる
-
dependent: :destroy
の威力を確かめる
$ rails c
irb(main):001:0> u3 = User.find(3)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
=> #<User id: 3, email: "user3@example.com", created_at: "2017-01-26 05:55:16", updated_at: "2017-01-26 05:55:16">
irb(main):002:0> u3.posts
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 3]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 7, title: "user3@example.comの記事 その1", body: "body1 by user3@example.com", user_id: 3, created_at: "2017-01-26 05:55:16", updated_at: "2017-01-26 05:55:16">, #<Post id: 8, title: "user3@example.comの記事 その2", body: "body2 by user3@example.com", user_id: 3, created_at: "2017-01-26 05:55:16", updated_at: "2017-01-26 05:55:16">, #<Post id: 9, title: "user3@example.comの記事 その3", body: "body3 by user3@example.com", user_id: 3, created_at: "2017-01-26 05:55:16", updated_at: "2017-01-26 05:55:16">]>
irb(main):003:0> u3.destroy
(0.1ms) begin transaction
SQL (0.7ms) DELETE FROM "posts" WHERE "posts"."id" = ? [["id", 7]]
SQL (0.1ms) DELETE FROM "posts" WHERE "posts"."id" = ? [["id", 8]]
SQL (0.1ms) DELETE FROM "posts" WHERE "posts"."id" = ? [["id", 9]]
SQL (0.2ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 3]]
(9.0ms) commit transaction
=> #<User id: 3, email: "user3@example.com", created_at: "2017-01-26 05:55:16", updated_at: "2017-01-26 05:55:16">
Post
- Postモデル生成時に
user:references
を使ったので既に記述されている
app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
end
関連モデルと外部キーと指定する場合の1対多アソシエーションの記述方法
- Postモデルに記述したアソシエーションはこれで問題ないが、別の書き方もできる
- そもそもアソシエーションを指定するにあたり必要な情報はいくつかある
- どんな関係性か? (belongs_to や has_manyなど)
- どういうメソッド名で取り出すか? (第1引数)
- どのモデルとの関係か? (
class_name
で指定) - 外部キーの名前は何か? (
foreign_key
で指定)
現状の記述
belongs_to :user
- 上記のような記述で指定しているのは、関係性(1)とメソッド名(2)だけである
- しかし、postsテーブルには
user_id
とカラムがあり、 - これらの名前がルールどおりになっているので、
- 自動的に、関連モデル(3)はUserモデル、外部キー(4)は
user_id
と判断している
逆に言えば、こちらから指定することもできるわけだ。
app/models/post.rb
class Post < ApplicationRecord
# belongs_to :user
# 上の記述と同じ
belongs_to :user, class_name: :User, foreign_key: :user_id
end
このように記述しても、先程の記述と同じ働きをする。
しかし、わざわざ書くのは手間なので(普段は)短く書いていたわけである。
アソシエーションを覚えたばかりの段階では、「規約通りの書き方」になるが、やはり色々な指定ができるので、次のような使い方もできる。
app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
# 上の記述と同じ
# belongs_to :user, class_name: :User, foreign_key: :user_id
# このような書き方もできる(post.user_infoでアクセス)
# belongs_to :user_info, class_name: :User, foreign_key: :user_id
end
この場合、ある1つのpostから、そのpostを書いたuserを辿るときに@post.user_info
という記述になる(下記にconsoleでの結果を載せた)。
何がメリットかというと、メソッド名を分かりやすくできるできるのだ。今回の場合は、「ブログ記事を書いたユーザー情報」なのでuser
でも違和感は無いが、モデルの設計が変わる場合(例えば、ユーザー情報は全てUserモデルで管理するが、ユーザー同士に上司と部下の関係性があるといった場合など)には有用である。
今回はbelongs_to :user
の記述で進める。
$ rails c
irb(main):003:0> p1 = Post.first
=> #<Post id: 1, title: "user1@example.comの記事 その1", body: "body1 by user1@example.com", user_id: 1, created_at: "2017-01-26 05:55:16", updated_at: "2017-01-26 05:55:16">
irb(main):004:0> p1.user_info
=> #<User id: 1, email: "user1@example.com", created_at: "2017-01-26 05:55:15", updated_at: "2017-01-26 05:55:15">
irb(main):005:0>
ルーティングの設計
- ブログ記事を見るときのルーティングをどうするか?
- 改めて要件を確認(どんなページを用意するか?)
- 全ユーザーの一覧が見れる
- 各ユーザーの詳細が見れる
- 全ユーザーが投稿したブログ記事の一覧が見れる
- 各ブログ記事の詳細が見れる
- 各ユーザーごとの投稿したブログ記事一覧が見れる
「1. 全ユーザーの一覧が見れる」 と 「2. 各ユーザーの詳細が見れる」
- まずはこれから考える
-
devise_for :users
という記述によってUser関連のルーティングがいくつかできる - しかし、この中にはCRUDで言うところの一覧(#index)と詳細(#show)がない
- なので、この2つのルーティングを素直に加えればOK
config/routes.rb
Rails.application.routes.draw do
devise_for :users
# ココを追記
resources :users, only: [:index, :show]
root 'posts#index'
end
これにより次のルーティングが追加される
ルーティング(一部)
Prefix Verb URI Pattern Controller#Action
users GET /users(.:format) users#index
user GET /users/:id(.:format) users#show
「3. 全ユーザーが投稿したブログ記事の一覧が見れる」 と 「4. 各ブログ記事の詳細が見れる」
- これは単純に
resources :posts
という記述でOK - postsのデータ生成についてはseedでまかなうので#indexと#showだけつくる
- あくまで主役はブログ記事だからuserは関係がない
config/routes.rb
Rails.application.routes.draw do
devise_for :users
resources :users, only: [:index, :show] do
resources :posts, only: [:index, :show]
root 'posts#index'
end
「5. 各ユーザーごとの投稿したブログ記事一覧が見れる」
- これが問題である
- 目的としては、「◯◯さんの記事一覧」や「★★さんの記事一覧」という設計にしたい
-
/users/1/posts
(user_idが1のユーザー記事一覧) -
/users/2/posts/3
(user_idが1のユーザーが投稿したブログ記事)
-
- つまり、URLに投稿したuserのidが必要になる
- ここで、ネストされたルーティングを利用する
config/routes.rb
Rails.application.routes.draw do
devise_for :users
resources :users, only: [:index, :show] do
resources :posts
end
resources :posts, only: [:index, :show]
root 'posts#index'
end
その結果、下記のルーティングが生成される
ルーティング(一部)
Prefix Verb URI Pattern Controller#Action
user_posts GET /users/:user_id/posts(.:format) posts#index
POST /users/:user_id/posts(.:format) posts#create
new_user_post GET /users/:user_id/posts/new(.:format) posts#new
edit_user_post GET /users/:user_id/posts/:id/edit(.:format) posts#edit
user_post GET /users/:user_id/posts/:id(.:format) posts#show
PATCH /users/:user_id/posts/:id(.:format) posts#update
PUT /users/:user_id/posts/:id(.:format) posts#update
DELETE /users/:user_id/posts/:id(.:format) posts#destroy
users GET /users(.:format) users#index
user GET /users/:id(.:format) users#show
- いくつかのポイントがある
-
users
が先にくる - パスの中で
userのid
は:user_id
というシンボルになっているが、postのid
は:id
となっている(:post_id
ではない!) - Prefixはuserとpostを含んだものになり、これによって新たなメソッドが開放される(後述)
-
アソシエーション設定によって開放されるメソッド群(パス編)
- ネストされたリソースのパスを返すメソッドがいくつ
-
link_to
やform_for
redirect_to
は パスを指定するが、そのパスを指定する際に、Prefix_path
だけではなくて パスを表す配列も使える
表にまとめる
-
前提
-
@user
は1つのuserオブジェクトが格納 -
@post
は1つのpostオブジェクトが格納
-
-
ポイント
- 単数形と複数形をみればわかる
- 順番も大事
アクション | path指定の場合 | 配列指定の場合 |
---|---|---|
index | user_entries_path(@user ) |
[@user , :posts] |
create | user_posts_path(@user , @post ) |
[@user , :posts] |
new | new_user_post_path(@user ) |
[:new, @post , :post] |
edit | edit_user_post_path(@user , @post ) |
[:edit, @user , @post ] |
show | user_post_path(@user , @post ) |
[@user , @post ] |
update | user_post_path(@user , @post ) |
[@user , @post ] |
destroy | user_post_path(@user , @post ) |
[@user , @post ] |
パスの指定
<!-- どっちも同じで、あるuserの記事一覧にとぶ -->
<!-- 生成パス: /users/:user_id/posts -->
<% @user = user.frist %>
<%= link_to 'firstさんの記事一覧へ', [@user, :posts] %>
<%= link_to 'firstさんの記事一覧へ', user_posts_path(@user) %>
Postsコントローラ生成
- ルーティングが出来たので画面を作成する
ブログ記事管理の設計を確認する
- Postsコントローラを作って、次の2つをつくる
- #index: 記事一覧(全ユーザーの記事一覧と各ユーザーごとの記事一覧)
- #show: 記事詳細
- #indexではURLを基にして全ユーザーの記事一覧を表示するか、特定のユーザーの記事一覧を表示するかを分ける
まずはコントローラ生成
$ rails g controller Posts index show
indexアクション
- URLを考えると
- 全ユーザー記事一覧 →
/posts
- 特定のユーザーの記事一覧 →
users/:user_id/post
- 全ユーザー記事一覧 →
- つまり、
params[:user_id]
の有無によって判断する
app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
if params[:user_id]
@user = User.find(params[:user_id])
@posts = @user.posts # ここでアソシエーションが生きる
else
@posts = Post.all
end
end
end
ビューは例えば、次のようになる
app/views/posts/index.html.erb
<% if @user.present? %>
<h1><%= @user.email %>さんの記事一覧</h1>
<% else %>
<h1>全ユーザーの記事一覧</h1>
<% end %>
<% @posts.each do |post| %>
<li>タイトル: <%= link_to post.title, post %></li>
<% end %>
showアクション
- 基本的な書き方
app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
if params[:user_id]
@user = User.find(params[:user_id])
@posts = @user.posts
else
@posts = Post.all # ここでアソシエーションが生きる
end
end
def show
@post = Post.find(params[:id])
end
end
app/views/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>
<h3>本文</h3>
<p><%= @post.body %></p>
ここまでのまとめ
- 長くなってしまったのでここで区切る
- ここまでの実装は
v1
というタグを付けています - mohira/like-btn v1
- ここまでの実装は
- 肝心のいいね機能は次の記事で実装する
ポイント
- アソシエーションにおける関連モデルと外部キーの記述
- ネストされたルーティング
- ネストされたリソースのパスを返すメソッドとその記述