非同期通信の実装(コメント機能)

非同期通信の実装

今回はコメント機能に非同期通信を実装していく。

  • 下記関連記事

コメント機能の実装 URL:コメント機能の実装 - takifugu’s blog

いいね機能に非同期通信の実装 URL:非同期通信の実装(いいね機能) - takifugu’s blog

実装の手順

  1. ajax処理をするための記述を追加する(remote :true、local: false)

  2. コメント数の部分テンプレート作成

  3. 部分テンプレートを読み込む箇所にクラス名の記述

  4. コメント機能実行後リダイレクト先を削除(book_commentsコントローラ)

  5. js.erbの作成

前提条件

jQuery導入済み(yarnでインストール)

導入方法参考URL https://nomad.office-aship.info/rails-yarn-jquery/

コメント機能に非同期通信の実装

ajax処理をするための記述を追加する(remote :true、local: false)

非同期通信を行う記述に変更していく。

まずはコメントの投稿機能について記述していく。

app/views/book_comments/_form.html.erb

# local: falseに変更
<%= form_with(model:[book, book_comment], local: false) do |f| %>
  <%= f.text_area :comment, rows:'5', placeholder:"コメントをここに", class:"w-100" %>
  <%= f.submit "送信" %>
<% end %>

form_withの記述をlocal: falseに変更。

次に、コメント削除機能について記述していく。

app/views/book_comments/_index.html.erb

<td>
  <% if book_comment.user == current_user %>
    # remote: trueを追加
    <%= link_to "Destroy", book_book_comment_path(book, book_comment), method: :delete, remote: true, class: "btn btn-sm btn-danger", "data-confirm" => "本当に消しますか?" %>
  <% end %>
</td>

link_toの記述にremote: trueを追加。

コメント数の部分テンプレート作成

views/book_commentsフォルダにコメント数の部分テンプレート_counter.html.erbを作成する。

app/views/book_comments/_counter.html.erb

コメント数:<%= book.book_comments.count %>
部分テンプレートを読み込む箇所にクラス名の記述

部分テンプレートを読み込む箇所にクラス名を記述していく。

app/views/books/show.html.erb

:
    # classを追加、コメント表示を部分テンプレートを読み込む記述に変更
    <td class="book-comments-counter">
      <%= render 'book_comments/counter', book: @book %>
    </td>
:
    # classを追加
    <div class="book-comments-index">
      <%= render 'book_comments/index', book: @book %>
    </div>
    <%= render 'book_comments/form', book: @book, book_comment: @book_comment %>
:

また、コメント投稿の部分テンプレートのコメント記述エリアにクラス名を記述する。

app/views/book_comments/_form.html.erb

<%= form_with(model:[book, book_comment], local: false) do |f| %>
  # comment-textareaのclassを追加
  <%= f.text_area :comment, rows:'5', placeholder:"コメントをここに", class:"w-100 comment-textarea" %>
  <%= f.submit "送信" %>
<% end %>
リダイレクト先を削除(book_commentsコントローラ)

app/controllers/book_comments.rb

class BookCommentsController < ApplicationController
  def create
    book = Book.find(params[:book_id])
    #インスタンス変数に変更
    @comment = current_user.book_comments.new(book_comment_params)
    @comment.book_id = book.id
    @comment.save
    #redirect記述削除
    #redirect_to request.referer
  end

  def destroy
    #記述変更
    @comment = BookComment.find(params[:id])
    @comment.destroy
    #redirect記述削除
    #redirect_to request.referer
  end
:

リダイレクト先を削除することで、createアクションでは、create.js.erbを探しにいき、

destroyアクションでは、destroy.js.erbを探しにいく。

ローカル変数commentをインスタンス変数に変更する。

js.erbの作成

views/book_commentsフォルダにcreate.js.erbとdestroy.js.erbを作成する。

app/views/book_comments/create.js.erb

$('.comment-textarea').val("")
$('.book-comments-counter').html("<%= j(render 'book_comments/counter', book: @comment.book) %>")
$('.book-comments-index').html("<%= j(render 'book_comments/index', book: @comment.book) %>")

app/views/book_comments/destroy.js.erb

$('.book-comments-counter').html("<%= j(render 'book_comments/counter', book: @comment.book) %>")
$('.book-comments-index').html("<%= j(render 'book_comments/index', book: @comment.book) %>")

jQueryの基本構文

$("セレクタ").メソッド("パラメータ[引数]");

セレクタには変更する箇所のクラス名の指定をする(「id」やクラス、グループなどの記述が可能。)

今回使用するメソッドは.html()と.val()の2つである。

.html()で指定のHTMLに変更するというメソッド。

.val()でHTMLタグ内に記述されているvalue属性を取得したり変更することができるメソッド。

