中間テーブルを使ったお気に入り機能の実装！

##実装したいこと

* 掲示板にブックマーク機能を追加したい。

* ユーザーがブックマークした掲示板を一覧できるページを実装したい。

これだけの機能なのに、めちゃくちゃ難しい。いろんなことを調べるいい機会になりました。
かなり多くのものを調べたので順序立てて説明をしていきます。

###実装の大まかな流れ

* 中間テーブルとなるBookmarkモデルの実装
* UserモデルとBoardモデルとのアソシエーションを実装
* Userモデルにお気に入り登録のギミックを定義
* BookmarkControllerの実装
* Routingの設定
* Viewの実装



##Bookmarkモデルの仕組み（多対多）
で、いざお気に入り機能をつけよう！となってもどうやって実装するの・・・？となります。なので、まずどうUserモデルとBoardモデルに紐づけていくのか考えて見ましょう。

###UserとBookmarkとBoardの関係
ユーザーと掲示板とブックマークの関係を見ていきましょう。

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

つまり、<b>UserもBoardもBookmarkをたくさん持っている</b>ということになります。このような関係を<b>多対多の関係</b>と言われています。

###中間モデル

多対多のモデルを実装するにはお互いのforeign_keyを知っている必要があります。そのために、お互いのidを格納するテーブル、中間テーブルを実装する必要があります。

##Bookmarkモデルの作成
中間テーブルとなるBookmarkモデルを作成していきます。

ターミナル
```ruby
rails g model Bookmark user:references board:references
```

migrateファイル

```ruby
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
```

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

`add_index :bookmarks, [:user_id, :board_id], unique: :true`でuser_idとboard_idの組み合わせがuniqueであることを設定します。
これで`rails db:migrate`を行います。


bookmark.rb

```ruby
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board

  validates :user_id, uniqueness: { scope: :board_id}
end
```

migrationにも`unique: :true`を付けたので、モデルにもバリデーションを記載します。

#####uniquenessとscopeについて
`validates :user_id, uniqueness: { scope: :board_id}
end`

上記は各掲示板idと同じユーザーidがお気に入り関係にならないように一意性制約を付けています。
rails cで確認して見ます。


```ruby
irb(main):001:0> user = User.first
irb(main):002:0> board = Board.first

# userが掲示板をお気に入り登録する。
irb(main):003:0> user.bookmark(board)
   (0.1ms)  begin transaction
  Bookmark Exists? (0.9ms)  
  Bookmark Create (3.2ms)  
   (0.7ms)  commit transaction
  
=> #<ActiveRecord::Associations::CollectionProxy [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">]>


# もう一度同じユーザーで掲示板のお気にいり登録を試みる。
irb(main):004:0> user.bookmark(board)

# すでにお気に入り登録されているので、バリデーションに引っ掛かりrollbackされる。
(0.1ms)  begin transaction
  Bookmark Exists? (0.2ms)  
   (0.1ms)  rollback transaction
Traceback (most recent call last):
        2: from (irb):4
        1: from app/models/user.rb:27:in `bookmark'
ActiveRecord::RecordInvalid (バリデーションに失敗しました: Userはすでに存在します)
```

```ruby
# 違うユーザーを指定
irb(main):005:0> user_2 = User.second

# 違うユーザーで掲示板をお気に入り登録を試みると成功する。
irb(main):006:0> user_2.bookmark(board)
(0.1ms)  begin transaction
  Bookmark Exists? (0.2ms)  
  Bookmark Create (0.8ms)  
   (1.4ms)  commit transaction
  
=> #<ActiveRecord::Associations::CollectionProxy [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">]>


```

bookmarkメソッドはUserモデルに定義しています。また後で紹介しますがconsoleで出てくるので載せておきます。

user.rb

```ruby
  # お気に入りにしている掲示板を取得する
  has_many :bookmarks_boards, through: :bookmarks, source: :board


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

