JPAのcascade(CascadeType)6種類の使い分けとON DELETE CASCADEとの違い
JPAのcascadeは、親エンティティに対するpersistやremoveといった操作を、関連先のエンティティにも伝播させる設定です。伝播するのはアプリケーション側のEntityManagerの操作だけで、データベースの外部キー制約に書くON DELETE CASCADEとは実行主体もタイミングも別物です。この記事では、Jakarta Persistence 3.2の規定と各実装のソースを根拠に、6つのCascadeTypeの使い分け、orphanRemovalとの境界、SQL側のCASCADEとの住み分け、そしてcascadeが効かないときの原因を整理します。
まとめ
- cascade属性の既定値は空(
{})で、何も指定しなければ操作は一切伝播しません。 - CascadeTypeは
ALL/PERSIST/MERGE/REMOVE/REFRESH/DETACHの6つで、それぞれEntityManagerの同名メソッドに対応します。 cascade=REMOVEは仕様上@OneToOneと@OneToManyにのみ適用すべきもので、@ManyToOneに付けると移植性が保証されません。- 子を親から切り離しただけで削除したい場合は
orphanRemoval=trueを使います。適用はflush時点です。 - Spring Data JPAの
save()は新規ならpersist、既存ならmergeを呼び分けるため、登録と更新で参照されるCascadeTypeが変わります。 - SQLの
ON DELETE CASCADEはデータベースが行を消す仕組みで、JPAの永続化コンテキストは削除を検知できません。両者は排他ではなく役割が違います。 - JPQLのバルクDELETEはcascadeを一切伝播しません。「削除したのに子が残る」の典型原因です。
以下では、この7点をそれぞれ仕様の記述と実装の挙動に紐づけて説明します。
cascade属性が伝播させる操作と伝播しない操作
EntityManagerの操作単位で伝播する仕組みと既定値
cascadeは「親を保存したら子も保存される」という曖昧な機能ではなく、EntityManagerのどのメソッド呼び出しを関連先にも適用するかを、操作単位で宣言する設定です。@OneToManyのJavadocでは、cascade要素は「The operations that must be cascaded to the target of the association.」と定義され、既定値は空配列({})になっています。つまり指定しなければ伝播はゼロで、親をpersistしても子は保存されません。
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
private Long id;
private LocalDateTime createdAt;
@OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST)
private List<OrderLine> lines = new ArrayList<>();
}
この設定でem.persist(order)を呼ぶと、linesに入っている未保存のOrderLineにもpersistが適用されます。逆にcascadeを外すとどうなるか。Jakarta Persistence 3.2の§3.3.4により、flush時点で関連先が新規または削除済みだった場合はIllegalStateExceptionが投げられ、トランザクションはロールバック対象になります。cascade=PERSISTは「便利だから付ける」ものではなく、この例外を避けるための設定です。
JPA・SQL・組織運営で意味が変わるカスケードの3用法
cascadeという語は文脈ごとに指すものが変わります。Javaの実装で迷いが生じるのは、次の3つが同じカタカナ語で語られるためです。
- JPAのcascade:アプリケーション(EntityManager)が関連エンティティに同じ操作を適用する。この記事の主題です。
- SQLのCASCADE:外部キー制約の
ON DELETE CASCADEやDROP TABLEのオプションとして、データベースが処理を連鎖させる。詳細は後述します。 - 組織運営のカスケードダウン:上位の目標を下位組織へ段階的に展開する手法で、システムとは無関係です。用語の詳細は組織の目標や戦略を社員まで浸透させるカスケードダウンで扱っています。
JPAそのものの位置づけや、Hibernateなど実装の選び方を先に押さえたい場合は、ORM(Object-Relational Mapping)とはとJava ORM(O/Rマッパー)の種類と選び方が前提知識になります。
CascadeType 6種類の挙動と選定基準
jakarta.persistence.CascadeTypeの定数は6つです。DETACHのみJPA 2.0で追加され、他はJPA 1.0から存在します。
| 定数 | 対応する操作 | 主な用途 | 注意点 |
|---|---|---|---|
| ALL | 下記5つすべて | 親が子を専有する構成 | REMOVEを含む点の見落としに注意 |
| PERSIST | em.persist() | 親と一緒に子を新規登録 | 未指定だとflushでIllegalStateException |
| MERGE | em.merge() | detached状態の親子をまとめて更新 | Spring Data JPAのsave()経路で必要 |
| REMOVE | em.remove() | 親の削除に子を追従 | OneToOne・OneToManyのみ |
| REFRESH | em.refresh() | 親子まとめてDBから再読込 | 子の未コミット変更が破棄される |
| DETACH | em.detach() | 親子まとめて管理外へ | JPA 2.0で追加 |
表の「対応する操作」がそのまま判断基準です。使わないメソッドの伝播まで宣言する必要はありません。
CascadeType.PERSIST:flush時の例外を避ける設定
§3.3.2は、persistの対象Xが既に管理下にある場合でも「the persist operation is cascaded to entities referenced by X」と規定しています。親が既存レコードで子だけが新規、という状況でもcascade=PERSISTは効きます。一方、XがdetachedのときはEntityExistsException、あるいはflush/commit時にPersistenceExceptionが投げられる可能性があると規定されています。detachedなインスタンスをpersistに渡す設計は仕様の範囲外だと考えてください。
CascadeType.MERGE:更新経路で必要になる設定
mergeはdetachedなインスタンスの状態を管理下のコピーへ写す操作です。§3.3.7.1では、cascade=MERGEまたはcascade=ALLが付いた関連についてのみ「Y is merged recursively as Y’」と再帰的なマージが規定されています。cascade指定が無い関連は、マージ後のX’から辿っても同じ永続性IDを持つ管理オブジェクトが返るだけで、子の変更内容は書き戻りません。画面から受け取ったDTOを親子まとめて更新する処理でcascade=MERGEが要るのは、この規定のためです。
CascadeType.REMOVE:OneToOne・OneToManyに限定される削除伝播
§2.11は「The cascade=REMOVE specification should only be applied to associations that are specified as OneToOne or OneToMany. Applications that apply cascade=REMOVE to other associations are not portable.」と明記しています。@ManyToOneや@ManyToManyにREMOVEを付ける構成は、動く実装があっても移植性が保証されません。@ManyToOne側にcascade=ALLを付ければ、注文明細を1件消しただけで注文本体まで削除される事故につながります。
なお§3.3.3により、detachedなインスタンスに対するremoveはIllegalArgumentExceptionになります。削除前にem.find()で管理下に戻すのが基本形です。
CascadeType.REFRESHとCascadeType.DETACH:使いどころが限られる2つ
REFRESHは親のリフレッシュに合わせて子もデータベースから読み直します。永続化コンテキスト上の未コミットの変更は捨てられるため、用途はバッチ処理で参照系の整合を取り直す場面に限られます。DETACHは親を管理外にする際に子も同時に切り離す設定で、JPA 2.0での追加です。どちらも通常のCRUDでは指定しません。「とりあえずALL」を避ける理由の1つが、この2つまで有効になる点にあります。
Hibernate独自のCascadeTypeがHibernate 7で削除予定になった経緯
Hibernateにはorg.hibernate.annotations.CascadeTypeという同名の列挙型があり、@Cascadeアノテーションと組み合わせてJPA標準に無いLOCKやREPLICATE、DELETE_ORPHANを指定できました。Hibernate ORM 7.4.5のJavadocでは、この列挙型に@Deprecated(since = "7", forRemoval = true)が付き「Use the JPA-defined CascadeType. This enumeration will be removed to alleviate the duplication in naming.」と記載されています。DELETE_ORPHANはJPA 2.0で追加されたorphanRemoval属性に置き換わっています。既存コードに@Cascadeが残っている場合は、Hibernateのバージョンを上げる前にjakarta.persistence側へ寄せてください。
CascadeType.ALLを選んでよい条件と避けるべき条件
ALLは5つの操作すべてを伝播させる指定です。付けてよいのは、子が親に専有され、親を消したら子も存在意義を失う構成に限られます。注文と注文明細、伝票とその行、といった関係が該当します。この条件を満たす関連では、ALLとorphanRemovalの併用が実務上の既定形になります。
@OneToMany(mappedBy = "order",
cascade = CascadeType.ALL,
orphanRemoval = true)
private List<OrderLine> lines = new ArrayList<>();
ただし、この組み合わせでALLが担っているのはPERSISTとMERGEです。削除の伝播はorphanRemoval側で保証されます(根拠は次章)。
逆に、次の3つに当てはまる関連へALLを付けるのは避けてください。第1に、@ManyToOne側です。§2.11の移植性の規定に反するうえ、子から親を巻き込んで削除する経路ができます。第2に、マスタデータを指す関連です。商品や部署のような他レコードからも参照されるエンティティにREMOVEが伝播すると、1件の削除が広範囲に波及します。第3に、双方向関連の両側です。両側にALLを付けると同じ操作が往復し、削除順序の制御が難しくなります。
判断に迷う場合は、ALLではなく{CascadeType.PERSIST, CascadeType.MERGE}のように必要な操作だけを列挙してください。REMOVEを明示的に外しておけば、事故の規模は登録・更新の範囲に収まります。
orphanRemovalとCascadeType.REMOVEの適用タイミングの違い
この2つは似た結果になりますが、発火条件が違います。cascade=REMOVEは親に対してremoveが呼ばれたときに子へ伝播します。orphanRemovalは子がコレクションから外された、あるいは関連がnullになったときに、外された子へremoveを適用します。
| 比較軸 | cascade=REMOVE | orphanRemoval=true |
|---|---|---|
| 発火条件 | 親のremove | 関連からの切り離し |
| 適用タイミング | remove呼び出し時に伝播 | flush時点 |
| 既定値 | 指定なし(空) | false |
| 適用可能な関連 | OneToOne・OneToMany | OneToOne・OneToMany |
見落とされやすいのが、両者の包含関係です。§2.11はorphanRemovalが有効な関連について、親にremoveを適用すれば削除は関連先へ伝播すると規定し、「(and hence it is not necessary to specify cascade=REMOVE for the relationship)」と続けています。orphanRemoval=trueを付けた時点で、親のremoveに対する削除の伝播は仕様として保証され、cascade=REMOVEの指定は不要です。ALLと併用する構成が成立するのも、ALLの役割がPERSISTとMERGEに寄っているからです。
適用タイミングにも注意が必要です。§2.11は「The remove operation is applied at the time of the flush operation.」と規定しており、切り離した瞬間にDELETEが走るわけではありません。加えて同節は「Portable applications must otherwise not depend upon a specific order of removal, and must not reassign an entity that has been orphaned to another relationship or otherwise attempt to persist it.」として、削除順序への依存と、孤児になったエンティティの別関連への付け替えを禁じています。子を別の親へ移す処理は、orphanRemovalとは別の設計で組んでください。
例外規定もあります。§2.11は「If the entity being orphaned is a detached, new, or removed entity, the semantics of orphanRemoval do not apply.」としており、detachedな子をコレクションから外しても削除されません。orphanRemovalが効かないという相談の多くは、対象が管理下に無いケースです。
JPAのcascadeとSQLのON DELETE CASCADEの違い
同じ「カスケード」でも、JPAの設定はアプリケーションが操作を連鎖させ、SQLのCASCADEはデータベースが処理を連鎖させます。どちらを使うかは、削除の主導権をどちらに置くかの設計判断です。
| 比較軸 | JPAのcascade | ON DELETE CASCADE |
|---|---|---|
| 実行主体 | アプリケーション | データベース |
| 定義場所 | エンティティのアノテーション | 外部キー制約のDDL |
| 発行されるSQL | 子ごとにDELETE | 親のDELETE 1文のみ |
| 永続化コンテキスト | 同期される | 同期されない |
| JPA以外の経路 | 効かない | 効く |
子が数千件ある親を消す場面では、cascade=REMOVEは件数分のDELETE文を発行します。件数が読めない関連には、次に説明するDB側の制約を検討してください。
外部キー制約のON DELETE CASCADE:DB側で行を消す仕組みと既定値
FK制約(外部キー制約)は、参照整合性を保つために子テーブルの列が親テーブルの行を指すことを保証する仕組みです。PostgreSQLの公式ドキュメントは、削除時の動作としてCASCADEを「specifies that when a referenced row is deleted, row(s) referencing it should be automatically deleted as well」と定義しています。重要なのは既定値で、「The default ON DELETE action is ON DELETE NO ACTION」と明記されています。外部キーを張っただけでは連鎖削除は起きません。参照されている行を消そうとすればエラーになります。
ALTER TABLE order_line
ADD CONSTRAINT fk_order_line_order
FOREIGN KEY (order_id) REFERENCES orders (id)
ON DELETE CASCADE;
RESTRICTとの違いも押さえておくと選択が早くなります。同ドキュメントはRESTRICTを「a stricter setting than NO ACTION」とし、制約チェックをトランザクション後半まで遅延できない点を挙げました。削除を止めたいだけならNO ACTIONで十分。遅延チェックも許したくない場合がRESTRICTの出番です。テーブル定義とスキーマ設計の全体像はデータベースのスキーマとは?三層スキーマ(外部・概念・内部)の違いと設計・管理の実務で整理しています。
DROP TABLE CASCADE:行ではなく依存オブジェクトの削除
同じCASCADEでも、DROP TABLEに付けるものは意味がまったく違います。PostgreSQLのドキュメントは「Automatically drop objects that depend on the table (such as views), and in turn all objects that depend on those objects」と説明しており、消えるのはビューや制約といった依存オブジェクトです。子テーブルの行を消すオプションではありません。既定はRESTRICTで、依存オブジェクトがあればDROPは拒否されます。検証環境でDROP TABLEにCASCADEを付けてビューが消えた、という事故はこの取り違えが原因です。
@OnDeleteでDDLにon delete cascadeを出す場合のトレードオフ
HibernateにはDDL側のON DELETE句を生成する@OnDeleteがあります。ORM 7.4.5のJavadocは「This code results in an on delete cascade clause in the DDL definition of the foreign key.」とし、続けて「The affect of @OnDelete(action = CASCADE) is quite different to CascadeType.REMOVE.」と明言しています。Javadocが主例として示すのは、外部キーを持つ@ManyToOne側への付与です。
@ManyToOne
@OnDelete(action = OnDeleteAction.CASCADE)
private Order order;
削除が親の1文で済むためパフォーマンス面では有利で、同Javadocも「It’s more efficient to delete a row via on delete cascade」としています。Hibernateが子への個別DELETEを実行時に省略するためです。ただし代償が2つあります。1つは同期のずれで、Javadocは「Like database triggers, on delete actions can cause state held in memory to lose synchronization with the database.」と注意し、二次キャッシュから削除済みインスタンスが除かれない可能性にも触れています。読み込み済みの子エンティティは、DB側で行が消えた後もメモリ上に残ります。
もう1つは適用範囲です。@OnDeleteがDDLに反映されるのはHibernateにスキーマを生成させる場合に限られます。FlywayやLiquibaseでマイグレーションを管理している場合、アノテーションを付けても実際の制約は変わりません。移行SQL側にON DELETE CASCADEを書く必要があります。
Spring Data JPAでcascadeが効く条件と効かない条件
Spring Data JPAのsave()は、リポジトリ実装であるSimpleJpaRepositoryの中で新規判定を行い、新規ならpersist、そうでなければmergeを呼びます。つまり登録時はcascade=PERSIST、更新時はcascade=MERGEが参照されるという分岐が、アプリケーションのコードからは見えないところで起きています。「新規登録では子も保存されるのに、更新すると子の変更が消える」という症状は、cascadeにPERSISTしか指定していない場合の典型です。
削除系は経路で挙動が分かれます。deleteByIdは内部でエンティティを取得してからremoveを呼ぶため、cascade=REMOVEが効きます。一方、@Modifying付きのDELETEクエリを自前で定義した場合、エンティティのライフサイクルを経由しないためcascadeは働きません。
また、detachedなエンティティを扱う頻度はspring.jpa.open-in-viewの設定で変わります。既定値と無効化の判断基準はspring.jpa.open-in-viewとは?OSIVの既定値trueと起動時警告の消し方で整理しました。関連するアノテーションの全体像はSpring Bootの主要なアノテーション一覧を参照してください。
cascadeが効かない・意図せず効く場合の原因
相談として多い順に、原因と切り分け方を挙げます。
JPQLバルクDELETEによる非伝播
Jakarta Persistence 3.2の§4.11は「A delete operation only applies to entities of the specified class and its subclasses. It does not cascade to related entities.」と規定しています。cascade=ALLでもorphanRemovalでも伝播しません。さらに「The persistence context is not synchronized with the result of the bulk update or delete.」とあるため、実行後の永続化コンテキストには消えたはずのエンティティが残ります。
em.createQuery("DELETE FROM Order o WHERE o.createdAt < :limit")
.setParameter("limit", limit)
.executeUpdate();
子も消したいなら、子側のDELETEを先に発行するか、外部キーにON DELETE CASCADEを付けてDB側に任せます。
detachedインスタンスをpersistに渡した場合の例外
Hibernateは内部でPersistentObjectExceptionを投げ、メッセージに「Detached entity passed to persist」を含めます。ただしEntityManager経由の呼び出しではExceptionConverterImplがこれをEntityExistsExceptionへ変換するため、アプリケーションが捕捉するのはJPA標準の例外です(メッセージは維持されます)。§3.3.2の規定どおりの挙動です。IDが設定済みのオブジェクトをそのまま保存しようとしたケースが大半で、mergeに切り替えるか、findで取得した管理下のインスタンスへ値を写してから保存します。
双方向関連の片側更新による外部キーのnull
子をコレクションに追加しても子側の親参照を設定しなければ、外部キー列がnullのまま挿入されます。cascadeの設定とは無関係な、関連の持ち主(owning side)の問題です。追加と削除を1つのメソッドにまとめておくと防げます。
public void addLine(OrderLine line) {
lines.add(line);
line.setOrder(this);
}
public void removeLine(OrderLine line) {
lines.remove(line);
line.setOrder(null);
}
ここでList.removeはequalsとhashCodeの実装に依存します。両者を業務キーで実装していないエンティティでは対象が一致せず、コレクションから外れないためorphanRemovalも発火しません。
@ManyToOne側のcascade=ALLによる親の巻き込み削除
意図せず親まで消える場合は、@ManyToOne側にcascade=ALLが付いていないか確認してください。§2.11の対象外の使い方であり、実装によって挙動が変わります。
よくある質問
cascadeの既定値は何ですか。
@OneToManyなどのcascade要素の既定値は空配列({})で、伝播は一切起きません。orphanRemovalの既定値はfalse、@OneToManyと@ManyToManyのfetchの既定値はLAZYです(@ManyToOneと@OneToOneのfetch既定はEAGER)。外部キー制約側のON DELETEもPostgreSQLでは既定がNO ACTIONのため、アノテーションとDDLのどちらにも明示しない限り、削除が連鎖することはありません。
CascadeType.ALLとorphanRemoval=trueは併用すべきですか。
子が親に専有される関連であれば併用します。ALLだけでは、コレクションから子を1件外す操作に反応しないためです。画面から明細を1行削除する仕様なら、orphanRemoval=trueが必要になります。逆に、子が他の親にも付け替えられる関連ではorphanRemovalを付けません。付け替えのたびに削除が走ります。
@ManyToOneにcascade=REMOVEを付けてもよいですか。
避けてください。Jakarta Persistence 3.2の§2.11は、cascade=REMOVEを適用すべき関連を@OneToOneと@OneToManyに限定し、それ以外に適用したアプリケーションは移植性が無い(not portable)と規定しています。子から親を消す方向の伝播は、削除範囲が予測しにくくなる点でも避けるべきです。
JPQLのDELETE文で子が消えないのはなぜですか。
Jakarta Persistence 3.2の§4.11により、バルクDELETEは指定したクラスとそのサブクラスにしか適用されず、関連エンティティへはカスケードしないためです。cascadeやorphanRemovalの設定は参照されません。子を先に削除するJPQLを発行するか、外部キーにON DELETE CASCADEを設定してデータベース側で連鎖させます。
ON DELETE CASCADEとJPAのcascadeはどちらを使うべきですか。
削除件数が多く、JPA以外の経路(管理用SQL、バッチ、他システム)からも削除が発生するならON DELETE CASCADEが向きます。DELETEが1文で済み、経路に依存せず参照整合性が保たれるためです。ただし削除された子は永続化コンテキストや二次キャッシュに残り続けるため、同一トランザクション内で子を参照するアプリケーションではcascade=REMOVEを選びます。両方を設定しても二重に削除が走るわけではありませんが、挙動の追跡が難しくなります。どちらか一方に寄せてください。