前提・実現したいこと
個人で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-composeでrubyコンテナとpostgresコンテナを立てて開発しています。
回答1件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/07/17 15:47
2020/07/18 09:26
2020/07/22 07:18