requestオブジェクトについて

requestオブジェクトについて

railsは、クライアントからサーバーへリクエストを投げる際に諸情報が含まれるrequestオブジェクトを作成している。
requestオブジェクトには、現在リクエストしているパスが入っていて、[request.path]で取得できる。(/boards(indexアクション)にリクエストしている時に、indexビュー内で検索フォームをrenderしているなら、検索フォームで[request.path]を記載するとURLには/boardsが渡る。)

検索機能の実装

検索機能の実装

railsで検索機能を実装するのに[ransack]Gemを使う。

[ransack Gemの利用手順]

Gemfileに以下のように記載する。

[Gemfile]

gem "ransack"


$ bundle install

コントローラに以下のように記載する。

[app/controllers ファイル]

  def index
    @q = Board.ransack(params[:q])
    @boards = @q.result(distinct: true)
  end


検索フォームを記載するビューファイルに以下のように記載する。

[app/views ファイル]

<%= search_form_for @q do |f| %>
    <%= f.search_field :title_or_body_cont %>
      <%= f.submit %>
<% end %>

※ポイント
・[search_form_for]でURLを指定しないと、indexアクションにフォーム欄のパラメータが行く。(デフォルト以外にパラメータを送って実行したい場合は、そのアクションのURLを記載する。)
・検索条件(文字を含むや、以上、以下など)は、[f.search_field]の後に[カラム名_マッチャー(公式ドキュメント記載)]のようにして記載する。
・[distinct: true]を記載するのは、関連する子テーブルの情報を検索条件に使って、親テーブルの検索結果を表示するとき。 例えば、検索条件が「railsというコメントがついている掲示板を取得する」という場合に同じ掲示板にrubyというコメントが2つ以上あった場合に、同じ掲示板を複数取得してしまう為、[distinct: true]を使うとそれを防げます。
・[@q = Board.ransack(params[:q])]で、検索オブジェクト(検索フォームで入力した値とその条件(一致や含むなど))を取得している。
Boardテーブル以外から検索条件を適用して、データを取得したい場合は[@q = current_user.bookmark_boards.ransack(params[:q])]のようにする。
・[@boards = @q.result(distinct: true)]で、検索条件を使って対応しているテーブルからデータを取得する。
・[search_form_for]に渡すURLにidを含めるのは、ふさわしくない。

参考記事:

Rubyon Rails で検索機能を作ろう(ransack) - Qiita

【Rails】 ransackを使って検索機能がついたアプリを作ろう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

ransackで検索機能を実装 - Ruby on Railsの備忘録

gemのransackで複数カラムを検索対象にする方法 - Qiita

ransackでRailsアプリのヘッダーに検索機能をつける - Qiita

ページネーションの実装

ページネーションの実装方法

ページネーションとは、一覧機能などにおいて指定した件数毎にページを分けるもの。
以下のようなもの。

Image from Gyazo

ページネーションの実装に[kaminari]Gemを使います。
[kaminari]Gemは、配列やActiverecord::Relationsのオブジェクトに対して以下のような設定をするとページネーションが実装できる。
・1ページあたりの表示件数を何件にするか([per]メソッドを利用)
・何ページ目を表示させるのか(pageメソッドを利用)
・配列 ・デザインをどのようにするか

※ポイント
ActiveRecord::Relation : User.all や User.where(name: "test") などで取得できるものが ActiveRecord::Relation のインスタンス
ActiveRecord::CollectionProxy : has_manyなどを設定することで利用できるメソッドで取得できるのがActiveRecord::CollectionProxy のインスタンス。(ActiveRecord::CollectionProxy は ActiveRecord::Relationを継承している。)

[kaminari Gemの利用手順]

Gemfileに以下のように記述

[Gemfile]

gem "kaminari"


$ bundle install


下記コマンドでページネーションの設定ファイルを作成

$ rails g kaminari:config


一覧などを取得しているコントローラに以下のように記述

[app/controllers ファイル]

  def index
    @boards = Board.all.includes(:user).order(created_at: :desc).page(params[:page]).per(10)
  end

perメソッドに記述している1ページあたりの表示件数は、以下のように記述することも可能(設定ファイルにて1ページあたりの表示件数を設定)

[app/controllers ファイル]

  def index
    @boards = Board.all.includes(:user).order(created_at: :desc).page(params[:page])
  end
