質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.93%

Railsの多対多のモデルにおいて、中間テーブルの追加カラムのデータまで一括保存したいです

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 165

motoyu5623

score 1

前提・実現したいこと

個人でRailsのWEBアプリ作成中のプログラミング初学者です。

仕事に関するレビューサイトを作ろうとしていて、下図のようなモデルを組んでいるのですが、データベースへの新規登録が上手くいかないので、ご助言いただけますと幸いです。

イメージ説明

前提

  • カレントユーザーは複数の仕事(Job)を登録できる
  • 各Jobは、仕事に必要なSkillを5個持つ
  • 一度登録されたSkillは、他のJobでも使い回せる(即ちJobとSkillは多対多の関係で、SkillはToxi法におけるタグのようなイメージ)
  • 中間テーブルであるJobSkillには、scoreという追加カラムを用意する(あるJobに対して、そのSkillの重要度を示す)
  • Job登録画面で、Jobの情報、Skillの名前、JobSkillのscoreを同時に登録できるようにしたい(←scoreができなくて詰まっています)

発生している問題・エラーメッセージ

Job登録フォームからJob、Skill、JobSkillの3モデルの情報を入力すると、Jobと5つのskillは登録されるものの、それに紐づくscoreが登録されていない「ように」見えます。

「ように」と表現したのは、エラーこそ発生しないものの、実際には以下の9行目と11行目に示すように、JobSkillテーブルへの不完全な登録が2回行われています。
各INSERTの内容は、

①1回目のINSERTは、scoreは入っているが、3つのモデルの紐付けができていない
②2回目のINSERTは、3つのモデルの紐付けはできているが、scoreが入っていない

となっているので、showページ等で取り出せるのは当然②となり、scoreが登録されていないのと実質的に同じ状況ということです。

