has_manyのidsを使って、簡単に中間テーブルの関連付けを行う


概要

チェックボックスを複数選択し、その情報を配列で受け取って、中間テーブルの関連付けを行います。


E-R図

スクリーンショット 2019-07-07 22.57.39.png

※今回説明で使用するアプリとカラム数があっていませんが、中間テーブルの使い方として掲載します。


事前準備

ToDo系アプリを想定して進めます。

画像のようなタスクを新規作成する画面を用意しました。

今回の話で使用するのは、Labelの複数のチェックボックスから値を取ってくる方法です。

スクリーンショット 2019-07-07 23.13.00.png


view


app/views/_form.html.erb

<tr>

<th>Label</th>
<td>
<% Label.all.each do |label| %>
<%= form.check_box :label_ids, { multiple: true, checked: @task.labels.find_by(id: label.id).present?, include_hidden: false }, label[:id] %>
<label class='badge badge-secondary'><%= label.name %></label>
<% end %>
</td>
</tr>

form.check_box :label_idsと記述すると、paramsの値として、"label_ids"=>["7", "8", "10"]このような形でハッシュのキーを持つ値を送れます。

multipleオプションを使用することで、複数のチェックボックスのパラメータを配列形式で送れます。

checked: @task.labels.find_by(id: label.id).present?これはeditで使用することを想定しており、編集するタスクに紐付いているラベルにチェックを付けるということをしています。

include_hidden: falseチェックしていない項目については、パラメータを送らないオプションです。


model


app/models/task.rb

class Task < ApplicationRecord

has_many :labelings, dependent: :destroy
has_many :labels, through: :labelings
end


app/models/labeling.rb

class Labeling < ApplicationRecord

belongs_to :task
belongs_to :label
end


app/models/label.rb

class Label < ApplicationRecord

has_many :labelings, dependent: :destroy
end


controller


app/controllers/tasks_controller.rb

  def create

@task = Task.new(task_params)
if @task.save
redirect_to root_path, notice: '新しいタスクを作成しました'
else
render :new
end
end

#省略

private

def task_params
params.require(:task).permit(:subject, :content, :expired_at, :state, :priority, :user_id, label_ids: [])
end


label_ids: []複数チェックボックスの値を配列で渡すことを許可しています。


タスク新規作成時に、ラベルを複数つける処理の動き

もう一度タスク新規投稿の画面を貼ります。

スクリーンショット 2019-07-07 23.13.00.png

Labelの開発DB操作次回MTGまでにチェックを入れた状態で登録ボタンを押します。

この3つのチェックボックスの値を取得します。

まずparamsには次のような値が送られます

Parameters: { "utf8"=>"✓",

"authenticity_token"=>"WoRUP+q21xCR760x9L3Y9lyLeQ+THkS9YOGISfGRyDRg0xaI1D2BNnRQLTuaHANpw+NCohg06QtB5U4RxruO5g==",
"task"=>{ "subject"=>"タスク新規作成",
"content"=>"ラベルを複数登録",
"expired_at(1i)"=>"2019",
"expired_at(2i)"=>"7",
"expired_at(3i)"=>"8",
"expired_at(4i)"=>"10",
"expired_at(5i)"=>"00",
"user_id"=>"87",
"state"=>"着手中",
"priority"=>"high",
"label_ids"=>["7", "8", "10"]},
"commit"=>"登録する" }

次に、controllerのcreateアクション時にbinding.pryで値を確認します。

  def create

@task = Task.new(task_params)
binding.pry
if @task.save
redirect_to root_path, notice: '新しいタスクを作成しました'
else
render :new
end
end

# idsだけ確認

