おことわり
「unknown attribute 'XXX' for XXX. で必要ないはず/存在するはずのカラムが要求される」の続きになります。
そのため、「詰まっている箇所」まで同じ文面が続きます。
前提
クリック/キータッチで効果音を鳴らすことができるサンプラーアプリを作っています。
ここで作っているアプリはこれに加えて好きな音源をフォームで読み込んで音量やループ可否なども設定できます。
さらに、ログイン中は変更した設定を保存して、使いたい時に保存した設定を読み込むことができます。
イメージとしては「効果音ラボ」の「効果音ポン出し」に保存機能がついたようなものです。もっと具体的に言うと「KLANG」に当たると思われます。
効果音ラボ 効果音ポン出し
https://soundeffect-lab.info/pon/app.html
KLANG
http://m-tank.info/common/klang
実現したいこと
マウスクリック/キータッチで効果音を鳴らせるボタンが12あり、
12のボタンごとに「使用する効果音の音源(とその名前)」と「ボタンごとの設定(どのキーに配置するか、音量、ループ可能かどうか、ボタンの色)」を読み込みます。
それら12ボタンごとの設定と読み込む音源を、「サンプラー」の一つの投稿として保存(作成)する仕組みです。
##使用するテーブル
使用するテーブルは以下の通りです。
ユーザー(User)もありますが今回は省略いたします。
ちなみに簡単に言うとログインはdeviseで、メールアドレスではなくユーザーネーム(半角英数字)とパスワードでログインする仕組みにしています。
サンプラー(Sampler)
|保存するもの|カラム名|データ型|必須?|
|:--|:--|:--|
|id|id|-|
|ユーザーid|user_id|bigint?|
|サンプラー名|sampler_name|string|
ボタンごとの設定(Seboard)
|保存するもの|カラム名|データ型|必須?|
|:--|:--|:--|
|id|id|-|
|サンプラーid|sampler_id|bigint|true|
|効果音ライブラリid|sefile_id|bigint?|
|キー配置|position|integer|
|ボタン色|btncoler|integer|
|音量|volume|integer|
|ループ可否|loopable|loopable|
効果音ライブラリ(Sefile)
|保存するもの|カラム名|データ型|必須?|
|:--|:--|:--|
|id|id|-|
|ユーザーid|user_id|bigint|
|ボタンごとの設定のid|seboard_id|bigint|
|効果音(をDBに保存する際)の名前|sename|string|
|効果音の音源データ|sedata|(binary)string|
「効果音の音源」を「ボタンごとの設定」と分けているのは、「音源」は「サンプラー」の一つの投稿として投稿する以外にも直接「音源」を投稿できる仕組みにしたいためです。
証拠に、user_idのカラムがあり、idを取得してリレーションできるようにしています。
投稿(保存)のしくみ
一度の投稿で更新されるsampler一つにつき12のseboardがあり、それぞれに一つずつ(空白可)sefileが割り当てられます。
新規作成/更新のしくみ
ボタンは一つに統一し、「新規作成/更新」機能は一つに統一します。
具体的に言うと以下の通りです。
「『サンプラーに付ける名前』に前回保存したサンプラーの名前と同じ投稿がある場合、その投稿に今回保存するサンプラーの設定を更新。
それ以外は新規作成とする。」
以上、基本的な保存の仕組みに加えて、もう一つ
「『ボタンごとの設定』で読み込む音源に、
『効果音ライブラリ』の「効果音の名前」「効果音の音源データ」両方の値が同じデータがある場合、そのデータのidを『ボタンごとの設定』の『効果音ライブラリid』に割り当てる。
ない場合は新規データとして『効果音ライブラリ』のデータとして保存。そのidを割り当てる」
が加わります。
詰まっている箇所(更新:210810)
エラー画面はこのような感じです。
ただし、どこで詰まっているかはターミナルを見た方が詳しい気がするのでそちらを説明します。
現在の状況(ターミナル)
コードを貼ると字数を超えてしまうため、画像2つで表示しています。
申し訳ありません。
このコードを見る限り、リレーションの最上位であるsamplerテーブルの値は満足に送れている気がします。
それが、その下層であるseboardテーブルの値から何らかのエラーが起きて送信が停止しているように見えます。
現在のコード
/app/controllers/sampler_controller.rb
ruby
1class SamplerController < ApplicationController 2 3 before_action :authenticate_user! 4 5 # 投稿を一覧形式にする(予定) 6 def seindex 7 @samplers = current_user.samplers.all 8 end 9 10 def save 11 @sampler = current_user.samplers.find_or_initialize_by(sampler_name: params[:sampler_name]) 12 @sampler.seboards.build 13 @sampler.sefile.find_or_initialize_by(sename: params[:sename], sedata: params[:sedata]) 14 end 15 16 def submit 17 @sampler = current_user.sampler.find_or_initialize_by(sampler_name: params[:sampler_name]) 18 unless @sampler.persisted? 19 @sampler = current_user.sampler.new(sampler_params) 20 21 unless @sampler.save! 22 flash[:danger] = "保存に失敗しました" 23 else 24 flash[:success] = "保存しました" 25 end 26 27 else 28 @sampler = current_user.sampler.update(update_sampler_params) 29 30 unless @sampler.save! 31 flash[:danger] = "更新に失敗しました" 32 else 33 flash[:success] = "更新しました" 34 end 35 36 end 37 38 redirect_to root_path 39 40 end 41 42 43 def show 44 @sampler = Sampler.find(params[:id]) 45 end 46 47 private 48 49 def sampler_params 50 params.require(:sampler).permit(:sampler_name, seboards_attributes: [:position, :btncolor, :volume, :loopable, sefile_attributes: [:sename, :sedata]]) 51 end 52 53 def update_sampler_params 54 params.require(:sampler).permit(:sampler_name, seboards_attributes: [:position, :btncolor, :volume, :loopable, sefile_attribute: [:sename, :sedata]]) 55 end 56end 57
/app/models/seboard.rb
Ruby
1class Seboard < ApplicationRecord 2 belongs_to :sampler,inverse_of: :seboards#, class_name: "Sampler", foreign_key: 'sampler_id' 3 4 has_one :sefile,inverse_of: :seboard#, class_name: "Sefile", foreign_key: 'sefile_id' 5 accepts_nested_attributes_for :sefile 6 7 8end
/app/models/sefile.rb
Ruby
1class Sefile < ApplicationRecord 2 belongs_to :user, class_name: "User", foreign_key: 'user_id' 3 4 belongs_to :seboard, class_name: "Seboard", foreign_key: 'seboard_id' ,inverse_of: :sefile, optional: true 5 6 mount_uploader :sedata, SeUploader 7 8end 9
/app/views/sampler/_save.html.erb
html
1 <%= form_with model: @sampler do |sp| %> 2 <%= sp.fields_for :seboards do |sb| %> 3 <%= sb.fields_for :sefile do |sf| %> 4 5 <!-- 上段 --> 6 <div id="bg-q" class="case-btn-q case-btn" onclick="play(soundQ, loopQ, loopValue_q); btnColorInit(btnColorQ, bg_Q)"> 7 <p>Q</p> 8 <%= sb.hidden_field :position,:name=>"sampler[seboards_attributes][1][position]", :value => "1" %> 9 <%= sb.range_field :volume, :name=>"sampler[seboards_attributes][1][volume]", :onchange=>"volumeInit(soundQ, volumeQ, volumeValue_q)", :id=>"vol-q", :value=>50, :min=>1, :max=>99 %> 10 <div class="radio-field_q"> 11 <%= sb.radio_button :btncoler, "1", :name=>"sampler[seboards_attributes][1][btncolor]", checked: true, :class=>"red" %> 12 <%= sb.radio_button :btncoler, "2", :name=>"sampler[seboards_attributes][1][btncolor]", :class=>"blue" %> 13 <%= sb.radio_button :btncoler, "3", :name=>"sampler[seboards_attributes][1][btncolor]", :class=>"white" %> 14 </div><!-- .radio-field_q --> 15 <%= sb.check_box "loopable", :name=>"sampler[seboards_attributes][1][loopable]", :onclick=>"loopInit(soundQ, loopQ, loopValue_q)", :id=>"loopable_q", :checked_value =>true, :unchecked_value=>false %> 16 <%= sf.file_field :sedata, :name=>"sampler[seboards_attributes][1][sefile_attributes][sedata]", :id=>"selectFile_q", :onclick=>"readFile(soundQ, form_fileQ)" %> 17 <%= sf.text_field :sename, :name=>"sampler[seboards_attributes][1][sefile_attributes][sename]" %> 18 19 </div><!-- .case-btn-d --> 20 <div id="bg-w" class="case-btn-w case-btn" onclick="play(soundW, loopW, loopValue_w); btnColorInit(btnColorW, bg_W)"> 21 <p>W</p> 22 <%= sb.hidden_field :position,:name=>"sampler[seboards_attributes][2][position]", :value => "2" %> 23 <%= sb.range_field :volume, :name=>"sampler[seboards_attributes][2][volume]", :onchange=>"volumeInit(soundW, volumeW, volumeValue_w)", :id=>"vol-w", :value=>50, :min=>1, :max=>99 %> 24 <div class="radio-field_w"> 25 <%= sb.radio_button :btncoler, "1", :name=>"sampler[seboards_attributes][2][btncolor]", checked: true, :class=>"red" %> 26 <%= sb.radio_button :btncoler, "2", :name=>"sampler[seboards_attributes][2][btncolor]", :class=>"blue" %> 27 <%= sb.radio_button :btncoler, "3", :name=>"sampler[seboards_attributes][2][btncolor]", :class=>"white" %> 28 </div><!-- .radio-field_q --> 29 <%= sb.check_box "loopable", :name=>"sampler[seboards_attributes][2][loopable]", :onclick=>"loopInit(soundW, loopW, loopValue_w)", :id=>"loopable_w", :checked_value =>true, :unchecked_value=>false %> 30 <%= sf.file_field :sedata, :name=>"sampler[seboards_attributes][2][sefile_attributes][sedata]", :id=>"selectFile_w", :onclick=>"readFile(soundW, form_fileW)" %> 31 <%= sf.text_field :sename, :name=>"sampler[seboards_attributes][2][sefile_attributes][sename]" %> 32 </div><!-- .case-btn-d --> 33 34 <% end %> 35 <% end %> 36 37 <%= sp.text_field :sampler_name %> 38 <%= sp.submit "保存する" %> 39 <% end %> 40 41
/db/schema.rb
リレーション、特に外部キーの設定で何度もつまづいているため、念のためschema.rbから外部キーの設定も貼っておきます。
ruby
1(略) 2 add_foreign_key "samplers", "users" 3 add_foreign_key "seboards", "samplers" 4 add_foreign_key "seboards", "sefiles" 5 add_foreign_key "sefiles", "seboards" 6 add_foreign_key "sefiles", "users" 7end
試したこと(更新:210814)
sefileにuser_idがない、とのご指摘をいただいたので解法を調べていたところ、
- deep_merge
- to_h.deep_merge(配列に変換した後deep_merge)
- reverse_merge
の3つがありました。
deep_merge
コードは以下の通りに修正しました。
/app/controllers/sampler_controller.rb
ruby
1 def sampler_params 2 params.require(:sampler).permit(:sampler_name, seboards_attributes: [:position, :btncolor, :volume, :loopable, sefile_attributes: [:sename, :sedata]]).deep_merge(seboards_attributes:[sefile_attributes:[user_id: current_user.id]]) 3 end 4 5 def update_sampler_params 6 params.require(:sampler).permit(:sampler_name, seboards_attributes: [:position, :btncolor, :volume, :loopable, sefile_attributes: [:sename, :sedata]]).deep_merge(seboards_attributes:[sefile_attribute:[user_id: current_user.id]]) 7 end 8
to_h.deep_merge
コードは以下の通りに修正しました。
/app/controllers/sampler_controller.rb
ruby
1 def sampler_params 2 params.require(:sampler).permit(:sampler_name, seboards_attributes: [:position, :btncolor, :volume, :loopable, sefile_attributes: [:sename, :sedata]]).to_h.deep_merge(seboards_attributes:[sefile_attributes:[user_id: current_user.id]]) 3 end 4 5 def update_sampler_params 6 params.require(:sampler).permit(:sampler_name, seboards_attributes: [:position, :btncolor, :volume, :loopable, sefile_attributes: [:sename, :sedata]]).to_h.deep_merge(seboards_attributes:[sefile_attribute:[user_id: current_user.id]]) 7 end 8
reverse_merge
最後はreverse_mergeですが、結果は変わりませんでした。
補足情報(FW/ツールのバージョンなど)
ブラウザ:chrome(最新のバージョン)
テキストエディタ:atom(ver:1.32.2)
rails: Rails 6.1.4
回答2件
あなたの回答
tips
プレビュー