[config/initializers/kaminari_config.rb]

Kaminari.configure do |config|
  config.default_per_page = 20
end


ページネーションを表示したい場所に以下のように記述

[app/views ファイル]

<%= paginate @boards %>


ページネーションのデザインを設定
rails g kaminari:views ○○(ダウンロードしたいviewテンプレート)

$ rails g kaminari:views bootstrap4

※ポイント
ページネーションの設定ファイルは、以下のようになる。

[config/initializers/kaminari_config.rb]

Kaminari.configure do |config|
  #config.default_per_page = 25              # デフォルトでの1ページあたりの表示件数(デフォルトは 25)
  # config.max_per_page = nil                   # 表示ページの最大件数(デフォルトでは nilになっていて、無限になる)
  # config.window = 4           # 表示中のページの左右何ページ分のをページネーションに表示するかを指定(デフォルトは 4)
  # config.outer_window = 0      # 先頭ページ、及び最終ページから何ページ分のページネーションを表示するかを指定(デフォルトは 0)left、right が指定された場合は、そちらの値が優先されます。
  # config.left = 0              # 先頭ページから何ページ分のページネーションを表示するかを指定(デフォルトは 0)
  # config.right = 0                                        # 最終ページから何ページ分のページネーションを表示するかを指定(デフォルトは 0)
  # config.page_method_name = :page     # モデルに追加されるページ番号を指定するスコープの名前(デフォルトは page)
  # config.param_name = :page                  # ページ番号を渡すために使用するリクエストパラメータの名前(デフォルトは page)
  # config.max_pages = nil
  # config.params_on_first_page = false
end

[page]メソッドが使えず、[paginate_array]を使う対象
[paginate_array]を使う対象は通常通りのオブジェクト(ただの配列。例: [1, 2, 3])。 [1, 2, 3].page(params[:page])などとしてもページネーション機能は使えない。 そういった場合に使えるのがpaginate_arrayで、公式ドキュメントにもあるように以下のように使う。

Kaminari.paginate_array([1, 2, 3]).page(params[:page]) そして、paginate_arrayなしでページネーションの機能を使えるのはActiveRecord::Relationを継承しているクラス。

参考記事:

【rails】kaminariを使ってページネーションを作る - Qiita

【Rails】 kaminariの使い方をマスターしよう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

Railsライブラリ紹介: ページングを行う「kaminari」 | TECHSCORE BLOG

kaminari - Webとデザインのあれこれ

ActiveRecord::Relationとは一体なんなのか | ⬢ Appirits spirits

Railsでページネーションを実装する(kaminari) - 線路は続くよどこまでも。

ハイパーレガシーコードクリエイターのblog : 【Rails】【メモ】Kaminari使ったら、検索結果をページネーションできなかった話

railsでの非同期通信の実装方法

railsでの非同期通信の実装方法

railsでの非同期通信の実装手順は、[remote: true]オプションを付与する方法とJSファイルに任意のタイミングでAjax処理を発火させるように記述する方法がある。

今回は、簡単な[remote: true]オプションを付与する方法を説明します。

①クライアント側で動作することでサーバーにリクエストを送る部分(link_toやform_withなど)にremote: trueと記載することで、railsの通信は非同期通信になる。


②サーバー側では、remote: trueのオプション付きでリクエストが送られてきて、いつも通り(ルーティング→コントローラーの対応しているアクション→DBへのアクセスが必要な処理の場合は、モデルに側でクエリを発行してDBへアクセス)の処理がされ、remote: trueがある事により、レスポンスのファイルは、htmlファイルではなく対応しているjsファイルがクライアントにレスポンスされる。
※非同期通信にする際は、リダイレクトやレンダーを指定しない。
リダイレクトやレンダーを指定しなければ、[app/views/コントローラ名/アクション名.js.erb]ファイルがクライアントにレスポンスされる。(js.erbファイルは、jsファイルでRuby記述も入れられるファイル。)


レスポンスとして[js.erb]ファイルを受け取った、クライアントはブラウザ上で受け取ったファイルを元にJavaScriptを実行する。

実際の具体例を以下に記述します。

[app/views ファイル]

<%= link_to bookmark_path(board.bookmarks.find_by(user_id: current_user.id)), id: "js-bookmark-button-for-board-#{board.id}", method: :delete, remote: true do %>
  <i class="fas fa-star"></i>
