はじめに
こんにちは、プログラミングスクール RUNTEQ を卒業したばかりの初学者、りたと申します。
卒業制作であるポートフォリオ作成時に、複数枚画像を投稿できる投稿機能を作成したのですが、画像付きの投稿を画像のアップロードなしで編集すると、もともと添付していた画像が消えてしまうという問題に遭遇しました。
この問題に対する記事があまり見つけられなかったので、忘備録的に原因と対処法について書いていこうと思います。
注意
初学者のため、内容に誤りがある可能性があります。
間違いなどありましたら、コメント欄等でご指摘いただけますと大変助かります。
開発環境
- WSL(Ubuntu 22.04LTS)
- Docker
- Ruby 3.3.6
- Rails 7.2
- ActiveStorage使用
前提
file_fieldにmultiple: trueを設定したときのみこの現象が発生しました。
modelにはhas_many_attachedを設定しています。
class Review < ApplicationRecord
has_many_attached :images
...
...
<%= f.file_field :images, multiple: true %>
...
この状態で画像添付済みの投稿を、画像の新たなアップロードはしない状態で編集すると、添付画像に変更は入らないはず…なのですが、消えます。
このままでは、投稿を編集するたびに画像をアップロードしなおさなければならなくなります。
どうすれば、編集しても画像を添付したままにしておけるのでしょうか。
対処法
file_fieldsにinclude_hidden: falseオプションを追加するだけで、画像が消えなくなります。ヤッター!!
...
# include_hidden: falseを追加
<%= f.file_field :images, multiple: true, include_hidden: false %>
...
あるいは、config/application.rb等に、下記の設定を追加するのでも対応できます。
...
config.active_storage.multiple_file_field_include_hidden = false
...
参考:Railsガイド - config.active_storage.multiple_file_field_include_hidden
ところで、何故デフォルト設定のままだと画像が消えてしまうのでしょうか。
原因を追っていこうと思います。
原因
どうも空文字が送信されてしまっている
送信されたパラメータを確認すると、画像のところで空文字が一つだけ入った配列が送信されてしまっています(見やすいように適当に改行を入れています)。
...
params: {
"_method"=>"patch",
"authenticity_token"=>"[FILTERED]",
"review"=>{
"title"=>"テスト3",
"images"=>[""], # 空文字の入った配列が送信されてる!
"body"=>"テスト\r\n\r\n編集",
"product_name"=>"ミッドナイトブルー",
"paper"=>"", "pen"=>""},
"commit"=>"更新する",
"controller"=>"reviews",
"action"=>"update",
"id"=>"9"
}
...
何故空文字が送信されてしまっているのでしょうか。ここに先ほどオプションで追加したinclude_hiddenが絡んできます。
file_fieldにmultiple: trueを設定すると、デフォルトでhidden_fieldが一緒に作成される
file_fieldのソースコードを見に行くと、以下のコメントが確認できます。
...
# * <tt>:include_hidden</tt> - When <tt>multiple: true</tt> and <tt>include_hidden: true</tt>, the field will be prefixed with an <tt><input type="hidden"></tt> field with an empty value to support submitting an empty collection of files. Since <tt>include_hidden</tt> will default to <tt>config.active_storage.multiple_file_field_include_hidden</tt> if you don't specify <tt>include_hidden</tt>, you will need to pass <tt>include_hidden: false</tt> to prevent submitting an empty collection of files when passing <tt>multiple: true</tt>.
...
multiple: trueが設定されており、かつ、include_hidden: trueが設定されていると、<input type="hidden">が追加される、というようなことが書いてあります。
include_hiddenオプションを設定しない場合は、config.active_storage.multiple_file_field_include_hiddenの設定が適用されます。
Rails7以降、config.active_storage.multiple_file_field_include_hiddenのデフォルト設定はtrueになっているので、multiple: trueを設定しただけで、<input type="hidden">が追加されるようになっています。
この結果、画像のアップロードなしで投稿を編集した際にhidden_fieldの値(空文字)が送信されてしまうようです。
ところで、何故hidden_fieldが追加されただけでもともとの添付画像が消えてしまうのでしょうか?こちらにはActiveStorageの仕様が絡んできます。
ActiveStorageは空文字のみが送信されると、既存の添付画像を消してしまう。
ここからはchatGPTの力も借りつつ、ActiveStorageのソースコードを見ていきます。
見ていくのはここです。
...
def #{name}=(attachables)
attachables = Array(attachables).compact_blank
pending_uploads = attachment_changes["#{name}"].try(:pending_uploads)
attachment_changes["#{name}"] = if attachables.none?
ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
else
ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables, pending_uploads: pending_uploads)
end
end
...
after_save { attachment_changes[name.to_s]&.save }
...
attachables = Array(attachables).compact_blankで渡された値を配列にし、空文字を除去しています。
compact_blankはRails6.1から追加されたメソッドで、配列内の空白の要素を取り除いた新しい配列を返すメソッドです(compact_blank)。
そして、none?メソッドを使って変数attachablesの配列の要素がすべて偽かどうかを判定し、偽であればActiveStorage::Attached::Changes::DeleteManyインスタンスをattachment_changesハッシュのカラム名のキーに代入します。
その後、after_saveコールバックでsaveやupdateが実行された後に、ActiveStorage::Attached::Changes::DeleteManyのsaveメソッドが実行されます。
ActiveStorage::Attached::Changes::DeleteManyのsaveメソッドの中身は以下のようになっています。
...
def save
record.public_send("#{name}_attachments=", [])
end
...
name_attachments=はhas_many_attached内で定義されたアソシエーションによって自動生成されたセッターです。
このセッターは内部でwriterというメソッドを呼んでおり、このwriterはさらにreplaceというメソッドを呼んでいます。
このreplaceはRubyのメソッドではなくRailsで定義されている方で、
...
# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
def replace(other_array)
other_array.each { |val| raise_on_type_mismatch!(val) }
original_target = skip_strict_loading { load_target }.dup
if owner.new_record?
replace_records(other_array, original_target)
else
replace_common_records_in_memory(other_array, original_target)
if other_array != original_target
transaction { replace_records(other_array, original_target) }
else
other_array
end
end
end
...
という感じに、変更されたもののみが削除あるいは追加されるものとなっています。
これにより、空配列([])が渡されると、差分である既存の添付画像がすべて削除される、という挙動になります。
なので、hidden_fieldで空文字が送信されると、既存の添付画像がすべて消えてしまうということになるわけですね。
まとめ
長々と書きましたが、つまりfile_fieldとActiveStorageの仕様が組み合わさって、デフォルト設定だと添付画像が消えてしまうようでした。
そのため、file_fieldにinclude_hidden: falseを設定したり、そもそもconfig.active_storage.multiple_file_field_include_hiddenを設定しておいて、hidden_fieldが作成されないようにすれば、画像のアップロードなしで投稿を編集しても既存の添付画像が消えなくなります。
ファイルアップロード機能に関して、AcitveStorageを採用している方で類似の現象に困っている方の力になることが出来れば幸いです。
今回、執筆に当たって、この問題に対処した当時調べた内容に追加して、そもそもなぜ空文字が送信されると画像が消されてしまうのかについて、chatGPTにRailsのソースコードの解説をしてもらって調べてみました。
まだまだ自力で読むのは難しいのですが、自分が使っているフレームワークについて、こうして記事を書きながら深く知っていければいいなと思います。
参考文献
- Rails7で複数画像file_filedを使っていると空画像が入る対策
- Rails: ActiveStorageでファイルを削除するときは、単にnilで更新するだけでいい
- activestorage/lib/active_storage/attached/model.rb
- actionview/lib/action_view/helpers/form_helper.rb
- Railsガイド - config.active_storage.multiple_file_field_include_hidden
- activestorage/lib/active_storage/attached/changes/delete_many.rb
- activerecord/lib/active_record/associations/builder/collection_association.rb
- activerecord/lib/active_record/associations/collection_association.rb
- compact_blank
- Rubyリファレンスマニュアル - instance method Array#none?