1  sidebiz_1  |    (0.3ms)  BEGIN
2  sidebiz_1  |   ↳ app/controllers/jobs_controller.rb:25:in `create'
3  sidebiz_1  |   User Load (27.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 5], ["LIMIT", 1]]
4  sidebiz_1  |   ↳ app/controllers/jobs_controller.rb:25:in `create'
5  sidebiz_1  |   Job Create (6.2ms)  INSERT INTO "jobs" ("name", "work_type", "section", "industry", "medium", "occupation", "started_at", "ended_at", "worktime_week", "description", "pulled_skill", "returned_skill", "user_id", "created_at", "updated_at", "company") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING "id"  [["name", "1357"], ["work_type", ""], ["section", ""], ["industry", ""], ["medium", ""], ["occupation", ""], ["started_at", "2016-01-01"], ["ended_at", "2016-01-01"], ["worktime_week", 1], ["description", "a"], ["pulled_skill", ""], ["returned_skill", ""], ["user_id", 5], ["created_at", "2020-07-17 04:57:42.920242"], ["updated_at", "2020-07-17 04:57:42.920242"], ["company", ""]]
6  sidebiz_1  |   ↳ app/controllers/jobs_controller.rb:25:in `create'
7  sidebiz_1  |   Skill Create (6.8ms)  INSERT INTO "skills" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["name", "5734"], ["created_at", "2020-07-17 04:57:42.934796"], ["updated_at", "2020-07-17 04:57:42.934796"]]
8  sidebiz_1  |   ↳ app/controllers/jobs_controller.rb:25:in `create'
9  sidebiz_1  |   JobSkill Create (7.7ms)  INSERT INTO "job_skills" ("skill_id", "score", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["skill_id", 51], ["score", 2], ["created_at", "2020-07-17 04:57:42.952086"], ["updated_at", "2020-07-17 04:57:42.952086"]]
10 sidebiz_1  |   ↳ app/controllers/jobs_controller.rb:25:in `create'
11 sidebiz_1  |   JobSkill Create (13.5ms)  INSERT INTO "job_skills" ("job_id", "skill_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["job_id", 56], ["skill_id", 51], ["created_at", "2020-07-17 04:57:42.969098"], ["updated_at", "2020-07-17 04:57:42.969098"]]**
12 sidebiz_1  |   ↳ app/controllers/jobs_controller.rb:25:in `create'
13 sidebiz_1  |    (5.5ms)  COMMIT
14 sidebiz_1  |   ↳ app/controllers/jobs_controller.rb:25:in `create'
15 sidebiz_1  | Redirected to http://localhost:3010/jobs/56
16 sidebiz_1  | Completed 302 Found in 177ms (ActiveRecord: 67.9ms | Allocations: 13690)

該当のソースコード

models/job.rb

class Job < ApplicationRecord
  belongs_to :user
  has_many :job_skills, dependent: :destroy
  has_many :skills, through: :job_skills
  accepts_nested_attributes_for :skills, allow_destroy: true
  accepts_nested_attributes_for :job_skills, allow_destroy: true


models/skill.rb

class Skill < ApplicationRecord
  has_many :job_skills, dependent: :destroy
  has_many :jobs, through: :job_skills
  accepts_nested_attributes_for :job_skills
end


models/job_skills.rb(JobとSkillの中間テーブル)

class JobSkill < ApplicationRecord
  belongs_to :job, optional: true, autosave: true
  belongs_to :skill, optional: true, autosave: true
end

controllers/jobs_controller.rb

class JobsController < ApplicationController
 before_action :authenticate_user!, except: %i[index show]

 def new
    @job = current_user.jobs.build
    5.times {@job.skills.build}
  end

  def create
    @job = current_user.jobs.build(job_params)
    if @job.save
      flash[:success] = 'job registered!'
      redirect_to job_path(@job)
    else
      render :new
    end
  end

  private

  def job_params
    params.require(:job).permit(:name, :user_id, 
                               skills_attributes: [:name,
                               job_skills_attributes: [:score, :job_id]])
  end
end


views/jobs/new.html.erb

<%= form_with(model: @job, local:true) do |f| %>
・
・
(Jobの色々な情報の入力フォーム)
・
・
  <div class="field">
    <%= f.fields_for :skills do |skill_field| %>
      <div>
        <%= skill_field.label :name %>
        <%= skill_field.text_field :name %>

★→→→    <%= skill_field.fields_for :job_skills, @job.job_skills.build do |score_field|%>
          <%= score_field.label :score %>
          <%= score_field.number_field :score %>
           <%= score_field.hidden_field :job_id, :value => @job.id %>
         <% end %>
      </div>
    <% end %>
  </div>

  <%= f.submit '登録する' %>
<% end %>

試したこと

job入力フォームの(★)の行にて、job_skillsをbuildしているため、SQLの二重発行が起きていると認識しています。一回目のINSERTはこのbuildによるもので、二回目のINSERTはskillがsaveされたときにhas many through効果で自動で関連付けされてできるものだと思います。

そこで、★行の@job.job_skills.buildを消してみたところ、今度はscoreを入力するフォームが消えてしまいます。
jobに紐づくskillがsaveされていない段階では、job_skillsオブジェクトが出来ていないからだと思います。

ユーザビリティの観点から、できれば一つのページで3モデルすべての情報を登録できるようにしたいと考えています。
しかしググって出てくるのは、既に中間テーブルの両親が存在する状態で、中間テーブルの追加カラムを登録するといった方法ばかりで、解決の糸口が全くつかめていないという状況です。(userとpostのデータが既にある状態で、中間テーブルのcommentをcommentコントローラーからcreateする、など)

質問まとめ

中間テーブルの両親(JobとSkill)がまだ出来ていない段階で、中間テーブルの追加カラム(score)まで一緒に新規保存する方法はありますでしょうか。それがそもそも難しい場合、他にベターなモデル設計はありますでしょうか
個人的には、JobとSkillの一意な組み合わせによってscoreが定まるはずなので、中間テーブルにscoreカラムを追加するのは正規化の観点からも悪くないと思っているのですが、もしそのあたりの認識から間違っていましたら、ご指摘のほどお願いいたします。

お忙しいところ最後までお読みいただきありがとうございました。
何かヒントになりそうな情報がございましたら、コメントいただけますと幸いです。
あるいは不足している情報等あれば、ご教示のほどお願いいたします。

補足情報(FW/ツールのバージョンなど)

ruby 2.7.1
Rails 6.0.3.2
docker-composerubyコンテナとpostgresコンテナを立てて開発しています。
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

モデルの設計はこれでよいと思いますが

  1. Skillは他のJobでも使い回せる
  2. 一つのページで3モデルすべての情報を登録できる

は矛盾します。使い回しをするなら、Skillは予めとうろくしてあるものから選ぶ、というUIになるのではないでしょうか。
3モデル同時登録ですと、同じnameのSkilが沢山できてしまい、結果 Job:Skillは1:多になります。

追記
まず logをみて paramsの構造を確認することをお薦めします。
jobをつくる、
params の Skill を拾って「なければ作る」
それに対応する job_skils を作る
というような手順でしょうか

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/07/18 00:47

    ご回答ありがとうございます!

    例えば、Job(id:10)を新規登録した時に入力したSkill(name:Excel)が、既にSkillテーブルにid =1, name=Excelとして登録されていた場合、Skillテーブルのレコードは増やさずに、Jobテーブルにid=10のレコードと、JobSkillテーブルにjob_id=10, skill_id=1のレコードが増えるというイメージを持っていました。

    「無ければ作る」ということで、find_or_craete_byがそのイメージに近い動きをしてくれるかと想像していました(初めて触るので本当にできるかわかりませんが)。

    使い回しという表現が誤解を招いてしまったかもしれず申し訳ございませんが、色んなJobに対するSkillを開発者側の私が予め洗い出してリスト化するのは難しいので、ユーザー側にどんどん登録してもらう想定をしておりました。
    その中で、同じ名前のSkillが一つのテーブルに何回も出てくるのは効率的ではないため、Skill名の重複を許さない形(即ちJob:Skillが多対多)にする方向性で考えていました。

    もしここまでで違和感、見落としている点などございましたら、ご指摘いただけますと幸いです。

    今後の方向性

    A. 上の多対多の考えがやはり成り立たない場合→1対多とする
    B. 上の多対多が一応実現できそうな場合→多対多か1対多のどちらかを選択

    Bとなった場合、1対多はシンプルに書けると思うので、自分のスキルでの実現性、時間、DBの応答速度(名前重複の分Skillのレコード数が増える+Skill名からJobのレコードを取ってくる手段がidから名前検索になる)あたりを天秤にかけて、どちらを選択するか決めようと思います。

    重ねてですが、貴重なご意見いただき、ありがとうございました。

    キャンセル

  • 2020/07/18 18:26

    「無ければ作る」 という考え方はあります。
    その場合は jobs.build(job_params) で一気には作れません。
    お書きになっている手順をコツコツ書くことになります。
    > 使い回しという表現が誤解を招いてしまったかも
    いや、コメントに書かれたようなことを考えているのだろうな、ということはしっかりわかってました 笑

    キャンセル

  • 2020/07/22 16:18

    追記ありがとうございます!

    ネストの順番を「Job→Skill→Job_skill」から「Job→Job_skill→Skill」に変えたら3モデル同時登録は一応できましたが、これだと「無ければ作る」が難しいので、記載いただいた手順で考えてみます。

    方向性を示していただき、ありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.93%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • トップ
  • Rubyに関する質問
  • Railsの多対多のモデルにおいて、中間テーブルの追加カラムのデータまで一括保存したいです