$('.book-comments-counter').html("<%= j(render 'book_comments/counter', book: @comment.book) %>")の記述で、

.book-comments_counterクラス内のhtmlをrender 'book_comments/counter', book: @comment.bookに書き換える。

$('.book-comments-index').html("<%= j(render 'book_comments/index', book: @comment.book) %>")も同様に、記述を書き換える。

$('.comment-textarea').val("")の記述で、コメント欄を空欄にしている。destroy時は必要ないため、createのみ記述。

以上でコメント機能に非同期通信の実装が完了。

非同期通信の実装(いいね機能)

非同期通信の実装

非同期通信とは

送信者のデータ送信タイミングと受信者のデータ受信タイミングを合わせずに通信を行う通信方式を指します。

実際の例としては、Twitterのいいね機能など、いいねを押した際に画面の遷移なく押した瞬間にいいねが反映されていると思います。

そのような通信を非同期通信と言います。

逆に、今まで導入してきたようにデータ受信後タイミングに合わせて通信を行う通信方式を同期通信と言います。

実装内容

今回は、以前に作成したいいね機能・コメント機能に非同期通信を実装していきます。(この記事ではいいね機能の実装を行う。)

参考URL いいね機能 - takifugu’s blog

実装手順

  1. ajax処理をするための記述を追加する(remote :true)

  2. 部分テンプレートを読み込む箇所にクラス名の記述

  3. いいね機能実行後リダイレクト先を変更(favoriteコントローラ)

  4. replace_favorite.js.erbの作成

前提条件

jQuery導入済み(yarnでインストール)

導入方法参考URL https://nomad.office-aship.info/rails-yarn-jquery/

いいね機能の非同期通信の実装

Ajax処理をするための記述を追加する(remote :true)

Ajaxについては下記URL参照。

https://developer.mozilla.org/ja/docs/Web/Guide/AJAX

いいね機能の際に作成した部分テンプレート_favorite.html.erbに記述を追加していく。

app/views/favorites/_favorite.html.erb

<% if book.favorited_by?(current_user) %>
  <%= link_to book_favorites_path(book), method: :delete, remote: true,  style: "color: red;" do %>#remote: true追加
    <i class="fas fa-heart" aria-hidden="true"></i>
    <%= book.favorites.count %>
  <% end %>
<% else %>
  <%= link_to book_favorites_path(book), method: :post, remote: true do %>#remote: true追加
    <i class="fas fa-heart" aria-hidden="true"></i>
    <%= book.favorites.count %>
  <% end %>
<% end %>

remote: trueを記述することで、非同期通信処理を実行するようになります。

部分テンプレートを読み込む箇所にクラス名の記述

部分テンプレートを読み込む箇所にクラス名を記述していく。

:
<td class=<%= "favorite-" + book.id.to_s %> > #クラス名の追加
  <%= render 'favorites/favorite', book: book %>
</td>
:
いいね機能実行後リダイレクト先を変更(favoriteコントローラ)

favoriteコントローラのcreateアクションとdestroyアクションの記述を変更する。

app/controllers/favorites_controller.rb

class FavoritesController < ApplicationController
  def create
    book = Book.find(params[:book_id])
    @favorite = current_user.favorites.new(book_id: book.id) #インスタンス変数に変更
    @favorite.save #インスタンス変数に変更
    render 'replace_favorite' #記述を変更
  end

  def destroy
    book = Book.find(params[:book_id])
    @favorite = current_user.favorites.find_by(book_id: book.id) #インスタンス変数に変更
    @favorite.destroy #インスタンス変数に変更
    render 'replace_favorite' #記述を変更
  end
end

非同期通信の状態で、renderの記述をすると、html.erbではなくjs.erbを探しにいく。

この状態では、replace_favorite.js.erbのファイルを探しにいきます。

replace_favorite.js.erbの作成

views/favoritesフォルダにreplace_favorite.js.erbファイルを作成し、記述していく。

app/views/favorites/replace_favorite.js.erb

$('.favorite-<%= @favorite.book_id %>').html("<%= j(render 'favorites/favorite',  book: @favorite.book ) %>")

jQueryの基本構文

$("セレクタ").メソッド("パラメータ[引数]");

セレクタには変更する箇所のクラス名の指定をする(「id」やクラス、グループなどの記述が可能。)

今回使用するメソッドは.html()で、指定のHTMLに変更するというメソッドである。

そのため、今回であれば、実行すると下記のようにビューファイルが書き換えられる。

#実行前
<td class=<%= "favorite-" + book.id.to_s %> >
  <%= render 'favorites/favorite', book: book %>
</td>

#実行後
<td class=<%= "favorite-" + book.id.to_s %> >
  <%= render 'favorites/favorite', book: @favorite.book %>
</td>

以上でいいね機能の非同期通信が完了。

次は、コメント機能に非同期通信を実装していく。