<% end %>

上記の記述で[link_to]に[remote: true]オプションを付与しているので、クライアント側が上記のアイコンをクリックしたら、サーバー側に非同期通信でのリクエストを送る。


  def create
    @board = Board.find(params[:board_id])
    current_user.bookmark(@board)
  end

  def destroy
    @board = current_user.bookmarks.find(params[:id]).board
    current_user.unbookmark(@board)
  end

送られてきたリクエストは、ルーティングを元に対応しているコントローラーのアクション側で処理される。
上記のようにそれぞれのアクションにリダイレクトやレンダーを記述していないので、[app/views/コントローラー名/create.js.erb]や[app/views/コントローラー名/destroy.js.erb]ファイルがクライアント側にレスポンスされる。


[app/views/bookmarks/create.js.erb]

$("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("<%= j(render('boards/bookmark', board: @board)) %>");

上記のような場合は、まずサーバー側でrender処理をしてから、上記ファイルをクライアント側にレスポンスする。
イメージは、以下のようなものをレスポンスする感じになる。
$("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("[app/views/boards/_bookmark.html.erb]ファイルの内容");

※ポイント
jQueryの[replaceWith]と[html]の違い
以下のようなコードがあった場合の例を示します。

[ブラウザ上でのHTML]

<div id="greeting">Hello</div>
[app/views/js.erb ファイル]

$("#greeting").replaceWith("こんにちは");

上記のような場合は、ブラウザ側で実行され以下のような結果になる。

[ブラウザ上でのHTML]

こんにちは
[app/views/js.erb ファイル]

$("#greeting").html("こんにちは");

上記のような場合は、ブラウザ側で実行され以下のような結果になる。

[ブラウザ上でのHTML]

<div id="greeting">こんにちは</div>

[replaceWith]は、対応している実際の要素($("")の部分)を全部replaceWithの引数に置き換える。
[html]は、対応している部分の要素($("")の部分)の内容を置き換える。

j(render)とは
escape_javasciptの事で、JavaScriptは[""]や['']があるとダメなので、エスケープする必要がある。

同期通信と非同期通信
・同期通信は、ブラウザとサーバーがやり取りする。
・非同期通信は、ブラウザに入っているJavaScriptがブラウザの代わりにサーバーとやり取りする。

jQueryとは
JavaScriptで出来ることを簡単に記述できるようにしたJavaScriptのライブラリ。
JavaScriptは、各ブラウザに搭載されたエンジンにより実行されるが、jQueryはブラウザ毎の記述の違いを吸収してくれる。
jQueryは、基本的にHTMLのDOM操作やAjax処理を簡単に記述する為のものなので、それ以外の処理(数値の足し算や配列データの操作)は、JavaScriptコードで記述する必要がある。
jQueryの使い方は、以下のようになる。

[app/views/js.erb ファイル]

$("#aaa");    # HTMLからidが[aaa]のDOMを取得する。
$(".aaa");      #HTMLからclassが[aaa]のDOMを取得する。
$("#aaa").html("こんにちは");     #先のように取得したDOMに対してメソッドを使う。

以下のように[app/views/js.erb]ファイルに$(function() {}を記載しない説明は、以下のようになる。

[app/views/bookmarks/create.js.erb]

$("#js-bookmark-button-for-board-<%= @board.id %>").replaceWith("<%= j(render('boards/bookmark', board: @board)) %>");

htmlファイル内に直接jQueryを埋め込んで利用する場合は、scriptタグで囲って$(function() {}を記述しなければ、基本的に動かないが、 今回のようにjs.erbファイルにjQueryを記載する際は、functionを記載しなくても動くが、functionを記載した方が分かりやすくなり、意味不明なエラーなども回避できるので、js.erbファイルにもfunctionを記載した方が良い。($(function() {}によって、HTMLが読み込まれるのを待ってからjQueryが実行される。)

参考記事:

「Ruby on Railsで簡単で素早くWebアプリを開発する」 最終回 Rails + jQuery でAjaxを使ってみよう

ブックマークボタンのajax化 - olive_miuのブログ

【Rails】簡単なajax処理 (remote true) - bokuの学習記録

escape_javascriptメソッドって何ぞや。 - Qiita

今さら聞けない!jQueryとは【初心者向け】 | TechAcademyマガジン

JS形式のレスポンスと remote: true を使う - Qiita

jQuery 要素を置き換える(replaceWith/replaceAll) | ITSakura

railsで普通のリンクをajax通信に変更するテンプレートセット - Qiita

Railsで remote: true と js.erbを使って簡単にAjax(非同期通信)を実装しよう!(いいね機能のデモ付) - Qiita

Railsで remote: true と js.erbを使って簡単にAjax(非同期通信)を実装しよう!(いいね機能のデモ付) - Qiita

jquery — jQuery replaceWith()とhtml()の違いは何ですか?

【IT用語】 Ajaxとは?初心者向けに豊富な画像で仕組みを解説 | Pikawaka

【Rails】 remote: trueでフォーム送信をAjax実装する方法とは? | Pikawaka

Ajax(非同期通信)についてわかりやすさ重視でまとめてみた(Rails使用のデモ付) - Qiita

【Rails入門】ajaxの使い方まとめ | 侍エンジニアブログ

【Rails】 respond_toメソッドの使い方まとめ | Pikawaka

$(function(){})ってどういう意味?jQueryのおまじないをわかりやすく解説します – More Web

Railsで remote: true と js.erbを使って簡単にAjax(非同期通信)を実装しよう!(いいね機能のデモ付) - Qiita

jQueryのデバッグ方法

jQueryデバッグ方法

jQueryデバッグ方法は、[console.log][debugger]の2つあります。
以下にそれぞれの使い方の手順を説明します。

console.logの使い方

Image from Gyazo

上記のように[js.erb]ファイルに[console.log]を記述する。


上記で記述した[js.erb]ファイルがレスポンスとしてクライアントに返ってくるような動作をブラウザでする。
(ブックマークボタンにremote: trueを設定していて、ルーティング→コントローラーの対応するアクション→js.erbファイルをレスポンスすると設定している場合は、ブックマークボタンをクリックするような感じ。)


ブラウザの検証ツールで以下のように[js.erb]ファイルに記述したconsole.logの内容が表示される。

Image from Gyazo

※変数の中の値を確認したい場合などに利用する。

debuggerの使い方

Image from Gyazo

上記のように[js.erb]ファイルに[debugger]を記述する。


上記で記述した[js.erb]ファイルがレスポンスとしてクライアントに返ってくるような動作をブラウザでする。
(ブックマークボタンにremote: trueを設定していて、ルーティング→コントローラーの対応するアクション→js.erbファイルをレスポンスすると設定している場合は、ブックマークボタンをクリックするような感じ。)


ブラウザの検証ツールで以下のように[js.erb]ファイルにdebuggerを記述した場所で処理が止まる。

Image from Gyazo


下記のようにConsole画面に入力すると処理が止まったところからの処理を見れる。

Image from Gyazo

railsの[binding.pry]のような感じ。
[debugger]は、条件文の実行前など確実に実行される行に記載するのがポイント。

JavaScriptでのイベントドリブン

JavaScriptの基本

JavaScriptRubyの違い
[プログラムが動く場所]
JavaScriptは、ブラウザ上で動く(サーバー上で動くものもある)
エラー内容は、開発者ツールなどから探す(ログの画面には、エラー内容は出てこない)

Rubyは、サーバーサイド側で動く
エラー内容は、ログの画面などから探

[JavaScriptの役割とRubyの役割]
JavaScriptは、DOMをいじって画面を装飾する(ブラウザが全てを行っている。)
DOM:Document Object Modelの略。(JavaScriptからHTMLの要素にアクセスするためのもの。)

Rubyは、クライアントからのリクエストをもとにクライアントが求めている情報を提供する(サーバーが全てを行っている)

[JavaScriptの基本操作手順]

①DOMを操作するには、HTMLの要素に[id]や[class]を付与する必要がある。

②DOMの取得コマンドは、以下のようになる。
document.getElementById("id名")

①と②の手順の具体例は、以下のようになる。

<button id="button-delete">削除ボタン</button>
<button id="button-update">更新ボタン</button>

Image from Gyazo

document.getElementById("button-delete")

上記コマンドを実行すると以下のようにDOMが取得できる

<button id="button-delete">削除ボタン</button>

以下のようなコマンドを実行すると、取得したDOMを変数に入れて、その変数からテキストを取り出せる。

const buttonDelete = document.getElementById("button-delete")
buttonDelete.innerText
=> "削除ボタン"

以下のようなコマンドを実行すると、画面上の表記を変える事ができる。

const buttonDelete = document.getElementById("button-delete")
buttonDelete.innerText = "デリートボタン"

Image from Gyazo

[イベントドリブンの基本操作手順]
①DOMに対してaddEventListenerを設定することでイベントを設定できる。
addEventListenerの使い方は、以下のようになる。
(取得したDOM).addEventListener('イベントの種類(クリックやホバーなど)',() => { alert("削除") # ここには、実際の処理内容を記載する })

具体例は、以下のようになる

<button id="button-delete">削除ボタン</button> 
<button id="button-update">更新ボタン</button> 
document.addEventListener('DOMContentLoaded', () => {
    console.log("DOMContentLoaded")
    
    // 削除ボタンのDOMを取得
    const buttonDelete = document.getElementById("button-delete")
    // 更新ボタンのDOMを取得
    const buttonUpdate = document.getElementById("button-update")
    // 削除ボタンにクリックイベントを仕掛ける
    buttonDelete.addEventListener('click', () => {
      // クリックされた時に動く処理
      alert("削除!")
    })
    // 更新ボタンにクリックイベントを仕掛ける
    buttonUpdate.addEventListener('click', () => {
      // クリックされた時に動く処理
      alert("更新!")
    })
});

Image from Gyazo

<textarea id="textarea"></textarea>
<button id="button">アラート</button>
const buttonAlert = document.getElementById("button")
const textarea = document.getElementById("textarea")
buttonAlert.addEventListener("click",() => {
  alert(textarea.value)
})

Image from Gyazo

上記のように[value]を使うと、テキストボックスの要素を取得して、その要素のvalueプロパティを参照すると、テキストボックスの値が取得できる。
[value]は、文字列として取得してくるため数値に変えたい場合は、[parseInt(jsAmount.value)]のようにする。

※[ document.addEventListener('DOMContentLoaded', () => {}) ]もイベントの1つでイベントの設定処理になる。(ブラウザがDOMの解析が終わったらみたいなイメージ。この記述がないとDOMの解析が終わる前に削除ボタンのDOMを取得しようとしてうまく動かなくなる。)

[addEventListenerのアロー関数の引数のeとは?]

下記のような画像アップロード時のプレビューコードがあった時に、

[ビューファイル]

= f.file_field :cooking_memory_image, class: 'form-control', id: 'js-cooking-memory-image'
- if @cooking_memory.cooking_memory_image.attached?
  = image_tag @cooking_memory.cooking_memory_image.variant(resize: '350'), id: 'preview'
- else
  = image_tag 'cooking_memory/default.png', id: 'preview'
[javascriptファイル]

document.addEventListener('DOMContentLoaded', () => {
  function previewImage(e) {
      const file = e.target.files[0];           ①
      const reader  = new FileReader(); ②
      reader.onloadend = function () {   ③
          const preview = document.querySelector("#preview")
          if(preview) {
              preview.src = reader.result;     ④
          }
      }
      if (file) {
          reader.readAsDataURL(file);    ⑤
      }
  }

  const cookingMemoryImage = document.getElementById('js-cooking-memory-image');

  cookingMemoryImage.addEventListener('change', (e) => {
    previewImage(e);
  })
});

アロー関数の第一引数には、そのイベントのあらゆる情報(何の要素に・何をきっかけにetc)が入っている

「イベントオブジェクト」なるものが代入されます。
つまり(e) => {のeには、イベントオブジェクトが入っている。
イベントオブジェクトのtargetの値にDOMの中の取得したい値が入っているので、DOMの中の値を取得したい場合は、targetで取得する。

ちなみに、画像アップロード時のコードの意味は以下のようになる。
①は、アップロードした画像のファイルを取得してfileという変数に格納
②は、new FileReaderとする事でFileReaderインスタンスを作成(このインスタンスは、HTML上でローカルにアップロードされたファイルを読み込むことができる)
③は、FileReaderの読み込みが完了した時にイベントが発生
④は、アップロードしたファイルをエンコードしてプレビューのsrc属性に付与している
エンコードというのは、アップロードしたファイルをWEBページ上で使えるようにしている
⑤でアップロードされた画像を読み込んでいる

参考記事: JavaScriptのテキストボックスから値を取得する方法を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

【JavaScript】addEventListenerの使い方 - Qiita

【10分でマスター】onChangeでフォームの項目をコントロールしよう | 侍エンジニアブログ

【JavaScript】イベントリスナーの引数(e)~トラブル回避の備忘録~ | Logical Studio Blog

画像をアップロード&プレビューしよう【わかりすぎて怖いJavaScript入門】 - YouTube

ブックマーク機能の追加

ブックマーク機能の追加

掲示板にブックマーク機能を設定する手順は、以下になります。

①Bookmarkモデル作成(中間テーブル)
※中間テーブルとは?
多対多の関係にある2つのテーブルの間に挟まって、2つの組み合わせパターンだけをレコードとして保存する。 多対多を実装するためには、お互いがお互いの外部キーを知る必要がある。 UserとBoard(ブックマークの関係)は、中間テーブルを通しての関係になっているので、そのような時は、has_many thorughを使う(多対多を直接参照できるようにするもの。)

$ rails g model Bookmark user:references board:references


②作成したマイグレーションファイルを下記のように設定
マイグレーションファイルの(A)は、user_idとboard_idの組み合わせが一意だよってこと。(1人のユーザーが同じboardにブックマークするのを防ぐ為)
※ポイント
テーブルのカラムに一意性制約をかけるときは、インデックスの作成も必要になる。全てのデータを検索しないと、過去のデータと重複しているかわからない為。add_indexメソッドの中でunique: trueと記述する。

def change
    create_table :bookmarks do |t|
      t.references :user, foreign_key: true
      t.references :board, foreign_key: true

      t.timestamps
    end

    add_index :bookmarks, [:user_id, :board_id], unique: true    # (A)
end


③[Userモデル]と[Boardモデル]と[Bookmarkモデル]にアソシエーションとbookmarksテーブルの[user_id]と[board_id]が一意になるバリデーションを設定する

[app/models/user.rb]

  has_many :boards, dependent: :destroy  # (F)
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy  # (G)
  has_many :bookmark_boards, through: :bookmarks, source: :board  # (H)
[app/models/board.rb]

  belongs_to :user  # (C)
  has_many :comments, dependent: :destroy
  has_many :bookmarks, dependent: :destroy  # (D)
  has_many :bookmark_users, through: :bookmarks, source: :user  # (E)
[app/models/bookmark.rb]

  belongs_to :board  # (B)
  belongs_to :user  # (B)
  validates :user_id, uniqueness: { scope: :board_id }   # (A)

※ポイント
・Bookmarkモデルに[user_id]と[board_id]の組み合わせが一意になるように設定 (A)
scope は範囲を指定して、一意かどうかをチェックしてくれる。(1つのuser_idが同じboard_idを持たないという事になる)

・1つのブックマークは、1つの掲示板と1つのユーザーに属しているので、belongs_toを記載する。(B)

・1つの掲示板は、掲示板の作成者は1人なので、1人のユーザーに属しているので、belongs_toを記載する。 (C)

・1つの掲示板は、複数のブックマークを持てるので、このように記載。(dependent: :destroyは、親である掲示板が削除されたらbookmarksテーブルからブックマークを削除するように記述している。)(D)

・1つの掲示板は、bookmarksテーブル(user_idとboard_idを持っているテーブル)を通したbookmark_users(bookmarksテーブルを通したusersテーブル[この掲示板にブックマークしているユーザーのみのusersテーブル])を複数持っているので、このように記載する。(E)
関連名に既に[users]を使っているので、[bookmark_users]と名前を変えおり、通常の[モデル名の複数形]と関連名を定義していないので、source: :userでuserモデルを参照するとしている

・1人のユーザーは、複数の掲示板を投稿できるので、このように記載する。 (F)

・1人のユーザーは、複数のブックマークを持てるので、このように記載。(dependent: :destroyは、親であるユーザーが削除されたらbookmarksテーブルからブックマークを削除するように記述している。)(G)

・1人のユーザーは、bookmarksテーブル(user_idとboard_idを持っているテーブル)を通したbookmark_boards(bookmarksテーブルを通したboardsテーブル[このユーザーがブックマークしている掲示板のみのboardrsテーブル])を複数持っているので、このように記載する。(H)
関連名に既に[boards]を使っているので、[bookmark_boards]と名前を変えている通常の[モデル名の複数形]と関連名を定義していないので、source: :boardでboardモデルを参照するとしている

・belongs_toを設定すると対象カラムに対するpresence: trueは、自動で設定される。

has_many throughの使い方

has_many :関連名, through: :中間テーブル名
has_many :中間テーブル名


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

[config/routes.rb]

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

ブックマークしている掲示板の一覧ページを表示する際は、http://localhost:3000/boardsというURLにした方が直感的で分かりやすいので、このように設定したい。
このようにURLを設定するには、resourcesを使うと[new, create,index,show,edit,update,destroy]アクションに相当する決まった形のURLしか作れない為、違う形のURLを作成するのにcollectionを利用する。(URLにidを含めて記載する時は、memberを利用する。)


⑤ブックマーク一覧の掲示板を取得するアクションを以下のように定義

[app/controllers/boards_controller.rb]

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


⑥ブックマーク一覧のビューファイルを以下のように記述

[app/views/boards/bookmark.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 @favorites.present? %>
            <%= render @favorites %>   #(A)
        <% else %>
          <p><%= (t '.no_result') %></p>
        <% end %>
      </div>
    </div>
  </div>
</div>

↓⑦ 上記のビューファイル(A)の部分で@favorites(ブックマークされている掲示板の一覧を取得)をrenderに渡しているので、railsが推測して[board.html.erb]ファイルをrenderする。
[
board.html.erb]ファイルは、以下のように記載している。

[app/views/boards/_board.html.erb]

        <div class="col-sm-12 col-lg-4 mb-3">
          <div id="board-id-1">
            <div class="card">
              <%= image_tag board.image.thumb.url, class: 'card-img-top' %>
              <div class="card-body">
                <h4 class="card-title">
                  <%= link_to board.title, board_path(board) %>
                </h4>
                <% if current_user.own?(board) %>
                  <%= render 'crud_menus', {board: board} %>
                <% else %>
                  <%= render 'bookmark_button', {board: board} %>
                <% end %>
                <ul class="list-inline">
                  <li class="list-inline-item"><i class="far fa-user"></i><%= board.user.decorate.full_name %></li>
                  <br><li class="list-inline-item"><i class="far fa-calendar"></i><%= l board.created_at %></li></br>
                </ul>
                <p class="card-text"><%= simple_format(board.body) %></p>
              </div>
            </div>
          </div>
        </div>


⑧bookmarksコントローラにブックマークする時の処理を記載

[app/controllers/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: (t '.success')
  end

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


⑨ブックマークされているかの判断やブックマークする処理、ブックマークを解除するメソッドを以下のように定義

[app/models/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


⑩未ブックマークのボタンと済ブックマークのボタンを以下のように記述することで出し分ける

[app/views/boards/_bookmark_button.html.erb]

<% if current_user.bookmark?(board) %>
  <%= render 'bookmark', { board: board } %>
<% else %>
  <%= render 'unbookmark', { board: board } %>
<% end %>
[app/views/boards/_unbookmark.html.erb]

<div class='mr10 float-right'>
  <%= link_to board_bookmarks_path(board), id: "js-bookmark-button-for-board-#{board.id}", method: :post do %>
  <i class="far fa-star"></i>
<% end %>
</div>
[app/views/boards/_bookmark.html.erb]

<div class='mr10 float-right'>
<%= link_to bookmark_path(board.bookmarks.find_by(user_id: current_user.id)), id: "js-bookmark-button-for-board-#{board.id}", method: :delete do %>
  <i class="fas fa-star"></i>
<% end %>
</div>

参考記事:

railsの中間テーブルでデータの組み合わせを一意にする方法 - Qiita

掲示板にお気に入り機能を実装する① - Ruby on Rails Learning Diary

【Rails】複数のカラムを使ったユニーク制約の方法 | Always be myself

uniqueness: scope を使ったユニーク制約方法の解説 - Qiita

【Rails】アソシエーションを図解形式で徹底的に理解しよう! | Pikawaka - ピカ1わかりやすいプログラミング用語サイト

中間テーブル|えーと|note

【Rails】多対多のアソシエーションに別名をつけたいあなたに - ひよっこエンジニアの雑多な日記

Rails 一意性制約のかけ方|Nori|note

ルーティングにアクションを追加 - Ruby on Rails入門