Rails お気に入り機能　解説

Rails お気に入り機能　手順　（自分用）

#中間モデルの作成
・UserモデルとBoardモデルの中間モデル（Bookmarkモデル）を作成する。
・中間モデル（Bookmarkモデル）を作成することによってuser_idとboard_idを抱えることができ、お気に入り機能を追加するに当たって便利になる。

###Bookmarkモデルの作成

```:ターミナル
$ rails g model Bookmark user:references board:references
```
・user_idとboard_idの外部キーが欲しいので、「user:references board:references」を付ける。

```rb:マイグレーションファイル
class CreateBookmarks < ActiveRecord::Migration[5.2]
  def change
    create_table :bookmarks do |t|
      t.references :user, foreign_key: true
      t.references :board, foreign_key: true

      t.timestamps
      t.index [:user_id, :board_id], unique: true　#追記
    end
  end
end
```

```rb:
 t.index [:user_id, :board_id], unique: true
```
・ユニーク制約（unipue）を付けることによって、DBにおいてデータ一を追加する際の制約に一意性制約（１つしか存在しないこと）を付けることができる。
・例えば「user_id/1」と「board_id/2」の組み合わせはひとつしかなく、２つ作ろうとしたらエラーを返す。という制約を作れる。これで「user_id/1」のユーザーが「board_id/2」に１度しかお気に入りすることができなくなる。

```rb:app/models/bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board
  validates :user_id, uniqueness: { scope: :board_id } #追記
end
```

```rb:追記文
validates :user_id, uniqueness: { scope: :board_id }
```
・「１ユーザー」は「１投稿」に「１お気に入り」しかできないというバリデーション。
・「validates :user_id, uniqueness」だけだと、１ユーザー（user_id）は１お気に入りしかできないということになって位しまうので、「scope」を付けることによって範囲を指定することができる。
・ { scope: :board_id }を加えることでBookmarkモデルでレコードを作る際に、特定のboard_idに対しては特定のuser_idは２つ以上入れることができない。例えば、Bookmarkモデルでレコードを作る際に、１つの投稿に対して、お気に入りをするのであらかじめ、「board_id」カラムにはすでに2が入っているとすると（下記の図）「user_id/3」が「board_id/2」に対してお気に入りしようとしても、すでに「id/1」のレコードで一度お気に入りしているので、お気に入りできない。→user_idとboard_idの組み合わせで被りをなくす。

#####Bookmarkモデルのテーブル
| id | user_id | board_id |
|:---|--------:|:--------:|
|  1 |	   ３   |     2    |
|２　||２|

#UserモデルとBoardモデルにアソシエーション（関連付け）をする

```rb:user.rb
has_many :boards, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy　#追記
  has_many :bookmark_boards, through: :bookmarks, source: :board　#追記
```

```rb:追記分
has_many :bookmarks, dependent: :destroy
```
・userは多くのbookmaksを持っている。（has_many :bookmarks）
・１つのuserが削除されたらそのuserが持っているbookmarksも削除される。（dependent: :destroy）

```rb:追記分
has_many :bookmark_boards, through: :bookmarks, source: :board
```
・「has_many :bookmark_boards」でUserモデルは多くのbookmark_boardsを持っている。
・ 「through: :bookmarks」bookmarksモデルを通して、
・「source: :board」boradモデルからデータを参照する。
＊これはお気に入り一覧を取得するときに使う。
＊has_many throuth オプションをつけたことによって、Userクラスのインスタンスメソッド「bookmark_boards」が定義されるようになる。

```rb:boards_controller
  def bookmarks
    @bookmarks_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)#後で定義する
  end
```
・currrent_userにuser.rbに設定したbookmark_boardsメソッドを使うことで、一覧を取得する際に中間テーブルbookmarksのuser_idを見に行き、user_idが１のレコード（current_userのidが１とする）を持っているboard_idを取得することができるようになる。下の図だと、id1,3,5のレコードを取得できる。
・これにより、ログインしているユーザー（current_user）のお気に入り一覧を取得できる。（中間テーブルbookmarksにはuser_idとboard_idの組み合わせでお気に入りを作っているから）

#####Bookmarkモデルのテーブル
| id | user_id | board_id |
|:---|--------:|:--------:|
|  1 |	   1  |     2    |　　　　 
|2|3|１|
|3|1|1|　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　
|4|2|3|
|5|1|4|　

#ルーティングの設定

