railsでブックマーク機能を実装する時のざっくりの流れ

今後、お気に入りマーク(中間テーブル)をアプリに実装したい時に、この記事見れば全解決するじゃんってなるための記事です。

## 大まかな流れ

1. モデルを作成
2. ルーティングを設定する
3. コントローラーを追加する
4. アクションをビューに反映させる

それぞれ具体的に見ていきます。

## モデルを追加、編集

- 今回の難しいポイント
    1. 一対多、多対多のイメージに苦しむ
    2. バリデーションの書き方に苦しむ
    3. モデル間のアソシエーションに謎のscopeが出てくるため、戸惑う

まずは、お気に入り(Bookmarks)機能の[エンティティ](https://wa3.i-3-i.info/word11594.html)を作成していきます。

UserモデルとBoardモデルの中間テーブルとしての役割を果たしてもらうために、Bookmarksモデルを作っていきます。

ターミナルで、下記コマンドを打った後、マイグレーションファイル、モデルファイルを編集していきます。

```jsx
$ rails g model Bookmark user:references board:references
```

マイグレーションファイルを下記のように編集してください。

```jsx
〇〇〇〇_create_bookmarks.rb
class CreateBookmarks < ActiveRecord::Migration[5.2]
  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
```

編集後は下記のコードを忘れずに打ちます。

```jsx
$ rails db:migrate
```

作成されたbookmark.ebは下記のように編集します。

```jsx
Bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  belongs_to :board
  validates_uniqueness_of :board_id, scope: :user_id 
  // 上のコードは下の書き方と一緒　
  validates :user_id, uniqueness: { scope: :board_id}
  // 一つのユーザーは同じ投稿に対して一回しかブックマークができませんというバリデーション
end
```

同様に、board.rb、user.rbもbookmarkとのアソシエーションを作っていきます。それぞれの対応関係は以下の通り。

・UserとBookmarkは多対多の関係

・BoardとBookmarkは１対多の関係

```jsx
board.rb

class Board < ApplicationRecord

  belongs_to :user
  has_many :comments, dependent: :destroy //追加
  has_many :bookmarks, dependent: :destroy//追加
  validates :title, presence: true,length: { maximum: 255 }
  validates :body, presence: true,length: { maximum: 65535 }
  mount_uploader :board_image, ImageUploader

  def bookmarked_by?(user)
    bookmarks.where(user_id: user).exists?
    //下記の書き方でも可能
    bookmarks.exists?(user_id: user)
  end

end
```

```jsx
user.rb

class User < ApplicationRecord
  authenticates_with_sorcery!

# sorceryを使用したログイン機能のバリデーション
  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
  validates :email, uniqueness: true, presence: true
  validates :last_name, presence: true
  validates :first_name, presence: true

# １対多の関係だと、User has many 〇〇になるから、このようになる。
  has_many :boards, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy
  has_many :bookmarks_boards, through: :bookmarks, source: :board
  
// fat controllerにならないように、modelにロジックを記載する。
// viewに呼び出すことができる
// self.が省略されている点に注意。
// own?(object)の場合、self.own?(object)という意味になる

  def own?(object)
    id == object.user_id
  end

  def bookmark(board)
    bookmarks_boards << board
  end

  def unbookmark(board)
    bookmarks_boards.delete(board)
  end

  def bookmark?(board)
    bookmarks_boards.include?(board)
    // Bookmark.where(user_id: id, board_id: board.id).exists?と同じ
  end
end
```

```jsx
ちなみに、下記三つの書き方は同じ意味になる。

controller内に書く場合
　user.bookmarks.map{|bookmark| bookmark.board}

　user.bookmarks.map(&:board)

model内に書く場合
　has_many :bookmarks_boards, through: :bookmarks, source: :board

```

つまり、has_many through: を使うことで、user.bookmarksをboardに結び付けることができるわけです。
そして、結びつけたモデル名をbookmarks_boardsとすることができます。

そのbookmarks_boardsをuser.rbにてアクション定義することで、リファクタリングができます。

## コントローラーの追加

- 今回の難しいポイント
    1. @を使うか、使わないかの違いの理解に苦しむ
    2. saveとsave!の違いの理解に苦しむ

まずはrails g controllerでアクションを追加していく。実際に使うアクションはcreateとdestory Actionのみなので、以下のように記載する流れになります。作成後にcreate.html.erbとdestroy.html.erbは使わないので削除して構いません。もしくはrails g controller Bookmarkのみにして、手動でcreateとdestroyを追加してください。こっちの方が楽かもしれませんね。

```jsx
$ rails g controller Bookmark create destroy
```

作成したbookmark_controllerをスッキリ書くために、ロジックをモデルに記載しておきます。

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

## [!を付けた場合の挙動に関して](https://qiita.com/ozin/items/5968971c9d2b3ab0a84d)
代表例) saveとsave!の違い

save -> 保存できない場合はfalse