URL 非同期通信の実装(コメント機能) - takifugu’s blog

検索機能の実装

検索機能の実装

今回も以前に作成したBookers2のアプリケーションに検索機能を実装していく。

今回はGemを使用しない方法で実装していく。

実装する機能

コントローラ
  • searchesコントローラの作成

  • searchアクション追加(用途:検索を行う)

ビュー
  • ログインしている場合に限り、ヘッダーに検索窓・検索ボタンを設置すること

  • 検索結果表示画面を作成し、検索結果を表示すること

  • 検索対象(ユーザーか投稿か)の選択かをプルダウンメニューで選択できること

ルーティング

検索ボタン実行時、searchesコントローラのsearchアクションが実行されるように定義。

config/routes.rb

:
get "search", to: "searches#search"
:

コントローラ

コントローラの作成

searchesコントローラを作成していく。

$ rails g controller searches
アクションの記述

searchesコントローラにsearchアクションを記述していく。

app/controllers/searches_controller.rb

class SearchesController < ApplicationController
  def search
    @model = params[:model]
    @content = params[:content]
    @method = params[:method]
    if @model == 'user'
      @records = User.search_for(@content, @method)
    else
      @records = Book.search_for(@content, @method)
    end
  end
end
  • 下記のコードにて検索フォームからの情報を受け取っています。

@model : 検索モデル(User、Bookから選択)

@content : 検索ワード(ユーザーが自由記述)

@method : 検索方法(前方一致、後方一致、部分一致、完全一致から選択)

if文で、検索モデルがUserの場合とBookの場合で、条件分岐させて、

search_forメソッドの引数に検索ワードと検索方法を渡して、検索を実行し、@recordsに代入しています。

現状では、search_forメソッドの定義をしていないため、モデルにメソッドの定義をしていきます。

モデル

モデルにメソッドの定義

app/models/users.rb

:
  def self.search_for(content, method)
    if method == 'perfect'
      User.where(name: content)
    elsif method == 'forward'
      User.where('name LIKE ?', content + '%')
    elsif method == 'backward'
      User.where('name LIKE ?', '%' + content)
    else
      User.where('name LIKE ?', '%' + content + '%')
    end
  end
:

app/models/books.rb

:
  def self.search_for(content, method)
    if method == 'perfect'
      Book.where(title: content)
    elsif method == 'forward'
      Book.where('title LIKE ?', content+'%')
    elsif method == 'backward'
      Book.where('title LIKE ?', '%'+content)
    else
      Book.where('title LIKE ?', '%'+content+'%')
    end
  end
:

selfを使用し、クラスメソッドsearch_forを作成する。

whereで検索した結果を全て取得する。

Userモデルのname、Bookモデルのtitleは検索するモデルのカラム名である。

Userであれば、nameカラムを検索し、条件に合う結果を取得する。

ビュー

ヘッダーに検索窓・検索ボタンの作成

/views/searchesフォルダに部分テンプレート_form.html.erbを作成し、記述していく。

app/views/searches/_form.html.erb

<% if user_signed_in? %>
  <%= form_with url: search_path, method: :get, local: true do |f| %>
    <%= f.text_field :content %>
    <%= f.select :model, options_for_select({"User" => "user", "Book" => "book" }) %>
    <%= f.select :method, options_for_select({ "完全一致" => "perfect", "前方一致" => "forward", "後方一致" => "backward", "部分一致" => "partial"}) %>
    <%= f.submit '検索' %>
  <% end %>
<% end %>

ヘッダーの部分テンプレートに検索窓・ボタンの追加

app/views/layouts/_header.html.erb

:
<div>
  <%= render 'searches/form' %>
</div>
:
検索結果の表示ビュー作成

/views/searchesフォルダにsearch.html.erbを作成し、記述していく。

app/views/searches/search.html.erb

<% if @model == 'user' %>
  <h3>Users search for "<%= @content %>"</h3>
  <%= render 'users/index', users: @records %>
<% elsif @model == 'book' %>
  <h3>Books search for "<%= @content %>"</h3>
  <%= render 'books/index', books: @records %>
<% end %>

以上で、検索機能の実装が完了。

フォロー・フォロワー機能の実装

フォロー・フォロワー機能の実装

以前作成したBookers2というアプリケーションにフォロー・フォロワー機能を実装していく。

実装する機能

コントローラ
  • relationshipsコントローラの作成

  • createアクションを追加 (用途:フォローを作成)

  • destroyアクションを追加 (用途:フォローを削除)

  • フォローする・外すボタンをクリックしたら元画面に遷移すること

モデル
  • relationshipモデルの作成
ビュー
  • サイドバーにフォロー数・フォロワー数を表示

  • マイページ以外のサイドバーにフォローする・外すボタンを追加

  • ユーザー一覧画面にフォロー数・フォロワー数・フォローする・外すボタンの設置

  • フォロー・フォロワー一覧画面を作成すること

