ActiveStorage に `attach!` が追加され、添付失敗を例外で検知可能に
ActiveStorage の attach メソッドに、バリデーション失敗時に例外を発生させる attach! が追加されました。これにより、他の ActiveRecord の永続化メソッド群と同様に、戻り値の確認漏れによるサイレントな添付失敗を防げるようになります。
背景
ActiveRecord の永続化メソッドには、バリデーション失敗時に例外を発生させる「bang メソッド」が一貫して存在していました。しかし ActiveStorage の attach にはその対となる attach! が存在せず、API に不整合がありました。
save / save!、create / create!、update / update! のように、バリデーションを通過しない場合に ActiveRecord::RecordInvalid を発生させるパターンは ActiveRecord の基本的な設計です。一方、attach はバリデーション失敗時に false を返すだけであるため、呼び出し側が戻り値のチェックを忘れると、添付が失敗していたにもかかわらず処理が続行されてしまいます。
この不整合は、attach の戻り値を確認しないコードでサイレントな添付失敗を引き起こすリスクとなっていました。
技術的な変更
Attached::One と Attached::Many の両クラスに、それぞれ attach! メソッドが追加されました。実装は attach の戻り値を評価し、falsy であれば ActiveRecord::RecordNotSaved を発生させるシンプルな構造です。
Attached::One への追加(attached/one.rb):
def attach!(attachable)
attach(attachable) || raise(ActiveRecord::RecordNotSaved.new("Failed to save the record", record))
end
Attached::Many への追加(attached/many.rb):
def attach!(*attachables)
attach(*attachables) || raise(ActiveRecord::RecordNotSaved.new("Failed to save the record", record))
end
使用方法は既存の attach と同じシグネチャを維持しています。Attached::One では単一引数、Attached::Many では可変長引数(*attachables)を受け取ります:
# Attached::One の例
person.avatar.attach!(params[:avatar])
person.avatar.attach!(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpeg")
# Attached::Many の例
document.images.attach!(params[:images])
document.images.attach!(first_blob, second_blob)
テストでは、name 属性を nil に更新してバリデーションを無効化したユーザーに対して attach! を呼び出し、ActiveRecord::RecordNotSaved が発生することを確認しています。
設計判断
ActiveRecord::RecordNotSaved を使用する点が、この実装の重要な判断です。ActiveRecord::RecordInvalid ではなく RecordNotSaved が選ばれており、これは ActiveRecord::Persistence#save! の実装を参考にした設計です。
実装を attach の薄いラッパーとして保つことで、attach 自体のロジックを変更せずに bang メソッドのセマンティクスを追加しています。attach が返す値(成功時は添付オブジェクト、失敗時は false または nil)をそのまま利用するため、attach の内部実装に依存する箇所が最小限に抑えられています。
まとめ
attach! の追加により、ActiveStorage の API は ActiveRecord の永続化メソッド群と対称性を持つようになりました。戻り値チェックを省略したコードでも添付失敗を確実に検知できるため、特にバリデーションを持つモデルへのファイル添付を行うアプリケーションにおいて、より堅牢なエラーハンドリングが可能になります。