```

#####参考記事


[https://qiita.com/j-sunaga/items/d7f0e944baad6e56206c:title]


[https://railsguides.jp/active_record_validations.html#uniqueness:title]



[https://qiita.com/kazukimatsumoto/items/14bdff681ec5ddac26d1#%E3%81%8A%E6%B0%97%E3%81%AB%E5%85%A5%E3%82%8A%E6%A9%9F%E8%83%BD%E3%82%92er%E5%9B%B3%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E8%A8%AD%E8%A8%88%E3%81%97%E3%82%88%E3%81%86:title]





##UserモデルとBoardモデルのアソシエーションの設定
Bookmarkモデルの実装が終わったので、他のモデルにもアソシエーションなどの設定を行っていきます。

###Boardモデル

board.rb

```ruby
class Board < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy # 追記
  mount_uploader :board_image, BoardImageUploader

  validates :title, length: { maximum: 255 }, presence: true
  validates :body, length: { maximum: 65535 }, presence: true
end
```

掲示板はたくさんのBookmarkを持つことができるので`has_many`を使います。

###Userモデル

user.rb

```ruby
  has_many :bookmarks, dependent: :destroy
  # お気に入りにしている掲示板を取得する
  has_many :bookmarks_boards, through: :bookmarks, source: :board

```

UserもたくさんのBookmarkを持つことができるので`has_many`を使っていきます。

ちょっと待って、`has_many :bookmarks_boards`って何？`through`とか`source`も使っているけどよくわからない・・・。
というわけでこれから`has_many through`について調べたことをまとめていきます。

#####has_many throuth
まず`has_many through`はUserモデルでどういう働きをしているのかというと、ユーザーがお気に入りしている掲示板を取得することができるようになります。

Twitterでいいね一覧が表示できる機能がありますよね。それと同じようにお気に入り登録した掲示板を一覧で表示できるページを作るために必要となってきます。

`has_many through`を使わずにユーザーのお気に入りした掲示板を取得したいとなると、このようなコードになります。

```ruby
# user.bookmarksでuserがお気に入り登録した掲示板のidが入っているレコードの集合を取得することができる。
irb(main):014:0> user.bookmarks

=> #<ActiveRecord::Associations::CollectionProxy [#<Bookmark id: 7, user_id: 1, board_id: 1, created_at: "2020-11-20 00:50:02", updated_at: "2020-11-20 00:50:02">, #<Bookmark id: 10, user_id: 1, board_id: 3, created_at: "2020-11-20 01:57:05", updated_at: "2020-11-20 01:57:05">, #<Bookmark id: nil, user_id: 1, board_id: 1, created_at: nil, updated_at: nil>]>

# userが最初にお気に入り登録した掲示板のレコードを取得
irb(main):005:0> user.bookmarks.first
  Bookmark Load (0.5ms)  
=> #<Bookmark id: 7, user_id: 1, board_id: 1, created_at: "2020-11-20 00:50:02", updated_at: "2020-11-20 00:50:02">

# 上記で取得したレコードにboardメソッドを実行するとお気に入り登録した掲示板の内容が取得できる！
irb(main):006:0> user.bookmarks.first.board
  Bookmark Load (0.1ms)  
  Board Load (0.2ms)  
=> #<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">

# つまり、user.bookmarksのひとつひとつのレコードにboardメソッドを実行すればユーザーがお気に入りにした掲示板の内容の集合を取得することができる！

# なのでmapメソッドを使ってユーザーがお気に入り登録した掲示板のレコードにboardメソッドを実行し、それを配列に組み込んでいく。
irb(main):007:0> user.bookmarks.map{|bookmark| bookmark.board}
  Bookmark Load (0.4ms)  
  Board Load (0.2ms) 
  Board Load (0.1ms)  
