A JPA native DELETE query is a plain SQL DELETE statement that we send to the database through the EntityManager and run with executeUpdate(), which returns the number of deleted rows. Hibernate does not parse or change the SQL, so it only binds the parameters before it sends the statement.
We use a native DELETE to remove many rows with one statement, for example to purge expired coupons in a nightly job, or when the SQL needs a database-specific feature that JPQL does not have. We write it inline with createNativeQuery() or declare it once on the entity with @NamedNativeQuery and call it by name.
The following example declares a named native DELETE on the Coupon entity and runs it inside a transaction.
@Entity
@Table(name = "coupon")
@NamedNativeQuery(
name = "Coupon.deleteExpired",
query = "delete from coupon where expires_on < :today")
public class Coupon { ... }
int deleted = emf.callInTransaction(em -> em
.createNamedQuery("Coupon.deleteExpired")
.setParameter("today", LocalDate.of(2026, 10, 1))
.executeUpdate()); // delete from coupon where expires_on < ?
// deleted = 2
Notice that the SQL uses the table and column names, and the only thing Hibernate gives back is the row count.
Because Hibernate does not inspect the statement, a native DELETE skips everything Hibernate does for em.remove(), such as cascades, orphanRemoval, lifecycle callbacks and soft delete. Next, we compare the native DELETE with remove() and a JPQL bulk delete, and then we fix the foreign key error and the stale entities that a native DELETE can leave behind.
1. How a Native DELETE Differs From remove()
The call em.remove() works on one entity object that Hibernate already knows, so Hibernate calls the entity’s callbacks and follows the cascade settings to the children before it deletes the rows in the right order. A native DELETE has no entity object at all. Hibernate sends our SQL as written, and only the database decides what happens, including whether a foreign key blocks the delete.