モデル

テーブルの設計

今回作成するフォロー・フォロワー機能について考える。

Aさん(user)がBさんを(user)をフォローしているとすると、

Aさんからみて、Bさんをフォローしている(follow)。

Bさんからみて、Aさんにフォローされている(followed)。

上記のような関係性になる。

そして、あるユーザーをフォローしている全ての集合体をfollowers、

あるユーザーにフォローされている全ての集合体をfollowingとする。

この関係性を完結に表すと、

followers(user) : following(user) → 多: 多

の関係性になる。

ER図で表すと下記のようになる。

relationshipsテーブルを設計すると下記のようになる。

カラム名 データ型 カラムの説明
id integer relationshipのID
follower_id integer フォローしているユーザーのID
followed_id integer フォローされているユーザーのID
モデル・テーブルの作成

テーブルの設計ができたので、テーブルを作成していく。

$ rails g model Relationship follower_id:integer followed_id:integer

今回は実装しないが、頻繁にfollower_id、followed_idを使用し、検索をかける場合やデータ量が多い場合はインデックスを追加する。

また、あるユーザーが同じユーザーを2回以上フォローできないように、複合キーインデックスを使用して、一意性を保証させたりする記述も実際に運用する上では必要となる。

参考URL 第12章 ユーザーをフォローする - Railsチュートリアル

作成されたマイグレーションファイルをデータベースに反映させる。

$ rails db:migrate

上記でモデル・テーブルの準備は完了。

関係性(アソシエーション)の記述

follower、followedはどちらもUsersテーブルのため、リレーションの書き方もコメント機能などの時とは変わってきます。

モデルに下記のように記述していく。

app/models/relationship.rb

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

belongs_to :followerだけの記述だと存在しないfollowersテーブルを参照してしまうため、

class_name : "User"を記述することで、usersテーブルに参照していくように記述する。

app/models/user.rb

:
  # 自分がフォローする側の関係
  has_many :relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
  # 与フォロー関係を通じて参照→自分がフォローしている人
  has_many :following, through: :relationships, source: :followed
  # 自分がフォローされる側の関係
  has_many :reverse_of_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
  # 被フォロー関係を通じて参照→自分をフォローしている人
  has_many :followers, through: :reverse_of_relationships, source: :follower
:

上記のように記述し、フォロー・フォロワーのアソシエーションの記述は完了です。

メソッドの記述

user.rbにメソッドを記述し、コントローラで記述が簡潔になるようにしていきます。

app/models/user.rb

:
  # フォローするとき
  def follow(user)
    relationships.create(followed_id: user.id)
  end
  
  # フォローを外すとき
  def unfollow(user)
    relationships.find_by(followed_id: user.id).destroy
  end

  # フォローしているか確認するとき
  def following?(user)
    following.include?(user)
  end
:

follow(user)で引数で渡したユーザーをフォローするメソッド。

unfollow(user)で引数で渡したユーザーのフォローを外すメソッド。

followings?(user)で引数で渡したユーザーをフォローしているか確認するメソッド。フォローしていればtrue、していなければfalseを返す。

コントローラの作成

relationshipsコントローラの作成
$ rails g controller relationships
アクションの記述

relationshipsコントローラにアクションを記述していく。

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  # フォロー追加機能
  def create
    user = User.find(params[:user_id])
    current_user.follow(user)
    redirect_to request.referer
  end
  # フォロー削除機能
  def destroy
    user = User.find(params[:user_id])
    current_user.unfollow(user)
    redirect_to request.referer
  end
  # フォロワー一覧表示
  def followers
    user = User.find(params[:user_id])
    @users = user.followers
  end
  # フォロー一覧表示
  def following
    user = User.find(params[:user_id])
    @users = user.following
  end
end

ルーティング

ルーティングの記述

config/routes.rb

  resources :users, only: [:index, :show, :update, :edit] do
    resource :relationships, only: [:create, :destroy]
    get "followers" => "relationships#followers", as: "followers"
    get "following" => "relationships#following", as: "following"
  end

コントローラのアクションでURLからuser_idを取得するため、usersにネストした記述を行う。

URLに/:idを含む必要がないため、resourceで記述。

ルーティングの確認
$ rails routes

ビュー

ビューは、フォローの追加・削除ボタンを表示させる部分テンプレート_button.html.erbを作成し、表示させる部分に記述を追加する。

また、フォロワーの一覧表示としてfollowers.html.erb、フォローの一覧表示としてfollowing.html.erbを作成し、記述する。

フォローの追加・削除ボタンの部分テンプレートの作成

views/relationshipsフォルダに_button.html.erbの部分テンプレートファイルを作成する。

