LoginSignup
29
46

More than 5 years have passed since last update.

【Rails】いいねボタンを作ろう part1/2

Last updated at Posted at 2017-01-26

この記事について

  • いわゆる「いいねボタン」をRailsで実装する記事です
  • 少々長いので記事は2分割されています
  • 自分で書いていたのですが、せっかくなので解説などを付け足してQiitaの記事にしました

対象読者(≒この記事を読むことによって幸せになれる可能性がある人)

  • いいね機能を作りたい人
  • 多対多の関係がいまいちピンと来ていない人
  • 愛に溢れるマサカリを投げてくれる奇特なエンジニアさん

こちらもどうぞ

設計

ざっくり要件

  • ユーザー認証機能(ログイン機能)
  • ログインユーザーはブログ記事を作成できる
  • [これが本丸]ログインユーザーは各ブログ記事に1回だけいいね! することができる
    • いいね! は1ユーザー1ブログ記事につき1回の制限
    • 自分のブログ記事に対してもいいね! することはできる
  • 既に、いいね! をしているブログ記事に対して、いいね! を解除することができる
    • ボタンは1つで実装し、いいね! の状態によって切り替える
  • ユーザーのブログ記事一覧が表示される
  • ブログ記事詳細画面で、いいね! 件数および、どのユーザーがいいね! しているかが表示される

テーブル

  • データ型はRails的な書き方

Users

  • ユーザーを管理するテーブル
  • Deviseによって生成される構造に準拠する
カラム データ型 役割
email 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モデルに記述したアソシエーションはこれで問題ないが、別の書き方もできる
  • そもそもアソシエーションを指定するにあたり必要な情報はいくつかある
    1. どんな関係性か? (belongs_to や has_manyなど)
    2. どういうメソッド名で取り出すか? (第1引数)
    3. どのモデルとの関係か? (class_nameで指定)
    4. 外部キーの名前は何か? (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. 各ユーザーの詳細が見れる
    3. 全ユーザーが投稿したブログ記事の一覧が見れる
    4. 各ブログ記事の詳細が見れる
    5. 各ユーザーごとの投稿したブログ記事一覧が見れる

「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
  • いくつかのポイントがある
    1. usersが先にくる
    2. パスの中でuserのid:user_idというシンボルになっているが、postのid:idとなっている(:post_idではない!)
    3. Prefixはuserとpostを含んだものになり、これによって新たなメソッドが開放される(後述)

アソシエーション設定によって開放されるメソッド群(パス編)

  • ネストされたリソースのパスを返すメソッドがいくつ
  • link_toform_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つをつくる
    1. #index: 記事一覧(全ユーザーの記事一覧と各ユーザーごとの記事一覧)
    2. #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>

ここまでのまとめ

  • 長くなってしまったのでここで区切る
  • 肝心のいいね機能は次の記事で実装する

ポイント

  • アソシエーションにおける関連モデルと外部キーの記述
  • ネストされたルーティング
  • ネストされたリソースのパスを返すメソッドとその記述

つづきはこちら!

29
46
0

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
29
46