JPA has a third option between the two, a JPQL bulk delete such as delete from Coupon c where c.expiresOn < :today. A JPQL bulk delete uses entity and field names, but at runtime it behaves like the native statement in most rows of the comparison.
| em.remove(coupon) | JPQL bulk delete | Native DELETE | |
|---|---|---|---|
| Written in | Java | JPQL (entity names) | SQL (table names) |
| Rows per call | One entity and its cascaded children | Many | Many |
| SQL sent | delete from coupon where id=? | delete from coupon c1_0 where c1_0.expires_on<? | Our SQL, unchanged |
| @PreRemove / @PostRemove | Called | Not called | Not called |
| CascadeType.REMOVE, orphanRemoval | Applied | Ignored | Ignored |
| @SoftDelete entity | Row marked deleted | Row marked deleted | Row deleted |
| Child rows still referencing the parent | Deleted first by cascade | Foreign key error | Foreign key error |
| Persistence context | Updated | Not updated | Not updated |
| Database-specific SQL | No | No | Yes |
We use a native DELETE when the SQL needs something JPQL does not have, such as a database-specific function or a table that has no entity. For deleting a few loaded entities, remove() is the safer choice, and the JPQL bulk delete fits when we can write the condition with entity fields.
2. Native DELETE Query Example
Our example is the coupon table of an online store, built with Hibernate 7.4, Java 25 and an in-memory H2 database. The complete project on GitHub runs every case in this section and checks the results with 15 JUnit tests (mvn -q compile exec:java, mvn test).
2.1. Coupons and Redemptions
A coupon has a code, a discount and an expiry date. Each time a customer uses a coupon, the store saves a redemption row that points to the coupon through the coupon_id foreign key, so the redemption count per coupon is part of the test data.
| coupon.id | code | discount_percent | expires_on | Redemptions |
|---|---|---|---|---|
| 1 | welcome | 10 | 2026-12-31 | 1 |
| 2 | summer | 20 | 2026-08-31 | 0, later 2 |
| 3 | flash | 50 | 2026-09-15 | 0 |
The examples use 2026-10-01 as today, so summer and flash are expired. The parent maps the children with @OneToMany and a cascade, which matters for remove() but, as we will see, not for the native DELETE.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
@Column(name = "discount_percent")
private int discountPercent;
@Column(name = "expires_on")
private LocalDate expiresOn;
@OneToMany(mappedBy = "coupon", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Redemption> redemptions = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "coupon_id", foreignKey = @ForeignKey(name = "fk_redemption_coupon"))
private Coupon coupon;
@Column(name = "redeemed_at")
private LocalDateTime redeemedAt;
A native query uses table and column names (coupon, expires_on), not entity and field names (Coupon, expiresOn). That is why the mapping sets the column names explicitly with @Column.
2.2. Deleting Expired Coupons With createNativeQuery()
The method em.createNativeQuery() takes the SQL string. We bind the parameter with setParameter() and run the statement with executeUpdate(), which returns the number of deleted rows.
int deleted = emf.callInTransaction(em -> em
.createNativeQuery("delete from coupon where expires_on < :today")
.setParameter("today", LocalDate.of(2026, 10, 1))
.executeUpdate()); // delete from coupon where expires_on < ?
// deleted = 2, left = [welcome]
Always pass values with setParameter(), never by concatenating strings into the SQL. Hibernate sends a ? placeholder and binds the LocalDate as a SQL DATE, so the value can never change the SQL itself (no SQL injection). The same executeUpdate() call also runs a native update.
2.3. Declaring the DELETE With @NamedNativeQuery
A named native query keeps the SQL on the entity, next to the mapping it depends on. The name must be unique in the persistence unit, so we prefix it with the entity name. Since JPA 2.2 the annotation is repeatable, so we no longer need the old @NamedNativeQueries wrapper.
@NamedNativeQuery(
name = "Coupon.deleteExpired",
query = "delete from coupon where expires_on < :today")
@NamedNativeQuery(
name = "Coupon.deleteByDiscount",
query = "delete from coupon where discount_percent >= ?1")
We look the query up with createNamedQuery(), the same method that runs a JPQL named query.
int expired = em.createNamedQuery("Coupon.deleteExpired")
.setParameter("today", LocalDate.of(2026, 10, 1))
.executeUpdate(); // delete from coupon where expires_on < ?
// 2, left = [welcome]
int highDiscount = em.createNamedQuery("Coupon.deleteByDiscount")
.setParameter(1, 50)
.executeUpdate(); // delete from coupon where discount_percent >= ?
// 1, left = [welcome, summer]
A DELETE returns no rows, so it needs no resultClass and no @SqlResultSetMapping.
2.4. Why the DELETE Fails When Child Rows Exist
Say a customer redeemed summer twice before it expired. When we run the same Coupon.deleteExpired query again, executeUpdate() throws an exception.
org.hibernate.exception.ConstraintViolationException: JDBC exception executing SQL [Referential integrity constraint violation: "FK_REDEMPTION_COUPON: PUBLIC.REDEMPTION FOREIGN KEY(COUPON_ID) REFERENCES PUBLIC.COUPON(ID) (CAST(2 AS BIGINT))"; SQL statement:
delete from coupon where expires_on < ? [23503-252]] [delete from coupon where expires_on < ?]
Two redemption rows still point to coupon 2. The database refuses to leave them without a parent, so it rejects the whole statement and no coupon is deleted, not even flash, which has no redemptions. The settings cascade = CascadeType.ALL and orphanRemoval = true on Coupon.redemptions do not help, because they are instructions for Hibernate’s remove(), and Hibernate never sees which rows our SQL deletes.

