JUN0.DEV
JUN0.DEV

Choosing Between @OnDelete and JPA Cascade

Published on
  • avatarJunyoung Yang

In a gift API project, deleting a product also had to delete its options. At first, database cascade delete seemed like the simplest solution.

But with JPA, database-level deletion and persistence context state can diverge. The question became: which layer should own the delete behavior?

Initial Relationship

The relationship was close to unidirectional at first.

@Entity
public class Option {

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;
}

Option knew its Product, but Product did not directly own an option collection. That left three possible ways to handle product deletion.

  • Add ON DELETE CASCADE at the database level.
  • Change the relationship to bidirectional and use JPA cascade with orphanRemoval.
  • Delete options explicitly from the service layer.

@OnDelete looked attractive because it could preserve the simpler relationship.

Option 1: @OnDelete

@OnDelete lets the database delete child rows. It can keep the Java relationship simpler and avoids loading child collections just to delete them.

@ManyToOne(fetch = FetchType.LAZY)
@OnDelete(action = OnDeleteAction.CASCADE)
private Product product;

The downside is that JPA may not fully know what the database deleted. If the persistence context still holds stale state, consistency can become harder to reason about.

The problem is not the delete itself. It is what happens inside the same transaction if the persistence context still has an Option entity that the database has already removed.

  • The database has deleted the child row.
  • The persistence context may still contain the child entity.
  • Later code in the same transaction can observe stale state.

That was the point where this stopped being only a DDL convenience decision.

Option 2: JPA Cascade

Using JPA cascade and orphanRemoval makes the aggregate relationship explicit in the object model. The parent owns the lifecycle of the child objects.

@Entity
public class Product {

    @OneToMany(
            mappedBy = "product",
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    private List<Option> options = new ArrayList<>();
}

This can require a bidirectional relationship and more careful collection management, but it keeps deletion inside the JPA lifecycle.

After comparing both options, I started asking whether a delete rule belongs to the aggregate model or only to the database schema. That question was more useful than comparing annotations directly.

Selection Points

ApproachBenefitRisk
@OnDeleteKeeps the Java relationship simpler and delegates deletion to the database.The database can delete rows without the persistence context fully tracking the state change.
JPA cascadeKeeps the delete flow inside the JPA lifecycle and aggregate model.Requires bidirectional relationship management and collection helper methods.

In this case, product and option were close to one aggregate. The option lifecycle depended on the product lifecycle, so I preferred the JPA-managed approach.

Takeaway

For this requirement, product and option were close to one aggregate, and option lifecycle depended on product lifecycle. I chose the JPA-managed approach because application-level consistency mattered more than keeping the relationship minimal.