```rb:routes.rb
resources :users, only: %i[new create]
  resources :boards do
    resources :comments, only: %i[create update destroy], shallow: true
    collection do #ここから追記
      get :bookmarks 
    end #ここまで
  end
  resources :bookmarks, only: %i[create destroy] #追記
```

```rb:追記分
collection do #ここから追記
      get :bookmarks 
    end #ここまで
```

・お気に入り一覧の「boards#bookmarks」を作るためのルーティング。「collection do」を付けることによって、親のidをパスに含めないようにできる。お気に入り一覧はuser.rbに設定した「bookmark_boards」メソッドを使ってこれからboardsコントローラーに＠bookmarks_boardsインスタンス変数を作るので深いネストをしてなくても良い。

```rb:追記分
resources :bookmarks, only: %i[create destroy] #追記
```
・お気に入りを作成、削除する際も
・

#bookmarksのコントローラーを生成

```:ターミナル
$ rails g controller bookmarks create destroy
```

お気に入り機能はcreate(作成)、destroy(削除)だけ作る。

#Userモデルにインスタンスメソッドを定義する

```rb:user.rb
  def bookmark(board)　
    bookmark_boards << board
  end

  def unbookmark(board)
    bookmark_boards.destroy(board)
  end

  def bookmark?(board)
    bookmark_boards.include?(board)
  end
```

```rb:
  def bookmark(board)
    bookmark_boards << board
  end
```
・引数で渡したboardレコードを中間テーブルに自動的に保存している。この中でsaveメソッドも自動で行っているので記述する必要はない。

```rb:
  def unbookmark(board)
    bookmark_boards.destroy(board)
  end
```
・引数で渡したboardレコードを中間テーブルからdestroy(削除)する。

```rb:
  def bookmark?(board)
    bookmark_boards.include?(board)
  end
```
・お気に入りしているかどうかを判断するメソッド
・include?で引数で渡したboardがbookmark_boardsに含まれているのかを判断し、含まれていればtrue含まれていなければfalseを返す。

#bookmarksコントローラーにcreateとdestroyを作成

```rb:boards_controller.rb
class BookmarksController < ApplicationController
  def create
    board = Board.find(params[:board_id])
    current_user.bookmark(board)
    redirect_back fallback_location: root_path, success: t('defaults.message.bookmark')
  end

  def destroy
    board = current_user.bookmarks.find(params[:id]).board
    current_user.unbookmark(board)
    redirect_back fallback_location: root_path, success: t('defaults.message.unbookmark')
  end
end
```

```rb:
board = Board.find(params[:board_id])
```
・Boardテーブルからパスで送られてくる（/boards/:board_id/comments）のboard_idのレコードを取得して「board」に代入している。

```rb:
current_user.bookmark(board)
```
・userモデルに定義した「current_user.bookmark(board)」メソッドを使っている。引数のboardには上記で定義したboardレコードが入っていてそこからboard_idを取得。current_userでuser_idを取得し、中間テーブルに保存している。＊<<を使っていることでsaveを使わずに保存することができている。

```rb:
board = current_user.bookmarks.find(params[:id]).board
```
・current_userからuser_idを取得し、パスから送られてくるbookmarksテーブルのid(/bookmarks/:id)を取得。boardからboard_idを取ってきて変数boardに代入。これで中間テーブルに保存されているレコードのuse_id（ログインしているユーザー）とboard_id（コメントしている投稿）が一致していることを確認している・

```rb:
current_user.unbookmark(board)
```
・userモデルで定義した「unbookmark(board)」を使用。
・current_userのuser_idを取得して中間テーブルbookmarksのuser_id（current_userで取得したuser_id）を見に行き、上記で取得したbookmarkレコード（board）と照らし合わせて一致しているものを削除。

#ユーザーのお気に入り一覧表示のためのbookmarksアクションを追加

```rb:boards_controller.rb
def bookmarks
    @bookmark_boards = current_user.bookmark_boards.includes(:user).order(created_at: :desc)
  end 
```
・一覧を表示する際にN+1問題を起こさないために、include(user)で関連するユーザー情報も取得。

#ビューの設定①お気に入りボタン

boards/_board.html.erb　→ 投稿がログインしているユーザーのものかを判断するファイル。