The H2 message names the constraint (FK_REDEMPTION_COUPON) and the parent key that is still referenced (2). Hibernate wraps it in org.hibernate.exception.ConstraintViolationException, and its getConstraintName() method returns the constraint name, which helps us show a clear error message to the user.
2.5. Delete the Child Rows First
The first fix is another native DELETE that removes the redemptions of the expired coupons. Both statements run in one transaction, so either both succeed or neither does.
emf.runInTransaction(em -> {
int children = em.createNativeQuery("""
delete from redemption
where coupon_id in (select id from coupon where expires_on < :today)""")
.setParameter("today", today)
.executeUpdate(); // children = 2
int parents = em.createNamedQuery("Coupon.deleteExpired")
.setParameter("today", today)
.executeUpdate(); // parents = 2
}); // left = [welcome], redemptions = 1
Deleting the children in our own code keeps the delete rules in the application, which is the right choice when the child rows must sometimes stay, for example when the finance team needs redemptions for accounting.
2.6. Add ON DELETE CASCADE to the Foreign Key
The second fix moves the rule into the database. A foreign key declared with ON DELETE CASCADE tells the database to delete the child rows itself whenever the parent row is deleted. For an existing table, we recreate the constraint.
alter table redemption drop constraint fk_redemption_coupon;
alter table redemption add constraint fk_redemption_coupon
foreign key (coupon_id) references coupon (id) on delete cascade;
int deleted = em.createNamedQuery("Coupon.deleteExpired")
.setParameter("today", today)
.executeUpdate(); // delete from coupon where expires_on < ?
// 2, left = [welcome], redemptions = 1
When Hibernate generates the schema, Hibernate’s @OnDelete annotation on the @ManyToOne adds the same clause to the DDL.
@ManyToOne(fetch = FetchType.LAZY)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "coupon_id", foreignKey = @ForeignKey(name = "fk_redemption_coupon"))
private Coupon coupon;
// alter table if exists redemption add constraint fk_redemption_coupon
// foreign key (coupon_id) references coupon on delete cascade
With ON DELETE CASCADE, every delete of a coupon removes its redemptions, from any SQL client, not only from our code. We choose it only when a child row has no meaning without its parent.
2.7. Callbacks, Cascades and Soft Delete Are Skipped
The Coupon entity has a @PreRemove method that records the code of every coupon being removed. The method is one of the JPA lifecycle callbacks, and Hibernate calls it only for an entity it removes itself.
@PreRemove
void beforeRemove() {
REMOVE_CALLBACKS.add(code);
}
// summer is loaded, then the native DELETE removes it
em.createNamedQuery("Coupon.deleteExpired")
.setParameter("today", today)
.executeUpdate(); // REMOVE_CALLBACKS = []
em.remove(summer); // REMOVE_CALLBACKS = [summer]
// delete from redemption where id=? (twice)
// delete from coupon where id=?
Hibernate’s @SoftDelete is skipped in the same way. On a soft-deleted entity, remove() and JPQL deletes become an UPDATE that only marks the row, whereas a native DELETE removes the row for real.
em.remove(blackFriday);
// update campaign set deleted=true where id=? and deleted=false
em.createQuery("delete from Campaign where name = 'Back to School'").executeUpdate();
// update campaign c1_0 set deleted=true where c1_0.name='Back to School' and c1_0.deleted=false
em.createNativeQuery("delete from campaign where name = 'New Year'").executeUpdate();
// delete from campaign where name = 'New Year'
// campaign rows = 2, visible campaigns = 0
If an entity relies on callbacks, cascades or soft delete, a native DELETE bypasses that entity logic without any warning. Audit entries are not written, and soft-deleted data is lost. For entities with callbacks or soft delete, we load them and use remove() instead.
2.8. Stale Entities After the DELETE
The persistence context is the set of entity objects that the EntityManager has loaded in the current transaction. A native DELETE changes only the table, so a coupon loaded before the DELETE stays in memory as a managed entity.
Coupon flash = find(em, "flash"); // loads the coupon by its code
em.createNamedQuery("Coupon.deleteExpired").setParameter("today", today).executeUpdate();
Coupon cached = em.find(Coupon.class, flash.getId()); // the same flash object, no SQL sent
boolean managed = em.contains(flash); // true
em.refresh(flash); // EntityNotFoundException
em.clear();
Coupon reloaded = em.find(Coupon.class, flash.getId()); // null

