ActiveStorage に `attach!` が追加され、添付失敗を例外で検知可能に

rails/rails

ActiveStorage の attach メソッドに、バリデーション失敗時に例外を発生させる attach! が追加されました。これにより、他の ActiveRecord の永続化メソッド群と同様に、戻り値の確認漏れによるサイレントな添付失敗を防げるようになります。

背景

ActiveRecord の永続化メソッドには、バリデーション失敗時に例外を発生させる「bang メソッド」が一貫して存在していました。しかし ActiveStorage の attach にはその対となる attach! が存在せず、API に不整合がありました。

save / save!create / create!update / update! のように、バリデーションを通過しない場合に ActiveRecord::RecordInvalid を発生させるパターンは ActiveRecord の基本的な設計です。一方、attach はバリデーション失敗時に false を返すだけであるため、呼び出し側が戻り値のチェックを忘れると、添付が失敗していたにもかかわらず処理が続行されてしまいます。

この不整合は、attach の戻り値を確認しないコードでサイレントな添付失敗を引き起こすリスクとなっていました。

技術的な変更

Attached::OneAttached::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 の永続化メソッド群と対称性を持つようになりました。戻り値チェックを省略したコードでも添付失敗を確実に検知できるため、特にバリデーションを持つモデルへのファイル添付を行うアプリケーションにおいて、より堅牢なエラーハンドリングが可能になります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
3f88fb12

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文→背景→技術詳細→設計判断→まとめ」という総論→各論→結論の構成が明確で、ガイドラインに完全に準拠しています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトとGitHub PRへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

ActiveRecordやActiveStorageの知識を持つエンジニアを対象とした、適切な技術レベルと表現で記述されています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則を遵守しており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiffの内容と完全に一致しており、正確です。テストに関する説明もDiffの内容に基づいています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「bangメソッド」「ActiveRecord::RecordNotSaved」などの技術用語が正確かつ適切な文脈で使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

attach!の実装内容、発生する例外の種類、設計の背景に関する説明は、すべて技術的に正確で論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiffの内容によって裏付けられており、ハルシネーション(捏造)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号やリンクURLなどの固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの内容を的確に要約しており、変更の核心を捉えています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PRで言及されていないバージョン情報やサポート状況などの外部知識は含まれておらず、事実に基づいた記述に徹しています。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「追加されました」という過去形の表現など、時間に関する表現はPRの事実関係と一致しており正確です。