実現したいこと
Railsで商品の出品、売買ができるフリマアプリを作っております。
accepts_nested_attributes_forを使って複数モデルの同時保存(update)を行いたいのですが、
商品を出品(create)後の商品編集(update)ができず、エラーが発生しています。
具体的には、商品画像データを上手くサーバー側へ渡すことができておらず、
意図していない商品画像の空データが渡され、null制約に掛かりエラーが出ております。
最終的には以下機能を実装したい。(④で躓いております。)
①登録済み画像プレビューの表示(ブラウザ上の削除ボタンでプレビュー削除可能)
②新規で画像追加もしくは削除
③ブラウザ上で削除された登録済み画像のid取得
④新規登録画像(および③で取得したid、他商品名等のデータ)をajaxでサーバーへ送信
⑤上記idをキーにしてDBから該当データを削除
前提
・商品と画像は別々のテーブルを作成
・accepts_nested_attributes_forを使って、画像テーブル(子)を商品テーブル(親)へネスト
・1つの商品に対して画像は10枚まで投稿可能
・商品出品時の複数枚の画像投稿は実装済
・画像のアップロードにはCarrierwaveを使用
・formdataオブジェクトを使い、ajaxでデータをサーバーへ送信
発生している問題・エラーメッセージ
ajaxでデータがサーバーへ送信され、DBへ保存される際に、
空のimagesレコードが生成されて、NotNullViolationとなります。
ActiveRecord::NotNullViolation (Mysql2::Error: Field 'image' doesn't have a default value: INSERT INTO `item_images` (`item_id`, `created_at`, `updated_at`) VALUES (1, '2019-11-11 14:08:12', '2019-11-11 14:08:12')):
該当のソースコード
association
item.rb
Ruby
1 has_many :item_images, dependent: :destroy 2 accepts_nested_attributes_for :item_images, allow_destroy: true
item_images.rb
Ruby
1 belongs_to :item 2 mount_uploader :image, ItemImageUploader
view
edit.html.haml
Ruby
1.items-sell-wrapper.edit_item 2 = render "items/header" 3 %main.items-sell-main 4 %section.items-sell-content__section 5 %h2.items-sell-content__section__header 6 商品の情報を入力 7 .items-sell-container 8 = form_with model: @item, local: true, class: "dropzone", id: "item-dropzone" do |f| 9 %section.items-sell-content__section 10 .items-sell-container__form__image 11 .items-sell-container__form__image__label 12 = f.label "出品画像" 13 %span.form--required 14 必須 15 %p.image-upload-explanation 最大10枚までアップロードできます 16 = f.fields_for :item_images, class: "fields_for" do |item_image| 17 .preview 18 = item_image.label :image, class: "dropzone-box", for: "upload-image-0" do 19 .input_area.items-sell-container__dropzone0 20 %p.image-upload-text ここをクリックして画像をアップロード 21 = item_image.file_field :image, style: "display: none;", name: "item[item_images_attributes][#{@item.item_images.length}][image]", id: "upload-image-0", class: "upload-image {validation-error if @item.errors.full_messages_for(:item_images).present?}", 'data-image': 0
Controller
items_controller.rb
Ruby
1 def update 2 @item = Item.find(params[:id]) 3 # 登録済画像のidの配列を生成 4 ids = @item.item_images.map{|image| image.id} 5 # 登録済画像のうち、編集後も残っている画像のidの配列を生成 6 exist_ids = registered_image_params[:ids].map(&:to_i) 7 # 登録済画像が残っていない場合(配列に0が格納されている)、配列を空にする 8 exist_ids.clear if exist_ids[0] == 0 9 if (exist_ids.length != 0 || new_image_params[:images][0] != " ") && @item.update!(create_params) 10 11 # 登録済画像のうち削除ボタンが押された画像を削除 12 unless ids.length == exist_ids.length 13 # 削除する画像のidの配列を生成 14 delete_ids = ids - exist_ids 15 delete_ids.each do |id| 16 @item.item_images.find(id).destroy 17 end 18 end 19 end 20 end 21 private 22 def create_params 23 params.require(:item).permit(:name, :description, :category_id, :item_state_id, :deliver_expend_id, :deliver_method_id, :prefecture_id, :deliver_day_id, :amount, item_images_attributes: [:image]) 24 end 25 26 def registered_image_params 27 params.require(:registered_images_ids).permit({ids: []}) 28 end
javascript
1$(document).on("turbolinks:load", function() { 2 var input_area = $(".input_area") 3 var preview = $(".preview") 4 // editでのみ発火するよう制限 5 var re = new RegExp('/items/[0-9]+/edit$'); 6 if(!re.test(location.pathname)) { 7 return; 8 } 9 10 for (var i = preview.length; i > 1; i--) { 11 preview[i-1].remove(); 12 } 13 preview = $('.preview'); 14 15 16 // 登録済画像データのidを格納する配列を作成 17 // ブラウザ上で削除された画像のidをコントローラへ送り返す 18 var registered_images_ids = []; 19 // 新規追加画像データ用の配列(DB用) 20 var new_image_files = []; 21 22 gon.item_images.forEach(function(image, index) { 23 var img = $(`<div class = "img-view"> 24 <img> 25 <div class="btn-wrapper"> 26 <div class="btn-edit">編集</div><!-- 27 --><div class="btn-delete">削除</div> 28 </div> 29 </div>`); 30 31 img.data("image", index) 32 binary_data = gon.item_images_binary_datas[index] 33 // 登録済画像データをsrcプロパティへ付与 34 img.find("img").attr({ 35 src: binary_data 36 }); 37 38 // 登録済画像データのidを配列へ格納 39 registered_images_ids.push(image.id) 40 // 登録済画像データを持たせたHTMLタグを、ビューへ追加してプレビューを表示させる 41 preview.append(img); 42 }); 43 44 // プレビュー表示されている画像の枚数に応じて、ラベルの大きさを調整 45 inputs_length = $('[type="file"].upload-image').length; 46 $(`.items-sell-container__dropzone0`).attr('class',`items-sell-container__dropzone${inputs_length}`); 47 48 // プレビュー画像の表示をラベル要素の前に移動させる 49 $('.dropzone-box').before($('.img-view')) 50 51 // 画像を新規追加 52 //inputの中身の変更時に発生 53 $(document).on('change', '[type="file"].upload-image', function(event) { 54 55 // 変更を行ったinputを取得する 56 changed_input = $(this); 57 changed_id = changed_input.data('image'); 58 // 現時点でのfile用inputタグを全取得する 59 inputs_length = $('[type="file"].upload-image').length; 60 //変更されたinputが末尾のinput(空欄からの追加)の場合、inputを更に追加 61 if(changed_id === inputs_length - 1 && inputs_length <= 10) { 62 var new_input = $(`<input class="upload-image" type="file" style="display: none;">`); 63 input_area.append(new_input); 64 } 65 66 $(`.items-sell-container__dropzone${inputs_length - 1}`).attr('class',`items-sell-container__dropzone${inputs_length}`); 67 68 //変更されたidに対応するimg-viewを取得 69 var image_view = $(".img-view[data-image="+changed_id+"]").first(); 70 //idに対応するimg_viewが存在しないときは追加 71 if(!image_view[0]) { 72 image_view = $(`<div class = "img-view"> 73 <img> 74 <div class="btn-wrapper"> 75 <div class="btn-edit">編集</div><!-- 76 --><div class="btn-delete">削除</div> 77 </div> 78 </div>`); 79 preview.append(image_view); 80 } 81 82 // プレビュー画像の表示をラベル要素の前に移動させる 83 $('.dropzone-box').before($('.img-view')) 84 85 //input,image_viewそれぞれにindexを再割り当て 86 reorder_data_image(); 87 88 // アップロードされた画像ファイル(ファイルオブジェクト)の属性値(filesプロパティ)を取得する 89 var file = changed_input.prop('files')[0]; 90 // FileReaderオブジェクトをインスタンス化する 91 var reader = new FileReader(); 92 // ファイル読み込み後の処理 93 reader.onload = function(e) { 94 // img_view内のimgタグのsrcプロパティへ、読み込みが完了した画像を入れ込む 95 image_view.find('img').attr('src', e.target.result); 96 }; 97 98 // FileReaderオブジェクトへ属性値(filesプロパティ)を代入する 99 reader.readAsDataURL(file); 100 }); 101 102 // data-imageをの番号を再割り当てする 103 function reorder_data_image() { 104 //input,image_viewそれぞれにindexを再割り当て 105 $('[type="file"]').each(function(index, input) { 106 $(input).attr({ 107 'data-image': index, 108 id: 'upload-image-' + index, 109 name: 'item[item_images_attributes][' + index + '][image]', 110 }); 111 $(input).prop('disabled', index >= 10); 112 }) 113 $('.img-view').each(function(index, image) { 114 $(image).attr('data-image', index); 115 }); 116 //dropzone-boxに対応付くinputを末尾のinputに再割り当て 117 $('.dropzone-box').attr('for','upload-image-' + ($('[type="file"].upload-image').length - 1)); 118 $('.items-sell-container__dropzone' + ($('[type="file"].upload-image').length)).attr('class', 'items-sell-container__dropzone' + ($('[type="file"].upload-image').length - 1)); 119 } 120 121 // 編集後のデータ送信 122 $('.edit_item').on('submit', function(e) { 123 e.preventDefault(); 124 125 // form情報をformDataに追加 126 var formElement = $('#item-dropzone').get(0) 127 var formData = new FormData(formElement); 128 129 // 登録済画像が残っていない場合は便宜的に0を入れる 130 if (registered_images_ids.length == 0) { 131 formData.append("registered_images_ids[ids][]", 0) 132 // 登録済画像で、まだ残っている画像があればidをformDataに追加していく 133 } 134 else { 135 registered_images_ids.forEach(function(registered_image) { 136 formData.append("registered_images_ids[ids][]", registered_image) 137 }); 138 } 139 $.ajax({ 140 url: '/items/' + gon.item.id, 141 type: "PATCH", 142 data: formData, 143 contentType: false, 144 processData: false, 145 }) 146 }); 147}); 148 149
params全体
Terminal
1[1] pry(#<ItemsController>)> params 2=> <ActionController::Parameters {"utf8"=>"✓", "_method"=>"patch", "authenticity_token"=>"1yaPCT8BvWJWGYnz/aqP2lh4Hdq5X/dA9Fn60xVmcwB94b4H0J/WarKft/GqSBkmeWw649eINI1xvYOkSpqkZg==", 3"item"=>{"item_images_attributes"=>{"0"=>{"image"=>#<ActionDispatch::Http::UploadedFile:0x00007fb9be21b8d8 @tempfile=#<Tempfile:/var/folders/2g/j8x_fnq55gx4s21hb925hfcr0000gn/T/RackMultipart20191112-26990-i8yvzd.jpg>, 4@original_filename="9.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"item[item_images_attributes][0][image]\"; filename=\"9.jpg\"\r\nContent-Type: image/jpeg\r\n">, "id"=>"173"}, 5"1"=>{"id"=>"176"}}, "name"=>"取り引い", "description"=>"テスト", "category"=>"メンズ", "category_id"=>"156", "item_state_id"=>"3", "deliver_expend_id"=>"1", "deliver_method_id"=>"3", "prefecture_id"=>"2", "deliver_day_id"=>"3", "amount"=>"456789"}, 6"registered_images_ids"=><ActionController::Parameters {"ids"=>["173", "176"]} permitted: false>, "controller"=>"items", "action"=>"update", "id"=>"1"} permitted: false>
試したこと
当然attributesをpermitから外せば、空のデータも入り込みませんが、
新規の追加もできません。
fields_forが空のデータを作り出していると仮説立て、
formdataにformの値を入れる直前にfields_forを消したのですが、
結果は変わりませんでした。
何が原因で空のデータが生成されているのか、検討がつかない状況です。
補足情報(FW/ツールのバージョンなど)
Ruby 2.5.1
Rails 5.2.3
回答1件
あなたの回答
tips
プレビュー