Changing the stale object causes a bigger problem. At commit, Hibernate sends an UPDATE for a row that no longer exists and rolls back the whole transaction.
jakarta.persistence.RollbackException: Error while committing the transaction [Unexpected row count (expected row count 1 but was 0) [update coupon set code=?,discount_percent=?,expires_on=? where id=?] for entity [com.howtodoinjava.hibernate.nativedelete.Coupon with id '3']]
The JPA specification gives the same advice for every bulk statement. We run the native DELETE in its own transaction or before we load the entities it affects, and if we loaded entities earlier, we call em.clear() after the DELETE.
3. Native DELETE FAQs
3.1. Why Does executeUpdate() Throw TransactionRequiredException?
The method executeUpdate() changes data, so JPA requires an active transaction. Calling it on an EntityManager without a transaction throws TransactionRequiredException before Hibernate sends any SQL.
jakarta.persistence.TransactionRequiredException: No active transaction for update or delete query
We wrap the call in emf.runInTransaction() or emf.callInTransaction(), or, in Spring, in a @Transactional method.
3.2. Should a Native Query Use Named or Positional Parameters?
Hibernate supports both, as section 2.3 shows. The JPA specification guarantees only positional parameters for native queries, so ?1 is the portable form.
| Form | Example | Portable across JPA providers |
|---|---|---|
| Named | expires_on < :today | No, but Hibernate accepts it |
| Positional | discount_percent >= ?1 | Yes |
3.3. Does a Native DELETE See Changes That Are Not Flushed Yet?
Yes. With the EntityManager API, Hibernate always flushes pending changes before it runs a native query, because it cannot tell which tables the SQL touches. In our run, flash gets a later expiry date in memory, and the UPDATE reaches the table before the DELETE.
find(em, "flash").setExpiresOn(LocalDate.of(2026, 11, 30));
int deleted = em.createNamedQuery("Coupon.deleteExpired").setParameter("today", today).executeUpdate();
// update coupon set code=?,discount_percent=?,expires_on=? where id=?
// delete from coupon where expires_on < ?
// 1, left = [welcome, flash]
3.4. How Do I Write a Native DELETE in Spring Data JPA?
In a Spring Data repository, the same SQL goes into @Query with nativeQuery = true, plus @Modifying so Spring calls executeUpdate(). The attribute clearAutomatically = true clears the persistence context after the DELETE, which avoids the stale entities from section 2.8. The repository form is not part of the example project.
@Modifying(clearAutomatically = true)
@Query(value = "delete from coupon where expires_on < :today", nativeQuery = true)
int deleteExpired(@Param("today") LocalDate today);
The calling service method needs @Transactional, for the reason in section 3.1.
4. Conclusion
A native DELETE runs our SQL with executeUpdate() and returns the row count, either inline with createNativeQuery() or by name with @NamedNativeQuery. Hibernate does not look inside the statement, so foreign keys are the only rule left.
We delete the children first or declare ON DELETE CASCADE, and we keep in mind that callbacks, cascades and @SoftDelete do not run. Running the DELETE before loading entities, or clearing the persistence context after it, keeps the objects in memory in line with the table.
5. References
- Query.executeUpdate() JavaDoc (Jakarta Persistence 3.2)
- NamedNativeQuery JavaDoc (Jakarta Persistence 3.2)
- Jakarta Persistence 3.2 specification: Positional Parameters
- Jakarta Persistence 3.2 specification: Bulk Update and Delete Operations
- Hibernate ORM 7.4 User Guide: Native SQL Queries
- Hibernate ORM 7.4 User Guide: AUTO flush on native SQL query
- Hibernate ORM 7.4 User Guide: Soft Delete
- OnDelete JavaDoc (Hibernate ORM 7.4)
- Spring Data JPA: Modifying Queries
Happy Learning !!