app/views/relationships/_button.html.erb

<% if current_user.following?(user) %>
  <%= link_to "フォローを外す", user_relationships_path(user.id), method: :delete, class: "btn btn-info" %>
<% else %>
  <%= link_to "フォローする", user_relationships_path(user.id), method: :post, class: "btn btn-success" %>
<% end %>

ログインユーザーがフォローしていない場合、フォローするボタンを表示、

フォローしている場合、フォローを外すボタンを表示。

サイドバーにフォロー、フォロワー数の表示、フォローボタンの追加

ユーザー情報のサイドバー部分、users/_info.html.erbに記述を追加する。

app/views/users/_info.html.erb

<h2>User info</h2>
<table class="table">
  <tbody>
    <tr><%= image_tag user.get_profile_image(100,100) %></tr>
    <tr>
      <th>name</th>
      <th><%= user.name %></th>
    </tr>
    <tr>
      <th>introduction</th>
      <th><%= user.introduction %></th>
    </tr>
    # ここから追加
    <tr>
      <th>follows</th>
      <th><%= link_to user.following.count, user_following_path(user) %></th>
    </tr>
    <tr>
      <th>followers</th>
      <th><%= link_to user.followers.count, user_followers_path(user) %></th>
    </tr>
    # ここまで
  </tbody>
</table>

<div class="row">
  # ここから追加
  <% if current_user == user %>
  # ここまで
    <%= link_to edit_user_path(user), class: "btn btn-block btn-outline-secondary" do %>
      <i class="fas fa-user-cog"></i>
    <% end %>
  # ここから追加
  <% else %>
    <%= render 'relationships/button', user: user %>
  <% end %>
  # ここまで
</div>
ユーザー一覧画面にフォロー・フォロワー数の追加、フォロー追加・削除リンクを表示

ユーザー一覧画面のusers/indexに記述を追加。

app/views/users/index.html.erb

:
<tbody>
  <% @users.each do |user| %>
    <tr>
      <td><%= image_tag user.get_profile_image(80,80) %></td>
      <td><%= user.name %></td>
      # ここから追加
      <td>フォロー数: <%= user.following.count %></td>
      <td>フォロワー数: <%= user.followers.count %></td>
      <td>
        <% if current_user != user %>
          <% if current_user.following?(user) %>
            <%= link_to "フォローを外す", user_relationships_path(user.id), method: :delete %>
          <% else %>
            <%= link_to "フォローする", user_relationships_path(user.id), method: :post %>
          <% end %>
        <% end %>
      </td>
      # ここまで
      <td>
        <%= link_to "Show", user_path(user.id) %>
      </td>
    </tr>
  <% end %>
</tbody>
:
フォロー一覧表示、フォロワー一覧表示

フォロー一覧とフォロワー一覧のビューを作成する。

users/indexのuser一覧部分と表示記述を同じにできるため、usersテーブルに_index.html.erb部分テンプレートを作成する。

app/views/users/_index.html.erb

<table class="table">
  <thead>
    <tr>
      <th>image</th>
      <th>name</th>
      <th colspan="4"></th>
    </tr>
  </thead>
  <tbody>
    <% users.each do |user| %>
      <tr>
        <td><%= image_tag user.get_profile_image(80,80) %></td>
        <td><%= user.name %></td>
        <td>フォロー数: <%= user.following.count %></td>
        <td>フォロワー数: <%= user.followers.count %></td>
        <td>
          <% if current_user != user %>
            <% if current_user.following?(user) %>
              <%= link_to "フォローを外す", user_relationships_path(user.id), method: :delete %>
            <% else %>
              <%= link_to "フォローする", user_relationships_path(user.id), method: :post %>
            <% end %>
          <% end %>
        </td>
        <td>
          <%= link_to "Show", user_path(user.id) %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

users/indexのビューを部分テンプレートに変更。

app/views/users/index.html.erb

:
    <!--ユーザー一覧表示-->
    <div class="col-md-8 offset-md-1">
      <h2>Users</h2>
      <%= render 'index', users: @users %>
    </div>
:

フォロー一覧(relationships/following)とフォロワー一覧(relationships/followers)のビューを作成する。

app/views/relationships/following.html.erb

<h2>Follow Users</h2>
<% if @users.exists? %>
  <%= render 'users/index', users: @users %>
<% else %>
  <p>ユーザーはいません</p>
<% end %>

app/views/relationships/following.html.erb

<h2>Follower Users</h2>
<% if @users.exists? %>
  <%= render 'users/index', users: @users %>
<% else %>
  <p>ユーザーはいません</p>
<% end %>

exists?メソッド (オブジェクト.exists?(条件))

指定した条件のレコードが存在するか確認するメソッド。存在する場合trueを返し、存在しない場合falseを返す。

