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

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

新規登録して質問してみよう
ただいま回答率
85.49%
Ruby on Rails 5

Ruby on Rails 5は、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Ajax

Ajaxとは、Webブラウザ内で搭載されているJavaScriptのHTTP通信機能を使って非同期通信を利用し、インターフェイスの構築などを行う技術の総称です。XMLドキュメントを指定したURLから読み込み、画面描画やユーザの操作などと並行してサーバと非同期に通信するWebアプリケーションを実現することができます。

Q&A

解決済

1回答

1426閲覧

accepts_nested_attributes_forを使って複数モデルの同時保存(update)を行いたい

Hiro191110

総合スコア5

Ruby on Rails 5

Ruby on Rails 5は、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Ajax

Ajaxとは、Webブラウザ内で搭載されているJavaScriptのHTTP通信機能を使って非同期通信を利用し、インターフェイスの構築などを行う技術の総称です。XMLドキュメントを指定したURLから読み込み、画面描画やユーザの操作などと並行してサーバと非同期に通信するWebアプリケーションを実現することができます。

0グッド

0クリップ

投稿2019/11/11 15:02

編集2019/11/12 14:08

実現したいこと

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

気になる質問をクリップする

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

winterboum

2019/11/11 23:53

「登録済み画像が2枚ある状態で、新規画像を追加した際の中身」の"0"=>の中の "9.jpg" が追加したfileの名前ですね?
Hiro191110

2019/11/12 07:30

説明不足ですみません。 ご理解の通り、”9.jpg”が新規追加したfileの名前となります。
guest

回答1

0

ベストアンサー

ちと不思議だったので、勘違いだと行けないと思いfile名のけん確認させていただきました。JSは苦手なので頓珍漢な答えになってるかもしれないです。そうなってたらケケと笑ってスルーして下さい。

不思議なのは2つ
1)viewにはnameをitem[item_images_attributes][#{@item.item_images.length}][image]とあるのに、paramsではitem[item_images_attributes][0][image] と 2でなく0になっていること
2)Viewでは item_image.file_field が一つしか無いのに、paramsでは二つ有ること

JSがなにかしているのか?
1)についてはブラウザでhtmlソースをみて確認いただけないでしょうか。

苦手ながらも読んでみたのですが viewにはない edit_item クラスのイベントを拾っていて、まずそこで理解できなくなっています。registered_images_ids[ids][]も突然出てきますし。
viewは省略していますか?全部載せると大きすぎる?
paramsも

投稿2019/11/12 13:03

winterboum

総合スコア23324

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Hiro191110

2019/11/12 14:52

ご回答ありがとうございます。 要点に絞ろうと思い省略しておりました。 改めてコードを見直して、質問の画像投稿に関わる部分(以下)を追加しました。 Controller、View、Javascript、params ViewとControllerは長すぎて字数制限に掛かるため一部省略してますが、 質問に関連するコードは網羅できてるかと思います。 1)出品を実装した際のJSのコードが影響していました。 ただ画像の枚数に合わせて[]の中の数字を変えても、空のデータが飛んでくることに変わりはありませんでした。 2) すみません、paramsの中にfile_fieldが2つあるというのは、具体的にどの部分を指しているのでしょうか?
winterboum

2019/11/13 00:06

2) "item"=>{"item_images_attributes"=>{"0"=>{"image"=>#<Ac...>, "id"=>"173"}, "1"=>{"id"=>"176"}} の部分です。1のほうが言われている空のデータですね。 id が二つ飛んでるのが?? なのと、そのIDは 既存データのregistered_images_ids"と同じですね。 この辺りなにかわかります?
Hiro191110

2019/11/13 13:55

解決しました! {"id"=>"176"}は登録済み画像データのidです。 プレビュー表示及び削除機能を実装するために、ブラウザに読み込ませていた登録済み画像データにより、 hidden_fieldが生成されており、新規登録分と一緒にデータが送信され、これが空なのでエラーを吐いてました。 hidden_fieldは全く意図していなかった為、完全に盲点でした。 あとparamsの中身がしっかり読めてなかったです。 submitを押した後、hidden_fieldをjsで削除するようにしたところ、 新規画像の追加はできるようになりました。 ありがとうございました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.49%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問