=> [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">, #<Board id: 3, title: "aaaaaaaaaa", body: "aaaaaaaaaaa", user_id: 2, created_at: "2020-11-20 01:56:08", updated_at: "2020-11-20 01:56:08", board_image: nil>]

# 上記の式を(&:)を使って書き換えます。
irb(main):008:0> user.bookmarks.map(&:board)
=> [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">, #<Board id: 3, title: "aaaaaaaaaa", body: "aaaaaaaaaaa", user_id: 2, created_at: "2020-11-20 01:56:08", updated_at: "2020-11-20 01:56:08", board_image: nil>]
```

つまり、`user.bookmarks.map(&:board)`を使えばユーザーのお気に入り登録している掲示板の情報の集合を取得できるというわけです！

このコードをcontrollerなどに書いて実装するのも一つの方法だと思いますが、あまり直接的ではないのと、このコードをビューに落とし込むのも大変です。

そこで`has_many through`の登場です。
これを使えば`user.bookmarks.map(&:board)`をモデル内に簡単に実装できちゃいます。

```ruby
has_many :bookmarks_boards, through: :bookmarks, source: :board
```

`:bookmarks_boards`と定義することでメソッド化して使うことができます。

`user.bookmarks.map(&:board)`このコードを見ながら解説していくと

<b>Userのインスタンスにbookmarksメソッド(`through:`で定義)を実行し、それで得られたBookmarksのインスタンスデータのひとつひとつの要素に対してboardメソッド(`source:`で定義)を実行する</b>

ということです。

なので、多対多のモデルを作った時に必ずと言っていいほど活躍するというわけです！

#####参考記事

実はRailsチュートリアルの第14章の動画を見るとすごくわかりやすいです。

[https://railstutorial.jp/:title]

Railsガイドの文献

[https://railsguides.jp/association_basics.html#has-many-through%E9%96%A2%E9%80%A3%E4%BB%98%E3%81%91:title]



#Bookmarks_Controllerの実装
よし、モデルのアソシエーションも終わったしcontroller作ろう！
ちょっと待ってください。controllerの可読性を上げるためにまずはモデルにお気に入り登録のギミックを定義していきましょう。すると驚くほどにcontrollerの実装が完結になりますよ！

###controllerを作る前にモデルにBookmarkのギミックを定義する。

Userモデルにお気に入り登録のギミックとなるメソッドを定義していきましょう。

user.rb
```ruby
  # お気に入り追加
  # <<で引数で渡した掲示板の情報が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

```

早速先ほど定義した`bookmarks_boards`が使われていますね。
一つずつメソッドを見ていきます。

####bookmarkメソッド

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

```

掲示板の情報のレコードが引数boardに格納され`bookmarks_boards`に`<<`で追加されています。

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

`<<`メソッドについて詳しくはこちら

[https://docs.ruby-lang.org/ja/latest/method/Array/i/=3c=3c.html:title]

####unbookmarkメソッド

```ruby
  # お気に入りを外す
  def unbookmark(board)
    bookmarks_boards.delete(board)
  end
```
`bookmarks_boards`からboardの引数に入っている掲示板idが入ったレコードを探し出して削除（delete)するメソッド。

####bookmark?メソッド

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

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

```ruby
def bookmark?(board)
    Bookmark.where(user_id: id, board_id: board.id).exist?
end
```
このように書くこともできますが、`include?`の方が直感的でわかりやすいです。

###bookmarks_controllerの実装
ここまできたら、bookmarks_controllerを作っていきましょう。
viewも必要ないのでcontrollerファイルだけ作って記載します。

bookmarks_controller.rb

```ruby
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
```

お気に入り登録のギミックをモデルに定義したことによって、かなり直感的なcontrollerになりました！

`redirect_back fallback_location : root_path`

`redirect_back`はユーザーが直前にリクエストを送ったページに戻すことができます。

`fallback_location`は直前にリクエストを送ったページがない場合のデフォルトのリダイレクト先を指定しています。


##Routingの設定
controllerも書けたので、次はRoutingの設定を行っていきます。

routes.rb

```ruby
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, shallow: true do
    resources :comments, only: %i[create destroy]
    resource :bookmarks, only: [:create, :destroy]
    collection do
      get :bookmarks
    end
  end
end

```

BoardとBookmarkは親子の関係なのでbookmarks_controllerのRoutingはboards_controllerにネストするように記載しています。

####collectionルーティング
railsの基本的なアクションはresourcesで定義した時に作られる7つのアクションですが、更に別のアクションを追加したい時があります。

その時に使えるのがcollectionルーティングです。boards_controllerに新しくbookmarksというアクションを追加することができます。
ちなみに、controllerのメンバーに対してアクションを追加する場合(idが伴う場合)はmemberルーティングを使います。

collection以外を抜いたルーティングがこちら。

```ruby
resources :boards, shallow: true do
    collection do
      get :bookmarks
    end
  end
```

`bookmarks_boards GET    /boards/bookmarks(.:format)                                                              boards#bookmarks
`
上記でこのようなルーティングが出来上がります。
このルーティングとアクションはお気に入りされた掲示板一覧を表示するページとして使っていきます。

####Boards_controllerにbookmarksアクションを追記
ルーティングが書けましたので、Boards_controllerにbookmarksアクションを追加していきます。

boards_controller.rb

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

無駄にSQL文を発行させない様に`includes(:user)`を記載して関連するuserの情報も取得しています。(n + 1問題の解消）

```ruby

irb(main):001:0> user = User.first

irb(main):002:0> user.bookmarks_boards
  Board Load (2.2ms)  SELECT "boards".* FROM "boards" INNER JOIN "bookmarks" ON "boards"."id" = "bookmarks"."board_id" WHERE "bookmarks"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">, #<Board id: 3, title: "aaaaaaaaaa", body: "aaaaaaaaaaa", user_id: 2, created_at: "2020-11-20 01:56:08", updated_at: "2020-11-20 01:56:08", board_image: nil>]>

irb(main):003:0> user.bookmarks_boards.includes(:user)
  Board Load (0.3ms)  SELECT "boards".* FROM "boards" INNER JOIN "bookmarks" ON "boards"."id" = "bookmarks"."board_id" WHERE "bookmarks"."user_id" = ? LIMIT ?  [["user_id", 1], ["LIMIT", 11]]
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?)  [["id", 1], ["id", 2]]
=> #<ActiveRecord::AssociationRelation [#<Board id: 1, title: "hogehoge", body: "3333333", user_id: 1, created_at: "2020-11-19 10:27:20", updated_at: "2020-11-19 10:27:20", board_image: "8965-1-20151228165149_b5680ea1512891.jpg">, #<Board id: 3, title: "aaaaaaaaaa", body: "aaaaaaaaaaa", user_id: 2, created_at: "2020-11-20 01:56:08", updated_at: "2020-11-20 01:56:08", board_image: nil>]>
```

##Viewの実装
ここまで仕組みを実装したら、あとはViewを作るだけです！
お気に入りボタンとお気に入りした掲示板一覧ページを作成していきます。

###お気に入りボタンの作成
まずはパーシャルでブックマークするボタンとブックマーク解除ボタンを切り替えるページを作ります。

bookmarks/_bookmark_area.html.erb

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

次に、お気に入り登録ボタンを実装していきます。
bookmarks/_bookmark.html.erb

```html
<%= link_to board_bookmarks_path(board_id: board.id), id: "js-bookmark-button-for-board-#{board.id}", method: :post do %>
  <%= icon 'far', 'star' %>
<% end%>
```

次に、お気に入り解除ボタンを実装していきます。
bookamrks/_unbookmarks.html.erb

```html
<%= 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 %>
```

`current_user`に紐づいているbookmarkインスタンスの中から掲示板idが含まれているものを探し、取得しています。

これで、お気に入りボタンが完成しました。これを掲示板のパーシャルに組み込んでいきます。

boards/_board.html.erb

```html
<% 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

```html
<% 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>
```

これで完成です！お疲れ様でした！

##おまけ
n + 1問題を解消するためにコードをリファクタリングしていきます。

boards_controller.rb

```ruby
def index
# userのみキャッシュしている。
  @boards = Board.all.includes(:user).order(created_at: :desc)

 #bookmarkも取得できる様になる。
  @boards = Board.all.includes([:user, :bookmarks]).order(created_at: :desc)
  end
```

user.rb

```ruby
# userを起点にしてSQLを走らせてしまっているためレコードを取得する時に毎回SQLが走ってしまう。
def bookmark?(board)
  bookmarks_boards.include?(board)
end

# boardを起点にしてSQLが走り、検索をかける。無駄なSQLが走らない。
def bookmark?(board)
  bookmarks_boards.pluck(:user_id).include?(id)
end
```

_unbookmark.html.erb

```ruby
<%= link_to bookmark_path(board.bookmarks.find { |b| b.user_id == current_user.id }),
            id: "js-bookmark-button-for-board-#{board.id}",
            class:"float-right",
            method: :delete,
            remote: true do %>
  <%= icon 'fas', 'star' %>
<% end %>
```

なるべくfindを使う様にして無駄にSQLを走らせない様にする。