今回は特に条件は指定していないため、@usersにレコードが存在すればtrue、レコードが存在しなければfalseを返す。

以上で、フォロー・フォロワー機能の実装が完了。

いいね機能

いいね機能の実装

今回も以前に作成したBookers2のアプリケーションにいいね機能を実装していく。

表示内容

  • 投稿した本にいいねする機能を追加。

  • いいねは、一つの本に対して一人一回まで、もう一度押すといいねが消える。

  • 本の一覧機能と本の詳細画面に表示する。

実装機能

コントローラ

  • favoritesコントローラの作成

  • createアクションの作成(いいねをする機能)

  • destroyアクションの作成(いいね削除機能)

モデル

  • favoriteモデルの作成 (用途:ユーザーと投稿のセットでいいねをしている状態とする)

  • ユーザーは一つの投稿に一つしかいいねできないこと

ビュー

  • 投稿一覧画面にいいね数, いいね(する, 外す)ボタンを追加

  • 投稿詳細画面にいいね数, いいね(する, 外す)ボタンを追加

  • いいねされていない投稿に対しては、いいね作成ボタンを表示させる

  • いいねされている投稿に対しては、いいね削除ボタンを表示させる

モデル

いいね機能のテーブルの設計

いいね機能のテーブル、favoritesテーブルについて設計していく。

いいねテーブルには、いいねのID、いいねした人のuser_id、いいねされた本のbook_idが必要である。

カラム名 データ型 カラムの説明
id integer いいねのID
user_id integer いいねした人のID
book_id integer いいねされた本のID
モデルの作成

Favoriteモデルを作成する。

$ rails g model Favorite user_id:integer book_id:integer

作成されたマイグレーションファイルをデータベース上に反映させる。

$ rails db:migrate
関係性(アソシエーション)の記述

Userモデル、Bookモデル、Favoriteモデルの関係性をER図で表すと下記のようになる。

UserモデルとFavoriteモデル、BookモデルとFavoriteモデルの関係性をそれぞれ考えると、下記のようになる。

Userモデル:Favoriteモデル → 1:多

Bookモデル:Favoriteモデル → 1:多

上記を踏まえてモデルに関係性の記述をしていく。

app/models/user.rb

:
  has_many :favorites, dependent: :destroy
:

app/models/book.rb

:
  has_many :favorites, dependent: :destroy
:
  def favorited_by?(user)
    favorites.where(user_id: user.id).exists?
  end
:

favorited_by?メソッド

引数で渡されたuser_idがFavoritesテーブル内に存在(exists?)するかどうかを調べるメソッド

存在していればtrue、存在していなければfalseを返す。

app/models/favorite.rb

:
  belongs_to :user
  belongs_to :book
:

これでモデルの記述が完了。

コントローラの作成

Favoritesコントローラを作成する。

$ rails g controller favorites

コントローラの作成が完了。

ルーティングの記述

いいね機能は、いいね作成機能といいね削除機能を実装するため、createアクションとdestroyアクションのルーティングを記述していく。

いいねは投稿した本に対して行われるため、favoritesはbooksに結びつき、下記のようにネストする記述を行います。

config/routes.rb

:
  resources :books, only: [:index, :create, :show, :edit, :update, :destroy] do
    resources :book_comments, only: [:create, :destroy]
    resource :favorites, only: [:create, :destroy]
  end
:

いいね機能では、resourcesではなくresourceを使用している。単数形にすることで、/:idがURLに含まれなくなります。

これは、いいね機能が「一つの投稿に一つしかいいねできない」という条件のため、いいねのIDを取得しないでも、

ユーザーのIDといいねした本のIDを取得できれば削除機能を使用できるためである。

このように、resourceは「それ自身のidが分からなくても、関連する他のモデルのidから特定できる」といった場合に用いることが多いです。

ルーティングを確認すると下記のようになる。

$ rails routes

上記でルーティングの記述が完了。

いいね機能の作成

コントローラにcreateアクションとdestroyアクションを作成し、記述していく。

app/controllers/favorites_controller.rb

class FavoritesController < ApplicationController
  def create
    book = Book.find(params[:book_id])
    favorite = current_user.favorites.new(book_id: book.id)
    favorite.save
    redirect_to request.referer
  end

  def destroy
    book = Book.find(params[:book_id])
    favorite = current_user.favorites.find_by(book_id: book.id)
    favorite.destroy
    redirect_to request.referer
  end
end
ビューにいいね欄の作成

いいね機能の部分テンプレートの作成

app/views/favorites/_favorite.html.erb

<% if book.favorited_by?(current_user) %>
  <%= link_to book_favorites_path(book), method: :delete,  style: "color: red;" do %>
    <i class="fas fa-heart" aria-hidden="true"></i>
    <%= book.favorites.count %>
  <% end %>