pry(#<TasksController>)> @task.label_ids
=> [7, 8, 10]

# newメソッドで作られたlabelsを確認
pry(#<TasksController>)> @task.labels
=> [#<Label:0x00007f92cfe32628 id: 7, name: "開発", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:10:17 JST +09:00>,
#<Label:0x00007f92cfe32470 id: 8, name: "DB操作", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:10:24 JST +09:00>,
#<Label:0x00007f92cfe322e0 id: 10, name: "次回MTGまで", created_at: Fri, 05 Jul 2019 15:28:18 JST +09:00, updated_at: Sun, 07 Jul 2019 23:11:06 JST +09:00>]

pry(#<TasksController>)> @task.save
(0.9ms) BEGIN
(pry):5
CACHE User Load (0.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 87], ["LIMIT", 1]]
(pry):5
Task Create (33.3ms) INSERT INTO "tasks" ("subject", "content", "created_at", "updated_at", "expired_at", "state", "priority", "user_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" [["subject", "タスク新規作成"], ["content", "ラベルを複数登録"], ["created_at", "2019-07-07 23:59:15.509944"], ["updated_at", "2019-07-07 23:59:15.509944"], ["expired_at", "2019-07-08 10:00:00"], ["state", "着手中"], ["priority", 0], ["user_id", 87]]
(pry):5
Labeling Create (0.9ms) INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["task_id", 212449], ["label_id", 7], ["created_at", "2019-07-07 23:59:15.553820"], ["updated_at", "2019-07-07 23:59:15.553820"]]
(pry):5
Labeling Create (0.3ms) INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["task_id", 212449], ["label_id", 8], ["created_at", "2019-07-07 23:59:15.556142"], ["updated_at", "2019-07-07 23:59:15.556142"]]
(pry):5
Labeling Create (0.2ms) INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["task_id", 212449], ["label_id", 10], ["created_at", "2019-07-07 23:59:15.557177"], ["updated_at", "2019-07-07 23:59:15.557177"]]
(pry):5
(0.8ms) COMMIT
(pry):5
=> true

pry(#<TasksController>)> @task.id
=> 212449
pry(#<TasksController>)> Labeling.where(task_id: 212449)
Labeling Load (0.2ms) SELECT "labelings".* FROM "labelings" WHERE "labelings"."task_id" = $1 [["task_id", 212449]]
app/controllers/tasks_controller.rb:38
=> [#<Labeling:0x00007f92d142e9e0
id: 11,
task_id: 212449,
label_id: 7,
created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00,
updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>,
#<Labeling:0x00007f92d142e8a0
id: 12,
task_id: 212449,
label_id: 8,
created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00,
updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>,
#<Labeling:0x00007f92d142e760
id: 13,
task_id: 212449,
label_id: 10,
created_at: Sun, 07 Jul 2019 23:59:15 JST +09:00,
updated_at: Sun, 07 Jul 2019 23:59:15 JST +09:00>]

無事1度のcreateアクションで、中間テーブルに3個保存されました!


タスク編集時に、複数のラベルを変更した時の、中間テーブルの動き

ここまで新規作成時の中間テーブルの関連付けの説明をしました。

では、更新する時はどうやるの?

外すラベルのid調べてテーブルからdeleteしてinsertするの?といった疑問が浮かぶと思います。

実はdeleteとかはいい感じに勝手にやってくれます!

実際の動きを確認してみました

スクリーンショット 2019-07-08 0.07.20.png

新規作成時に開発DB操作次回MTGまでにチェックを入れてます。

viewにchecked: @task.labels.find_by(id: label.id).present?を記載しているので、デフォルトでチェックが入っています

スクリーンショット 2019-07-08 0.07.35.png

DB操作次回MTGまでのチェックボックスを外し、

新たに検討中にチェックしました。

更新するボタンを押します。

   (0.2ms)  BEGIN

app/controllers/tasks_controller.rb:52
Label Load (0.3ms) SELECT "labels".* FROM "labels" WHERE "labels"."id" IN ($1, $2) [["id", 7], ["id", 9]]
app/controllers/tasks_controller.rb:52
Labeling Destroy (0.3ms) DELETE FROM "labelings" WHERE "labelings"."task_id" = $1 AND "labelings"."label_id" IN ($2, $3) [["task_id", 212449], ["label_id", 8], ["label_id", 10]]
app/controllers/tasks_controller.rb:52
Labeling Create (0.4ms) INSERT INTO "labelings" ("task_id", "label_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["task_id", 212449], ["label_id", 9], ["created_at", "2019-07-08 00:08:31.021375"], ["updated_at", "2019-07-08 00:08:31.021375"]]
app/controllers/tasks_controller.rb:52
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 87], ["LIMIT", 1]]
app/controllers/tasks_controller.rb:52
(2.1ms) COMMIT
app/controllers/tasks_controller.rb:52

すると、中間テーブルからチェックボックスを外したlabel_idがDestroyされた後にCreateされています。