JPAのcascadeは、親エンティティに対するpersistやremoveなどのEntityManagerの操作を、関連先のエンティティにも適用する設定です。データベースの外部キー制約に書くON DELETE CASCADEとは、実行する主体もタイミングも違います。この記事では、Jakarta Persistence 3.2の条文と、Hibernate ORM 7.4で実際に発行されたSQL・例外メッセージをもとに、6つのCascadeTypeの使い分け、orphanRemovalとの境界、SQL側のCASCADEとの住み分け、cascadeが効かないときの原因を整理します。
まとめ
- cascade属性の既定値は空で、指定しなければ操作は一切伝播しません。
- CascadeTypeは
ALL/PERSIST/MERGE/REMOVE/REFRESH/DETACHの6つです。策定中のJakarta Persistence 4.0(4.0.0-M7)でも6つのままです。 ALLとorphanRemoval=trueの併用が向くのは、子が1つの親に専有される関連だけです。@ManyToOneや@ManyToManyにREMOVEを含めると、他の行と共有している親や相手側まで消しにいきます。- Hibernateでは、
mappedBy側のコレクションにcascadeが無いと、子は例外も出ずに保存されません。 - cascade=REMOVEは子の件数分のDELETEを発行します。DB側の
ON DELETE CASCADEに任せるなら、@OnDeleteは@OneToMany側にcascade=REMOVEと一緒に付けます。 - JPQLのバルクDELETEと、Spring Data JPAの
deleteAllInBatch系はcascadeを伝播しません。 - Hibernate独自の
@Cascadeは7で非推奨になり、8.0.0.Beta3のjarでは削除されています。
cascadeが伝播させる操作と既定値
EntityManagerの操作単位で伝播する仕組み
cascadeは「親を保存したら子も保存される」という漠然とした機能ではありません。EntityManagerのどのメソッド呼び出しを関連先にも適用するかを、操作ごとに宣言する設定です。Jakarta Persistence 3.2仕様の@OneToOneの要素表では、cascadeの既定値を「No operations are cascaded.」としています。@OneToManyなども同じで、指定しなければ伝播はゼロです。
この記事の実測は、次の注文と注文明細の関連を基準にしています。掲載例はHibernate用の抜粋です。JPA仕様に準拠させる場合は、両エンティティにpublicまたはprotectedの引数なしコンストラクタを明示します。Hibernate ORM 7.4.13.FinalとH2 2.5.252、Java 25で実行し、Spring Boot 4.1.1のBOMが指定するHibernate 7.4.5.Finalでも同じSQLと例外になることを確かめました。
@Entity
@Table(name = "orders")
class Order {
@Id @GeneratedValue Long id;
String note;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
List<OrderLine> lines = new ArrayList<>();
void addLine(OrderLine l) { lines.add(l); l.order = this; }
void removeLine(OrderLine l) { lines.remove(l); l.order = null; }
}
@Entity
@Table(name = "order_line")
class OrderLine {
@Id @GeneratedValue Long id;
String item;
@ManyToOne(fetch = FetchType.LAZY) Order order;
}
JPAそのものの位置づけや実装の選び方を先に押さえたい場合は、ORMとは?O/Rマッパーの仕組み・メリットとデメリットとJava ORM(O/Rマッパー)の種類と選び方が前提知識になります。
JPA・SQL・組織運営で意味が変わるカスケードの3用法
「cascade 意味」で調べると、別の3つが同じ語で説明されています。
- JPAのcascade:アプリケーション(EntityManager)が関連エンティティに同じ操作を適用する。この記事の主題です。
- SQL・DBのCASCADE:外部キー制約の
ON DELETE CASCADEやDROP TABLE ... CASCADEで、データベースが処理を連鎖させる。後半の章で扱います。 - 組織運営のカスケードダウン:上位の目標を下位組織へ段階的に展開する手法で、システムとは無関係です。詳しくはカスケードダウンの意味とブレイクダウンとの違いで扱っています。
CascadeType 6種類の挙動と選定基準
jakarta.persistence.CascadeTypeの定数は6つです。DETACHだけがJPA 2.0で追加され、他はJPA 1.0からあります。
| 定数 | 伝播する操作 | 主な用途 | 注意点 |
|---|---|---|---|
| ALL | 下記5つすべて | 親が子を専有する構成 | REMOVEを含む |
| PERSIST | em.persist() | 親と一緒に子を新規登録 | 新規登録の経路で必要 |
| MERGE | em.merge() | detachedな親子をまとめて更新 | 無いと子の変更が書き戻らない |
| REMOVE | em.remove() | 親の削除に子を追従 | OneToOne・OneToManyのみ |
| REFRESH | em.refresh() | 親子まとめてDBから再読込 | 子の未反映の変更が消える |
| DETACH | em.detach() | 親子まとめて管理外へ | JPA 2.0で追加 |
CascadeType.PERSIST:親1件と子3件で発行されるINSERT
上のOrderに明細を3件追加してem.persist(order)を呼ぶと、この実測ではcommit時に次のINSERTが4文出ます。採番用SELECTは省略しています。親を先に、子を後に入れる順序はHibernateが決めます。
insert into orders (note,id) values (?,?)
insert into order_line (item,order_id,id) values (?,?,?)
insert into order_line (item,order_id,id) values (?,?,?)
insert into order_line (item,order_id,id) values (?,?,?)
仕様の§3.3.2は、persistの対象Xが既に管理下にある場合でも「the persist operation is cascaded to entities referenced by X」と定めています。既存の親に新しい子を足す場合でも、cascade=PERSISTがあれば子は登録されます。
CascadeType.MERGE:detachedな子の変更の反映条件
mergeはdetachedなインスタンスの状態を管理下のコピーへ写す操作です。§3.3.7.1は、cascade=MERGEかALLが付いた関連だけを「Y is merged recursively as Y’」と再帰的にマージすると定めています。cascadeをPERSISTだけにしたカートで、detachedな明細の数量を1から5に変えて親をmergeしたところ、UPDATEは発行されず、DBの値は1のままでした。例外も出ません。画面から受け取った親子をまとめて更新する処理でMERGEが必要になるのは、この規定によるものです。
CascadeType.REMOVE:子の件数分のDELETE
cascade=ALLのOrder(明細3件)をem.remove()すると、明細を1回のSELECTで読み込んだうえで、子のDELETEを3本、親のDELETEを1本発行しました。子が数千件ある親なら、DELETEも数千本になります。関連を読み込むSELECTの回数が膨らむ問題はN+1問題とは?発行クエリ数の実測と検出方法で扱っています。
select l1_0.order_id,l1_0.id,l1_0.item from order_line l1_0 where l1_0.order_id=?
delete from order_line where id=?
delete from order_line where id=?
delete from order_line where id=?
delete from orders where id=?
§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に付けたときに起きることは、次の章で実測します。
CascadeType.REFRESHとCascadeType.DETACH:通常のCRUDでは不要な2つ
REFRESHは親の再読込に合わせて子もDBから読み直すため、子に加えたまだflushしていない変更が捨てられます。DETACHは親を管理外にするときに子も切り離します。どちらも通常の登録・更新・削除では指定しません。「とりあえずALL」を避ける理由の1つは、この2つまで有効になることです。
Hibernate独自CascadeTypeの非推奨化と8.0ベータでの削除
Hibernateにはorg.hibernate.annotations.CascadeTypeという同名の列挙型があり、@Cascadeと組み合わせてLOCKやREPLICATEなどJPAに無い操作を指定できました。Hibernate 7.4のJavadocでは@Deprecated(since="7", forRemoval=true)が付き、「Use the JPA-defined CascadeType.」と案内されています。2026年9月25日に公開されたhibernate-core 8.0.0.Beta3のjarを確認すると、org.hibernate.annotations.CascadeとCascadeTypeのクラスは含まれていませんでした。7.4.13.Finalのjarにはまだあります。@Cascadeが残っているコードは、8へ上げる前にjakarta.persistence.CascadeTypeへ寄せてください。
CascadeType.ALLの採用条件と共有関連への適用リスク
ALLとorphanRemovalの併用が向く親子の条件
ALLを付けてよいのは、子が1つの親に専有され、親が消えたら子も意味を失う関連に限られます。注文と注文明細、伝票とその行が該当し、冒頭のOrderのようにorphanRemovalと併用するのが実務の基本形です。ただし§2.11は、orphanRemovalが有効な関連では親のremoveが子へ伝播すると定め、「(and hence it is not necessary to specify cascade=REMOVE for the relationship)」と続けています。この組み合わせでもALLはREFRESHとDETACHを含みます。登録・更新の伝播だけが必要なら、orphanRemoval=trueにPERSISTとMERGEを組み合わせれば足ります。
判断に迷う関連には、ALLではなく{CascadeType.PERSIST, CascadeType.MERGE}のように必要な操作だけを並べます。orphanRemovalも無効で、DB側にも連鎖削除がなければ、PERSISTとMERGEだけの指定では親のremoveは子へ伝播しません。エンティティの境界や関連の持たせ方から見直す場合は、フルスクラッチ開発の設計段階で集約の単位を決めておくと、cascadeの範囲もそこから決まります。
@ManyToOne側のcascade=ALL:社員削除による共有部署への削除伝播
子から親へ向かう@ManyToOne(cascade = CascadeType.ALL)を社員に付け、同じ部署に2人いる状態で1人を削除すると、Hibernateは社員に続けて部署のDELETEを発行しました。もう1人が部署を参照しているため外部キー違反でロールバックされましたが、部署に他の社員がいなければ違反は起きず、部署の行も消えます。
delete from emp where id=?
delete from dept where id=?
Referential integrity constraint violation: "FKLF4Q52CBLN60SP28ODAM4NAT2: PUBLIC.EMP FOREIGN KEY(DEPT_ID) REFERENCES PUBLIC.DEPT(ID) (CAST(1 AS BIGINT))"
@ManyToManyのREMOVE:共有エンティティへの削除伝播
@ManyToMany(cascade = {PERSIST, REMOVE})で学生と講座を結び、2人が同じ講座を履修している状態で1人を削除しました。中間表の行を消したあと、講座本体のDELETEまで発行し、もう1人の中間表の行が参照しているため外部キー違反になりました。中間表で共有される相手は、REMOVEの対象にしてはいけません。
delete from student_course where Student_id=?
delete from course where id=?
Referential integrity constraint violation: "FKK5XF4JF59E96I2RF90V9GFKVQ: PUBLIC.STUDENT_COURSE FOREIGN KEY(COURSES_ID) REFERENCES PUBLIC.COURSE(ID) (CAST(1 AS BIGINT))"
orphanRemovalとCascadeType.REMOVEの違い
2つは似た結果になりますが、発火する条件が違います。cascade=REMOVEは親にremoveが呼ばれたときに子へ伝播します。orphanRemovalは子がコレクションから外された、または関連がnullになったときに、外された子をremoveします。
| 比較軸 | cascade=REMOVE | orphanRemoval=true |
|---|---|---|
| 発火条件 | 親のremove | 関連からの切り離し |
| DELETEが出る時点 | flush時 | flush時 |
| 既定値 | 指定なし | false |
| 付けられる関連 | OneToOne・OneToMany | OneToOne・OneToMany |
§2.11は「The remove operation is applied at the time of the flush operation.」と定めています。実測でも、removeLine()を呼んだ時点ではSQLが出ず、commit時にdelete from order_line where id=?が1本出ました。同じ節は、孤児になったエンティティを別の関連へ付け替えることも禁じています。子を別の親へ移す業務がある関連には、orphanRemovalを付けません。
同節の「If the entity being orphaned is a detached, new, or removed entity, the semantics of orphanRemoval do not apply.」という例外規定にも注意してください。detachedな子をコレクションから外しても削除されません。また、管理下の親のコレクションをnew ArrayList<>()で差し替えると例外になります(後述)。全件外すならclear()を使えば、子の件数分のDELETEが出ます。
JPAのcascadeとSQLのON DELETE CASCADEの違い
JPAのcascadeはアプリケーションが操作を連鎖させ、SQLのCASCADEはデータベースが連鎖させます。どちらを使うかは、削除の主導権をどちらに置くかの設計判断です。
| 比較軸 | JPAのcascade | ON DELETE CASCADE |
|---|---|---|
| 実行主体 | アプリケーション | データベース |
| 定義場所 | エンティティのアノテーション | 外部キー制約のDDL |
| 発行されるSQL | 子ごとにDELETE | 親のDELETE 1文 |
| 永続化コンテキスト | 同期される | 同期されない |
| JPA以外の経路 | 効かない | 効く |
外部キー制約のON DELETE CASCADE:既定はNO ACTION
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」と書いています。外部キーを張っただけでは連鎖削除は起きず、PostgreSQL 18.6で子が残る親を消すと「violates foreign key constraint」で拒否されました。RESTRICTは「a stricter setting than NO ACTION」で、チェックをトランザクションの後半まで遅らせられない点が違います。
ALTER TABLE order_line
ADD CONSTRAINT fk_order_line_order
FOREIGN KEY (order_id) REFERENCES orders (id)
ON DELETE CASCADE;
DBによって違いが出るのがトリガーです。子テーブルにAFTER DELETEの行トリガーを置き、親1件・子3件の状態で親を消しました。PostgreSQL 18.6ではトリガーが3回動き、MySQL 8.4.8では0回でした(子を直接DELETEした場合は1回動きます)。MySQLのリファレンスも、外部キーのカスケード動作ではトリガーが起動しないと明記しています。削除ログや監査をトリガーで取っている場合、MySQLではON DELETE CASCADEに任せた子の削除が記録されません。スキーマ設計全体の考え方はデータスキーマとは?データベースのスキーマと三層スキーマの違いで整理しています。
DROP TABLE CASCADE:行ではなく依存オブジェクトの削除
DROP TABLEに付けるCASCADEは意味が別です。PostgreSQLのDROP TABLEは「Automatically drop objects that depend on the table (such as views)」と説明しており、消えるのはビューなどの依存オブジェクトです。既定のRESTRICTでは、ビューが参照するテーブルのDROPは「cannot drop table order_line because other objects depend on it」で拒否され、CASCADEを付けると「drop cascades to view v_lines」と通知してビューごと消えました。
@OnDeleteの配置とcascade=REMOVEによる発行SQLの違い
Hibernateの@OnDelete(action = OnDeleteAction.CASCADE)は、外部キーのDDLにon delete cascadeを出すアノテーションです。ただし、付ける位置とcascade=REMOVEの有無で、実行時のSQLが変わります。子3件の親をremoveした結果は次のとおりです。
| 付け方 | 子のSELECT | 子のDELETE | 親のDELETE |
|---|---|---|---|
| @ManyToOne側に@OnDelete+親にREMOVE | 1回 | 3本 | 1本 |
| @ManyToOne側に@OnDeleteのみ | なし | なし(DBが削除) | 1本 |
| @OneToMany側に@OnDelete+REMOVE | 1回 | なし(DBが削除) | 1本 |
@ManyToOne側に@OnDeleteを付けても、親にcascade=REMOVEがあれば子のDELETEは減りません。Hibernate 7.4の@OnDeleteのJavadocが勧めるのは3行目の形で、子は読み込まれて永続化コンテキスト上で削除済みになり、二次キャッシュからも外されます。@OnDeleteだけの形では子が読み込まれないため、メモリ上の子は残ります。
@OneToMany(mappedBy = "post", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
@OnDelete(action = OnDeleteAction.CASCADE)
List<PostComment> comments = new ArrayList<>();
DDLに反映されるのは、Hibernateにスキーマを生成させる場合だけです。FlywayやLiquibaseで管理しているなら、移行SQLにON DELETE CASCADEを書く必要があります。
他のORMの同等設定:Doctrine・Django
PHPのDoctrine ORM(3.7系)はcascade: ['persist', 'remove']のように書き、指定できるのはpersist・remove・detach・refresh・allです。JPAと同じく「Cascade operations are performed in memory.」で、DB側に任せる場合は結合列のonDeleteを使います。Djangoのon_delete=models.CASCADEは、DjangoがON DELETE CASCADEを模倣してアプリ側で消す方式で、6.1でDB側に任せるDB_CASCADEが加わりました。選び方はDjango on_deleteの選び方:7種の挙動と6.1のDB_*系で扱っています。
Spring Data JPAでcascadeが効く経路と効かない経路
Spring Data JPA 4.1.1のSimpleJpaRepositoryを読むと、メソッドごとに呼ぶEntityManagerの操作が決まっています。
| メソッド | 内部の操作 | 参照されるcascade |
|---|---|---|
| save()(新規と判定) | persist | PERSIST |
| save()(既存と判定) | merge | MERGE |
| delete・deleteById・deleteAll() | remove | REMOVE・orphanRemoval |
| deleteAllInBatch・deleteAllByIdInBatch | JPQLの一括DELETE | 伝播しない |
| @Modifying付きのDELETEクエリ | JPQLの一括DELETE | 伝播しない |
新規かどうかの判定は、@Versionがラッパー型ならその値がnullか、それ以外はIDがnull(プリミティブなら0)かで決まります。手動採番でIDが設定済みで、ラッパー型のVersion属性がなく、Persistableなどによる新規判定も変更していない場合は、新規でもsave()がmergeを呼びます。IDを手で入れた新規の親子をPERSISTだけの関連でmergeしたところ、子のIDでSELECTしたうえでEntityNotFoundException: No row with the given identifier exists for entity [demo.SlipLine with id '1000']になり、親も保存されませんでした。「新規登録では子も保存されるのに、更新すると子の変更が消える」症状は、PERSISTしか付けていない関連で起きます。
detachedなエンティティを扱う頻度はspring.jpa.open-in-viewでも変わります。既定値と無効化の判断はspring.jpa.open-in-viewとは?OSIVの既定値trueと起動時警告の消し方、関連アノテーションの全体像はSpring Bootのアノテーション一覧を参照してください。
cascadeが効かない・意図せず効くときの原因と例外メッセージ
mappedBy側のcascade未指定による子の保存漏れ
§3.3.4は、flush時にcascade=PERSISTの無い関連の先が新規エンティティならIllegalStateExceptionを投げると定めています。ところがHibernate 7.4では、mappedBy側(関連の持ち主でない側)のコレクションにだけ新規の子があっても例外にならず、親のINSERTだけが出て子は0件のままでした。逆に、関連の持ち主である子側から未保存の親を参照すると、仕様どおりcommit時に例外になります。
java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException:
Persistent instance of 'demo.InvoiceLine' references an unsaved transient instance of 'demo.Invoice'
(persist the transient instance before flushing) [demo.InvoiceLine.invoice -> demo.Invoice]
「子が保存されない」のにログに何も出ない場合は、親側のcascadeを先に疑ってください。子側の親参照を設定し忘れて外部キー列がnullになるケースもあるため、冒頭のaddLine()のように追加と参照の設定を1つのメソッドにまとめます。
orphanRemoval付きコレクションの参照差し替えによる例外
orphanRemoval=trueのコレクションを、管理下の親でorder.lines = new ArrayList<>()のように差し替えると、commit時にHibernateException: A collection with orphan deletion was no longer referenced by the owning entity instance: demo.Order.linesになります。Hibernateは元のコレクションの出入りを追跡しているため、参照ごと置き換えると孤児を判定できません。setterでリストを受け取る場合も、管理中のコレクションは保持します。削除対象はremoveLine()、追加対象はaddLine()で差分を反映し、子側の親参照も更新します。
detachedなエンティティへのpersistによる例外
一度detachしたエンティティをem.persist()に渡すと、jakarta.persistence.EntityExistsException: Detached entity passed to persist: demo.Orderになりました。原因はHibernate内部のPersistentObjectExceptionで、EntityManager経由ではJPA標準の例外に変換されます。§3.3.2の規定どおりの挙動です。detachedなエンティティの状態を書き戻すならmergeを使います。手動採番した新規エンティティは、IDが設定済みでもpersistの対象です。
JPQLのバルクDELETEによる子の残存と外部キー違反
§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のOrderをJPQLで消すと、明細へは伝播せず、親のDELETE 1文が外部キー違反で失敗しました。
em.createQuery("delete from Order o where o.id = :id")
.setParameter("id", id)
.executeUpdate();
// => ConstraintViolationException: Referential integrity constraint violation:
// "FKK9F9T1TMKBQ5W27U8RRJBXXG6: PUBLIC.ORDER_LINE FOREIGN KEY(ORDER_ID) ..."
外部キーにON DELETE CASCADEがある場合はDBが子を消しますが、同節の「The persistence context is not synchronized with the result of the bulk update or delete.」のとおり、先に読み込んだ子は残ります。実測でも、DBの子は0行なのにem.contains(comment)はtrueでした。バルクDELETEは新しいトランザクションの最初に実行するか、実行後にem.clear()します。トランザクションの境界の考え方はトランザクションとは?分離レベルの違い・ACID・commit/rollbackを参照してください。
よくある質問
cascadeの既定値は何ですか?
空です。何も伝播しません。orphanRemovalの既定値もfalseです。外部キー制約のON DELETEもPostgreSQLとMySQLでは既定がNO ACTIONなので、アノテーションとDDLのどちらにも書かない限り削除は連鎖しません。
CascadeType.MERGEとPERSISTはどう違いますか?
PERSISTはem.persist()で新規の子を登録するとき、MERGEはem.merge()でdetachedな子の変更を書き戻すときに参照されます。Spring Data JPAのsave()は新規判定で両者を呼び分けるため、親経由のpersistと、detachedな親子のmergeを両方使う場合は、PERSISTとMERGEを指定します。管理下の子の変更は、MERGEの伝播がなくてもflush時に反映されます。
CascadeType.ALLとorphanRemoval=trueは併用すべきですか?
子が1つの親に専有される関連なら併用します。ALLだけでは、明細を1行コレクションから外しても削除されません。子を別の親へ付け替える業務がある関連では、orphanRemovalを付けません。
Hibernate 8でも@Cascadeは使えますか?
使えない見込みです。org.hibernate.annotations.CascadeTypeは7で削除予定の非推奨になり、8.0.0.Beta3のhibernate-core jarにはCascadeもCascadeTypeも含まれていません。正式版の内容はリリースノートで確認し、今のうちにjakarta.persistence.CascadeTypeへ置き換えてください。
ON DELETE CASCADEとJPAのcascadeはどちらを使うべきですか?
子の件数が多い、またはJPA以外の経路(管理用SQL・バッチ・他システム)からも削除するならON DELETE CASCADEを使います。同じトランザクションの中で削除後の子を参照する、MySQLで子の削除をトリガーで記録している、といった場合はcascade=REMOVEを使います。両方を使うなら、Hibernateでは@OneToMany側に@OnDeleteとcascade=REMOVEを並べる形にします。