<% else %>
  <%= link_to book_favorites_path(book), method: :post do %>
    <i class="fas fa-heart" aria-hidden="true"></i>
    <%= book.favorites.count %>
  <% end %>
<% end %>

上記の部分テンプレートをビューに追加していく。

app/views/books/_index.html.erb

:
<td>
  <%= render 'favorites/favorite', book: book %>
</td>
:

app/views/books/show.html.erb

:
<td>
  <%= render 'favorites/favorite', book: @book %>
</td>
:

いいね機能の実装が完了。

以上。

コメント機能の実装

コメント機能の実装

今回は、以前に作成したBookers2というアプリケーションにコメント機能を実装していく。

使用するアプリケーションについて参考URL 作成するアプリケーションについて - takifugu’s blog

コメント機能の作成

コメント機能を実装していく。

手順は下記の流れで作業していく。

  1. コメントのモデルとテーブルの設計と作成。

  2. 関係性(アソシエーション)の記述

  3. コメントのコントローラ作成

  4. ルーティングの記述

  5. コントローラにコメント作成のcreateアクション記述

  6. ビューにコメント投稿欄の作成

  7. コントローラにコメント削除のdestroyアクションの記述

  8. ビューに削除ボタンの実装

コメントのモデルとテーブルの設計と作成

コメント保存用のテーブルには、「コメントの内容」「どの本の投稿にコメントするか。」「誰がコメントしたか。」を定義する必要がある。

このため、comment(コメントの内容)、book_id(どの本へのコメントか)、user_id(コメントした人)のカラムを持たせる必要がある。

カラム名 データ型 カラムの説明
id integer コメントのID
comment text コメントの内容
book_id integer コメントする本のID
user_id integer コメントした人のID

テーブルの設計が完了したので、book_commentモデルを作成する。

$ rails g model BookComment comment:text book_id:integer user_id:integer

データベースへマイグレーションする。

$ rails db:migrate

関係性(アソシエーション)の記述

Userモデル、Bookモデル、BookCommentモデルの関係性をER図で表すと下記のようになる。

UserモデルとBookCommetnモデル、BookモデルとBookCommentモデルの関係性をそれぞれ考えると、下記のようになる。

Userモデル:BookCommentモデル → 1:多

Bookモデル:BookCommentモデル → 1:多

上記を踏まえてモデルに記述をしていく。(アソシエーション部分を抜粋)

app/models/user.rb

  has_many :books, dependent: :destroy
  has_many :book_comments, dependent: :destroy

app/models/book.rb

  belongs_to :user
  has_many :book_comments, dependent: :destroy

app/models/book_comment.rb

  belongs_to :user
  belongs_to :book

これでモデルの準備が完了しました。

コメントのコントローラ作成

book_commentコントローラを作成していきます。

$ rails g controller book_comments

ルーティングの記述

コメント機能は、コメント作成機能とコメント削除機能を実装するため、createアクションとdestroyアクションのルーティングを記述していく。

コメントは投稿した本に対して行われるため、book_commetnsはbooksに結びつき、下記のようにネストする記述を行います。

config/routes.rb(追加部分を抜粋)

  resources :books, only: [:index, :show, :edit, :create, :destroy, :update] do
    resources :book_comments, only: [:create, :destroy]
  end
ルーティングの確認
$ rails routes

コメント投稿機能の実装

book_commentコントローラにコメント作成機能のアクションを記述する。

app/controllers/book_controller.rb

def create
    book = Book.find(params[:book_id])
    comment = current_user.book_comments.new(book_comment_params)
    comment.book_id = book.id
    comment.save
    redirect_to request.referer
  end

private
  def book_comment_params
    params.require(:book_comment).permit(:comment)
  end

※redirect_to request.refererについて

Referer リクエストヘッダーには、現在リクエストされているページへのリンク先を持った直前のウェブページのアドレスが含まれています。

そのため、上記のような記述をすることで、直前のページにredirectすることができます。

bookコントローラのshowアクションに記述の追加

app/controllers/books_controller.rb

  def show
    @book = Book.find(params[:id])
    @book_comment = BookComment.new     #追加
  end

ビューにコメント投稿欄の作成

createアクションを記述したので、book_commentsフォルダに、コメント表示部分をindex.html.erb、コメント作成部分をform.html.erbとして部分テンプレートを作成する。

そして、books/show.html.erbにコメント作成機能、コメントの一覧表示を記述する。

app/views/book_comments/_form.html.erb

<%= form_with(model:[book, book_comment], local: true) do |f| %>
  <%= f.text_area :comment, rows:'5',placeholder: "コメントをここに", class: "w-100" %>
  <%= f.submit "送信" %>
<% end %>

book_commentモデルはbookモデルにネストしているため、

book_commentモデルクラスのインスタンスにデータを保存するときは、form_withにbookとbook_commentの2つの引数を渡す必要がある。