save! -> 保存できない場合はActiveRecord::RecordInvalid発生

感嘆詞を付けない場合(saveやcreate等)は、レコードの作成、保存に失敗した際、通常の場合はnilを返します。しかし、感嘆詞をつける場合(create!、save!)は、ActiveRecord::RecordNotFoundエラーを発生させることができます。
つまり！をつけることにより、例外を発生させるか発生させないかの違いです。

### !の使い分け方
　saveメソッドの場合は保存の成功と失敗を true/false で返します。一方で、save!メソッドは保存を失敗した場合に例外を返します。
　つまり、save!を使えば例外をトリガーにしてロールバックなどの処理を行うことができるため、比較的使用頻度が高いかと思います。

## Bookmarks_controllerの作成
Bookmark機能作成(createアクション)の流れは以下の通り。

1. 対応する掲示板を検索する
2. 1で見つけたidをログイン中のユーザーと紐付けでdbに保存する

Book mark機能削除(destroyアクション)の流れは以下の通り。

1. ログイン中のユーザーがいいねした投稿idを検索する
2. 2をmodelに定義したunbookmarkインスタンスを用いて削除する

それでは実装していきます。

```jsx
Bookmarks_controller.rb

class BookmarksController < ApplicationController

  def create
    @board = Board.find(params[:board_id])
#Boardモデルからboard_idを探してくる。
    current_user.bookmark(@board)
# ログイン中のユーザーと紐づけられたidを取ってくる。この時、user.rbに定義したaliasを
使用し、idの情報を保存する。
  end

  def destroy
    @board = current_user.bookmarks.find(params[:board_id]).board
    current_user.unbookmark(@board)
    # redirect_backはユーザーが直前にリクエストを送ったページに戻す
    # fallback_location: デフォルトの設定
  end

end
```

## ルーティングを設定する

- 今回の難しいポイント
    1. params[:id]に何が受け渡されているかイメージしづらい
    2. HTTPの受け渡しがイメージしづらい

次に、bookmarkアクションが実行された時のルーティングの設定をしていきます。
[collection](https://qiita.com/k152744/items/141345e34fc0095217fe)という機能をcontrollerに追加することで、個別のルーティング(idを付けない)追加することができます。下記のようにネストして書くことで、/board/bookmarksのようにパスを作ることができます。

[Rails のルーティング - Railsガイド](https://railsguides.jp/routing.html)

```jsx
resources :users, only: %i[new create]
resources :boards do 
  resorces collection do
   get :bookmarks
end

resources :bookmarks, only: %i [create destroy]
```

## アクションをビューに反映させる

コントローラーで作成したインスタンス変数、モデルで定義したアクションを上手い具合にビューに反映させていきます。肌感、これが一番難しいです。

- 今回の難しいポイント
    1. parialを呼び出すときの値の受け渡し方が難しい。（@つけるの？付けないの？）
    2. pathとurlの違いが理解しづらい。（form_withの話やルーティングの話）
    3. 条件分岐でpartialを表示させるのが難しい

partialを作成して、可読性を高めていきます。必要なpartialは以下の通り。

・ボタンパーシャル（ブックマーク作成削除を含むパーシャル）

・ブックマーク作成パーシャル

・ブックマーク削除パーシャル

また、作成するviewファイルは以下の通り。

・ブックマーク一覧ファイル

・renderと条件分岐を使用して既存のviewを編集していきます。

それでは、実装していきます。

まずはボタンパーシャルの作成から。

```jsx
_bookmark.button.html.erb

<% if current_user.bookmark?(board) %>
  <%= render 'bookmarks/unbookmark', board: board %>
<% else %>
  <%= render 'bookmarks/bookmark', board: board %>
<% end %>

```

次に、ブックマーク作成と削除パーシャルを実装していきます。

```

_bookmark.html.erb

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

link_to board_bookmarks_path(board_id: board.id)

```

_unbookmark.html.erb

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

link_to board_bookmarks_path(current_user.bookmarks.find_by(board_id: board.id))

次に、既存のviewファイルを編集していく。今回実装したい内容は以下の通り。

1. 他人のユーザーに対してのみブックマークができる。
2. 自分の投稿しか編集削除ができないようにする

boards/_board.html.erb

```
~省略~
#ログイン中のユーザーがboardで渡されたidを持っているかどうかの場合分け。

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

boards/bookmarks.html.erb　 ブックマーク一覧表示view

```
<% 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">
     #collection 
      <% if @bookmark_boards.present? %>
        <%= render partial: "board", collection: @bookmark_boards %>
      <% else %>
        <p>ブックマーク中の掲示板がありません</p>
      <% end%>

      </div>
    </div>
  </div>
</div>
```
完成です。

## 参考文献
https://railsguides.jp/routing.html

https://qiita.com/ozin/items/5968971c9d2b3ab0a84d

https://qiita.com/k152744/items/141345e34fc0095217fe