```rb:app/views/boards/_board.html.erb
<% if current_user.own?(board) %>
　 <%= render 'crud_menus', board: board %>
<% else %>
     <%= render 'bookmark_button', board:board %>
<% end %>
```
・userモデルに定義したown?(object)メソッドを使用している。
・引数で渡したobject(board)のuser_idとcurrent_userのuser_idが一致しているのかを検証。
・一致していれば、パーシャルの「crud_menus（編集と削除ボタン）」をレンダー。
一致していなければ、パーシャルの「bookmark_button（お気に入りボタン）」をレンダー。
・これで、ログインしているユーザーの投稿ならお気に入りボタンを表示させず、編集と削除ボタンを表示させ、ログインしているユーザーの投稿でなければ、お気にボタンを表示させることができている。

```rb:user.rb
 def own?(object)
    id == object.user_id
 end
```

#パーシャルのbookmark_buttonを作成

```rb:app/views/boards/_bookmark_button.html.erb
<% if current_user.bookmark?(board) %>
  <%= render 'unbookmark', { board: board } %>
<% else %>
  <%= render 'bookmark', { board: board } %>
<% end %>
```
・userモデルに定義した「bookmark?(board)」を使用している。

```rb:user.rb
def bookmark?(board)
    bookmark_boards.include?(board)
end
```
・current_userがお気に入りしていたら、パーシャルの「unbookmark（お気に入り解除ボタン）」をレンダー。
・current_userがお気に入りしていなければ、パーシャルの「bookmark（お気に入りボタン）」をレンダー。
＊bookmark_bordsを通して引数で渡されたboardレコードのboard_idを取得。include?で、この投稿はbookmark_boardsに含まれているかを確認。含まれていればtrue含まれていなければ、falseを返す。current_userをつけることで中間テーブルにboard_idとuser_idに同じ組み合わせがないか確認し、なければfalse(render 'bookmark')同じ組み合わせがあればtrue(render 'umbookmark')。→ログインしているユーザーが１投稿をお気に入りしているかしてないかを検証している。


```rb:app/views/boards/_unbookmark.html.erb
<%= link_to bookmark_path(current_user.bookmarks.find_by(board_id: board.id)), id: "js-bookmark-button-for-board-#{board.id}", class:"float-right", method: :delete do %>
  <%= icon 'fas', 'star' %>
<% end %>
```

・link_to bookmark_pathで「bookmarks#destroy」にデータを送っている。
・「(current_user.bookmarks.find_by(board_id: board.id))」でcurrent_user（ログインしているユーザー）のからusre_idを取得し、board_idにはパスから受け取れるパラメーターでその投稿のidを入れている。これを引数に「bookmarks#destroy」にデータを送っている。
・method: :deleteを指定することで、HTTPメソッドを指定し、削除を行う。
＊link_toで、パス（bookmark_path）にパラメータ（(current_user.bookmarks.find_by(board_id: board.id))）を付与することができる。

```rb:app/views/boards/_bookmark.html.erb
<%= link_to bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", class:"float-right", method: :post do %>
  <%= icon 'far', 'star' %>
<% end %>
```
・link_to bookmarks_pathで「bookmarks#create」にデータを送っている。
・「(board_id: board.id)」でbookmarksテーブルのboard_idにその投稿のidを入れている。これを引数に「bookmarks#create」へデータを送っている。
・method: :postを指定することで、HTTPメソッドを指定し、削除を行う。
＊link_toで、パス（bookmarks_path）にパラメータ（(board_id: board.id)）を付与することができる。

#お気に入り一覧画面

```rb:app/views/boards/bookmarks.html.erb
<% content_for(:title, t('.title')) %>
<div class="container pt-3">
  <div class="row">
    <div class="col-lg-10 offset-lg-1">
      <!-- 検索フォーム -->
      <form>
        <div class="input-group mb-3">
          <input class="form-control" placeholder="検索ワード" type="search" />
          <div class="input-group-append">
            <input type="submit" value="検索" class="btn btn-primary" />
          </div>
        </div>
      </form>
    </div>
  </div>

  <!-- 掲示板一覧 -->
  <div class="row">
    <div class="col-12">
      <div class="row">
        <% if @bookmark_boards.present? %>
          <%= render @bookmark_boards %>
        <% else %>
          <p><%= t('.no_result') %></p>
        <% end %>
      </div>
    </div>
  </div>
</div>
```
・boardsコントローラーで作成した、インスタンス変数「@bookmark_boards」をrenderしている。
・「<% if @bookmark_boards.present? %> 」→「present?」は＠bookmark_boardsに中身があるかを検証している。中身があればtrue,なければfalseを返す。