<<演算子を使って、掲示板にブックマーク(いいね)機能実装手順

##実装したいこと
・掲示板の☆ボタンを押すと、その掲示板をブックマーク/解除出来る機能。
・ユーザーがブックマークした掲示板を一覧できるページを実装。

##お気に入り機能実装の流れ
①中間テーブルとなるBookmarkモデルの実装
②Userモデルにbookmarkの定義を追加
③Bookmarksコントローラーの実装
④Boardコントローラーにbookmarkの定義を追加
⑤Routingの設定
⑥Viewの実装

##①中間テーブルとなるBookmarkモデルの実装
###Bookmarkモデルの仕組み（多対多）
**UserとBookmarkとBoardの関係**
Bookmarkモデルを実装する前に、モデルの関係を確認する。

・ユーザーはたくさんの掲示板をブックマークすることができる。
・反対に、掲示板はたくさんのユーザーにフォローされることができる。

つまり、UserもBoardもBookmarkをたくさん持っているということになる。このような関係を``多対多の関係``と言う。


[![Image from Gyazo](https://i.gyazo.com/a55b8f29b19372ee51315422b7347933.png)](https://gyazo.com/a55b8f29b19372ee51315422b7347933)

###中間モデル（Bookmarkモデル）の作成
多対多のモデルを実装するにはお互いのforeign_keyを知っている必要がある。

```ターミナル.
rails g model Bookmark user:references board:references
```
```db/migrateファイル.rb
class CreateBookmarks < ActiveRecord::Migration[6.0]
  def change
    create_table :bookmarks do |t|
      t.references :user, null: false, foreign_key: true
      t.references :board, null: false, foreign_key: true

      t.timestamps
    end
    add_index :bookmarks, [:user_id, :board_id], unique: :true
  end
end
```

``add_index :bookmarks, [:user_id, :board_id], unique: :true``でuser_idとboard_idの組み合わせがuniqueであることを設定。
▶︎[migrationファイルのadd_indexは何なのか](https://qiita.com/mmaumtjgj/items/9f4fca618d2f395f985e)

boardモデルとuserモデルにも追加記入。

```model/board.rb
#この一文を追加
has_many :bookmarks, dependent: :destroy
```

```model/user.rb
#以下2文を追加
has_many :bookmarks, dependent: :destroy
 
# ↓お気に入りにしている掲示板を取得する
has_many :bookmarks_boards, through: :bookmarks, source: :board
```
``throughオプション``によりbookmarkテーブル経由でboardテーブルにアクセスできるようになる。
多対多のモデルを作った時に必ずと言っていいほど活躍するオプションである。

>◆has_many :through　とは
多対多で別のモデルと関連している
従属している第3のモデル（結合モデル）を介して、対象のモデルと多対多の関連付けになっている

>◆source　とは
has_many :through関連付けの関連付け元（従属するモデル）名を指定する

またユーザーが同じ掲示板をお気に入り登録しないように``unique: :true``をつける必要がある。


```model/bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board

  validates :user_id, uniqueness: { scope: :board_id}
end
```
``scope``をつけることによって、１投稿に対して、１人のユーザーが１ブックマーク（いいね）しかできなくする。
▶︎[【uniqueness: scopeの使い方】ブックマーク、いいね機能実装に使える](https://qiita.com/mmaumtjgj/items/b8b8509328274c324f1e)

##②Userモデルにbookmarkの定義を追加
controllerの可読性を上げるためにまずは**モデルに**お気に入り登録のギミックを定義する。

```model/user.rb
  # お気に入り追加
  # <<で引数で渡した掲示板の情報がbookmark_boardsに入っている
  def bookmark(board)
    bookmarks_boards << board
  end

  # お気に入りを外す
  def unbookmark(board)
    bookmarks_boards.delete(board)
  end

  # お気に入り登録しているか判定するメソッド
  def bookmark?(board)
    bookmarks_boards.include?(board)
  end
```

**・bookmarkメソッド**
掲示板の情報のレコードが引数boardに格納されbookmarks_boardsに``<<演算子``で追加されている。

``<<``は指定されたオブジェクトの末尾に破壊的に追加できるメソッド。
強制的に追加されて保存もされているので``saveメソッドなどは必要ない。``

**``bookmarks_boards << board``は``bookmarks.create!(board_id: board.id)``と同様の処理がされている。**

**・unbookmarkメソッド**
``bookmarks_boards``からboardの引数に入っている掲示板idが入ったレコードを探し出して削除（delete)するメソッド。

**・bookmark?メソッド**
``bookmarks_boards``にboardの引数に入っている掲示板idが含まれているレコードがあるかどうか判定するメソッド。

なぜ**Bookmarkモデル**ではなく**Userモデル**にbookmarkを定義するのか。
もし後から機能を（無料会員はブックマークが10個まで、ブックマークしたら、ブックマークされた側に通知とか）追加するとなった場合、**Userモデル**の方が後々管理しやすいからである。

また``Bookmarkモデル``は、``Userモデル``と``Boardモデル``の中間テーブルなので、イメージとしては紐付け役として存在するので実態が薄い。そのため``Userモデル``もしくは``Boardモデル``に記入した方が分かりやすい。

もっと細かく言うとbookmarkを
・``Userモデル``に記入した場合、``user.bookmark(board)``となる。
・``Boardモデル``に記入した場合、``board.bookmarked_by(user)``となる。
この２つのどちらが分かりやすいか？と見比べた時に、直下感的にも``user.bookmark(board)``の方が見やすいのでより``Userモデル``に記入した方がいいとなる。

##③Bookmarksコントローラーの実装
viewの必要がないので``controllerファイル``だけ作って記載する。

```bookmarks_controller.rb
class BookmarksController < ApplicationController
　　　　

  def create
    board = Board.find(params[:board_id])
    current_user.bookmark(board)
    redirect_back fallback_location: root_path, success: 'ブックマークしました'
  end

  def destroy
    board = current_user.bookmarks.find_by(params[:id]).board
    current_user.unbookmark(board)
    redirect_back fallback_location: root_path, success: 'ブックマークを外しました'
  end
end
```
``redirect_back fallback_location:``を使うと、直前のページにリダイレクトをしてくれる。
▶︎[【Rails】redirect_backとは](https://qiita.com/mmaumtjgj/items/b8992a9926134801cfaf)

##④Boardコントローラーにbookmarkの定義を追加
無駄にSQL文を発行させない様に``includes(:user)``を記載して関連するuserの情報も取得している。(n + 1問題の解消）

```boards_controller.rb
def bookmarks
    @bookmark_boards = current_user.bookmarks_boards.includes(:user).order(created_at: :desc)
end
```

##⑤Routingの設定
```routes.rb
Rails.application.routes.draw do
  root 'static_pages#top'

  resources :users

  get 'login', to: 'user_sessions#new'
  post 'login', to: 'user_sessions#create'
  delete 'logout', to: 'user_sessions#destroy'

  resources :boards do
    resources :comments,only: [:create,:destroy], shallow: true
    collection do
      get :bookmarks
    end
  end
  resources :bookmarks, only: %i[create destroy]
end
```

railsの基本的なアクションは``resources``で定義した時に作られる7つのアクションだが、更に別のアクションを追加したい時はcoolection(またはmember)を利用する。（この場合はbookmarksというアクションを追加したいということになる）

collectionとmemberの違いは、生成するroutingに、:idの有無で決まる。
``menber``はidが**有り**、``collection``はidが**無い**。
▶︎[【Rails】memberとcollectionの違い](https://qiita.com/mmaumtjgj/items/cc1ed24573bccd8ade6a)

##⑥Viewの実装
**☆ボタンの実装**
まずはブックマークをするボタンを作成。(見やすいようにrbファイルにしているが本来はerbファイル。)

```bookmarks/_bookmark_button.html.rb
<%= link_to board_bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", method: :post do %>
  <%= icon 'far', 'star' %>
<% end%>
```
次にブックマーク解除ボタンを作成。

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

↑の２つ作ったボタンをパーシャルでブックマークするボタンと、ブックマーク解除ボタンを切り替えるページを作成。

```bookmarks/_bookmark.html.rb
<% if current_user.bookmark?(board) %>
  <%= render 'bookmarks/unbookmark', board: board %>
<% else %>
  <%= render 'bookmarks/bookmark', board: board %>
<% end %>
```
``user.rb``で定義された``bookmark?メソッド``がここで使われる。 掲示板がブックマークされていたら掲示板解除ボタン、掲示板がブックマークされていなかったらブックマーク登録ボタンに切り替わる仕組みになる。

この↑完成した``_bookmark.html.erb``を掲示板のパーシャルに記入する。

```boards/_board.html.rb
<% if current_user.own?(board) %>
   <div class='mr10 float-right'>
      <%= render 'crud_menus', board: board %>
<% else %>
     <%= render 'bookmarks/bookmark_area', board: board %>
   </div>
<% end %>
```

掲示板がログインしているユーザーのものだったら``crud_menus``ボタンが表示され、ユーザーのものではなかった場合はお気に入りボタンに切り替わるようになる。

**お気に入り掲示板一覧機能ページの作成**
``boards``のbookmarks.html.erbに注意。

```boards/bookmarks.html.rb
<% 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 partial: "board", collection: @bookmark_boards %>
      <% else %>
        <p>ブックマーク中の掲示板がありません</p>
      <% end%>
      </div>
    </div>
  </div>
</div>
```

##参考記事
[中間テーブルを使ったお気に入り機能の実装！](https://miiina01220.hatenablog.com/entry/2020/11/20/172739)
[【Rails初心者必見】has_manyでデータ管理を行おう！](https://web-camp.io/magazine/archives/17680)
[Rails4のhas_many throughで多対多のリレーションを実装する](https://qiita.com/samurai_runner/items/cbd91bb9e3f8b0433b99)