rows: '5'でコメント投稿欄を5行に指定。

class: "w-100"で横幅100%というBootstrapを使用した記述。

app/views/book_comments/_form.html.erb

<table>
  <tbody>
      <% book.book_comments.each do |book_comment| %>
      <tr>
        <td>
          <%= link_to user_path(book_comment.user) do %>
            <%= image_tag book_comment.user.get_profile_image(50,50) %><br>
            <%= book_comment.user.name %>
          <% end %>
        </td>
        <td><%= book_comment.comment %></td>
        <td>
        </td>
      </tr>
      <% end %>
  </tbody>
</table>

app/views/books/show

:
      </table>
      <%= render "book_comments/index", book: @book %>
      <%= render "book_comments/form", book: @book, book_comment: @book_comment %>
:

上記でコメント投稿機能の作成が完了。

コメント削除機能の実装

次にコメント削除機能を実装していく。

destroyアクションの記述

book_commentsコントローラにdestroyアクションを記述していく。

app/controller/book_comments_controller.rb

:
  def destroy
    BookComment.find_by(id: params[:id], book_id: params[:book_id]).destroy
    redirect_to request_referer
  end
:

BookComment.find_by(id: params[:id], book_id: params[:book_id]).destroy

URLから、IDとコメントをした本のIDを取得し、削除するコマンド。

削除ボタンの作成

book_commentsファイルの_index部分テンプレートのテーブルの要素に削除ボタンの追加。

自分が投稿したコメントのみに削除ボタンが表示されるようにする。

app/views/book_comments/_index.html.erb

:
<td>
  <% if book_comment.user == current_user %>
    <%= link_to "Destroy", book_book_comment_path(book, book_comment), method: :delete, class: "btn btn-sm btn-danger", "data-confirm" => "本当に消しますか?" %>
  <% end %>
</td>
:

上記でコメント削除機能の作成が完了。

コメント件数の表示

ユーザーの詳細画面、本の一覧画面、本の詳細画面に登録されている本にコメント数を表示させていく。

本の詳細画面のテーブル要素に下記記述を追加。

app/views/books/show.html.erb

:
<td>
  コメント数:<%= @book.book_comments.count %>
</td>
:

ユーザー詳細画面と本の一覧画面は、booksフォルダの_index部分テンプレートに記述を追加。

部分テンプレートのため、@はつけない。

app/views/books/_index.html.erb

:
<td>
  コメント数:<%= book.book_comments.count %>
</td>
:

上記で、コメント件数の表示が完了。

以上。

他人のユーザー編集画面、本の編集画面に遷移できないようにする

他人のユーザー編集画面、本の編集画面に遷移できないようにする

現状の作成したアプリケーションでは、他のユーザー編集画面、他のユーザーが投稿した本の編集画面に遷移することができてしまいます。

そのため、ログインユーザー以外が編集画面に遷移した際に、別の画面に遷移するように設定していきます。

他のユーザーの編集画面に遷移できないようにする

まず、ユーザー編集画面への遷移から設定を変更していきます。

他のユーザー編集画面に遷移しようとすると、自分のユーザー詳細画面に遷移するように設定します。

usersコントローラに記述していきます。

app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :is_matching_login_user, only: [:edit, :update]
:
  private
:
  def is_matching_login_user
    user = User.find(params[:id])
    unless user.id == current_user.id
      redirect_to user_path(current_user)
    end
  end

is_matching_login_userというメソッドを定義し、画面のuser_IDを取得し、ログインしているユーザーと同一のユーザーであるか確認している。

異なっていた場合、redirect_toでログインユーザーの詳細画面に遷移するようにしている。

また、定義したメソッドは、before_actionでeditアクションとupdateアクションが行われる前に、実行されるように記述した。

※ before_actionとは、コントローラーで各アクションを実行する前に実行したい処理を指定することができるメソッドです。

指定したい処理は、今回のようにメソッドとしてまとめることで、使用することができます。

他のユーザーが投稿した本の編集画面に遷移できないようにする

次に、他のユーザーが投稿した本の編集画面に遷移できないように設定していく。

booksコントーラーに記述していく。

app/controllers/books_controller.rb

class BooksController < ApplicationController
  before_action :ensure_correct_user, only: [:edit, :update]
:
private
:
  def ensure_correct_user
    book = Book.find(params[:id])
    unless book.user_id == current_user.id
      redirect_to books_path
    end
  end
end

ensure_correct_userというメソッドを定義し、URLのIDを取得し、そのIDを持つbookの投稿ユーザーとログインユーザーが同一人物であるか、確認している。

異なっていた場合、本の一覧表示画面に遷移するようになっている。

以上。

アプリケーションの作成 参考URL  作成するアプリケーションについて - takifugu’s blog