To delete an entity in Hibernate, we load it into the persistence context, the set of objects the EntityManager tracks, and pass it to EntityManager.remove(), and Hibernate sends the SQL DELETE when the transaction commits. The remove() call handles one entity at a time and runs its lifecycle callbacks and cascades.
We delete entities whenever a user leaves or data expires, for example when a newsletter service drops the subscribers who never confirmed their email address. To remove many rows with one statement, we run a JPQL delete query instead, which skips the callbacks and cascades.
The following example shows the ways to delete a Subscriber, from remove() on a loaded entity to a bulk delete.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "newsletter_id")
private Newsletter newsletter;
@PreRemove
void beforeRemove() {
System.out.println("@PreRemove " + name);
}
Subscriber alex = em.find(Subscriber.class, alexId); // select ... from Subscriber s1_0 where s1_0.id=?
em.remove(alex); // delete from Subscriber where id=? (at commit)
em.remove(em.getReference(Subscriber.class, alexId)); // same SELECT and DELETE
int deleted = em.createQuery("delete from Subscriber s where s.confirmed = false")
.executeUpdate(); // delete from Subscriber s1_0 where s1_0.confirmed=false
// deleted = 2
em.remove(detached); // IllegalArgumentException
Notice that a detached object needs a managed copy before remove(), and that the bulk delete returns the number of deleted rows without loading any entity.
Next, we see how Hibernate deletes an entity, and then the cases that fail in real apps, such as stale objects in memory after a bulk delete and a parent row that still has child rows.
1. How Hibernate Deletes an Entity
The method em.remove() works only on a managed entity, an object that the current EntityManager loaded or saved and still tracks. The call does not send SQL. It changes the entity’s state to removed and runs the @PreRemove callback, and the DELETE waits for the next flush, which happens at commit or before a query that needs it.

Two facts in the picture surprise many developers.
- The DELETE runs at flush, not at the remove() call. Before the commit, the row is still in the table, but em.find() already returns null for it.
- After the commit, the alex object still exists in memory with its id and name. It is no longer connected to any row.
There are four ways to send the DELETE. They differ in the number of statements and in what they skip.
| Option | SQL to delete Alex and Brian | Cascades | @PreRemove | Persistence context |
|---|---|---|---|---|
| find() or a query, then remove() | 1 SELECT + 2 DELETE | Applied | Runs | Updated |
| getReference(), then remove() | 1 SELECT + 1 DELETE per row (only the DELETE when the entity has no callback) | Applied | Runs | Updated |
| JPQL delete query | 1 DELETE | Skipped | Skipped | Not updated |
| Hibernate Session.remove() | Same as em.remove() | Applied | Runs | Updated |
We use remove() when the entity has callbacks, cascades or is already loaded, and a bulk delete when we remove many rows and nothing else needs to react.
2. Deleting Entities Example
The following example is a newsletter service built with Hibernate 7.4.11, Java 25 and an in-memory H2 database.
2.1. Newsletter and Subscriber Entities
A newsletter “Java Weekly” has three subscribers. Lokesh confirmed his email address, whereas Alex and Brian did not. The Subscriber table holds the foreign key newsletter_id, so a subscriber row points to its newsletter row.
The newsletter side is a plain @OneToMany without any cascade, so deleting a newsletter does not touch its subscribers.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "newsletter")
private List<Subscriber> subscribers = new ArrayList<>();
Subscriber owns the foreign key and has a @PreRemove method. A lifecycle callback like this runs inside remove(), so the printed line shows us which delete options call it.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private boolean confirmed;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "newsletter_id")
private Newsletter newsletter;
@PreRemove
void beforeRemove() {
System.out.println("@PreRemove " + name);
}
2.2. Deleting a Loaded Entity With remove()
The usual way is to load the entity with em.find() and pass it to remove() inside a transaction. The helper emf.runInTransaction() opens an EntityManager, begins the transaction, runs our code and commits.
emf.runInTransaction(em -> {
Subscriber alex = em.find(Subscriber.class, alexId); // select ... from Subscriber s1_0 where s1_0.id=?
em.remove(alex); // prints "@PreRemove Alex", no SQL
boolean managed = em.contains(alex); // false
Subscriber found = em.find(Subscriber.class, alexId); // null, no SQL
}); // delete from Subscriber where id=?
A removed entity can still be saved again in the same transaction. Calling persist() on it makes it managed again, and Hibernate sends no DELETE.
em.remove(alex);
em.persist(alex); // the commit sends no DELETE, all 3 subscribers stay
2.3. Deleting by Id With getReference()
When we know only the id, for example from a request DELETE /subscribers/2, em.getReference() returns a proxy, a placeholder object that holds only the id. Whether Hibernate still loads the row depends on the entity.
em.remove(em.getReference(Subscriber.class, alexId));
// select ... from Subscriber s1_0 where s1_0.id=?
// delete from Subscriber where id=?
em.remove(em.getReference(Newsletter.class, kotlinId)); // an empty newsletter
// delete from Newsletter where id=?
Subscriber has a @PreRemove method, and the callback needs a real object with its fields, so Hibernate loads the row first. Newsletter has no callback, so Hibernate sends only the DELETE. A reference saves the SELECT only for entities without callbacks.
2.4. Removing a Detached Entity
A detached entity was loaded by an EntityManager that is now closed, for example an object returned by a service method. Passing it to remove() in a new transaction fails.
Subscriber detached = emf.callInTransaction(em -> em.find(Subscriber.class, alexId));
emf.runInTransaction(em -> em.remove(detached));
java.lang.IllegalArgumentException: org.hibernate.DetachedObjectException: Given entity is not associated with the persistence context
The new EntityManager can delete only an object it manages, so we give it its own copy first. Both fixes delete the row.
emf.runInTransaction(em -> em.remove(em.merge(detached)));
// select ... from Subscriber s1_0 where s1_0.id=?
// delete from Subscriber where id=?
emf.runInTransaction(em -> em.remove(em.getReference(Subscriber.class, detached.getId())));
The method merge() loads the row and copies the detached state onto the managed object, whereas getReference() is enough when we only want the row gone.
2.5. Bulk Delete With JPQL
A JPQL delete statement removes all matching rows with one SQL statement. For example, a nightly job removes every subscriber who did not confirm the email address. The method executeUpdate() returns the number of deleted rows.
emf.runInTransaction(em -> {
int deleted = em.createQuery("delete from Subscriber s where s.confirmed = false")
.executeUpdate(); // delete from Subscriber s1_0 where s1_0.confirmed=false
// deleted = 2
});
The console shows no “@PreRemove” line. Removing the same two subscribers one by one costs three statements and runs both callbacks.
em.createQuery("from Subscriber s where s.confirmed = false", Subscriber.class)
.getResultList() // select ... from Subscriber s1_0 where s1_0.confirmed=false
.forEach(em::remove); // @PreRemove Alex, @PreRemove Brian
// delete from Subscriber where id=? (twice)
The gap grows with the row count, because the loop sends one DELETE per row, whereas the query always sends one statement. For large deletes we can also turn on JDBC batching, which sends the loop’s DELETE statements in groups.
2.6. Bulk Delete and the Persistence Context
A bulk delete changes the table itself and does not touch the objects that the EntityManager has already loaded. The persistence context still holds them, and Hibernate keeps returning them from memory.

emf.runInTransaction(em -> {
Subscriber alex = em.find(Subscriber.class, alexId);
em.createQuery("delete from Subscriber s where s.confirmed = false").executeUpdate();
boolean managed = em.contains(alex); // true
Subscriber cached = em.find(Subscriber.class, alexId); // returns alex, no SQL
em.clear();
Subscriber gone = em.find(Subscriber.class, alexId); // select ..., null
});
The stale object causes an error when we use it. For example, when we remove Alex after the bulk delete has deleted his row, the commit fails.
Subscriber alex = em.find(Subscriber.class, alexId);
em.createQuery("delete from Subscriber s where s.confirmed = false").executeUpdate();
em.remove(alex); // commit sends delete from Subscriber where id=? and fails
jakarta.persistence.RollbackException: Error while committing the transaction [Unexpected row count (expected row count 1 but was 0) [delete from Subscriber where id=?] for entity [com.howtodoinjava.hibernate.delete.Subscriber with id '2']]
The whole transaction rolls back, so the bulk delete is undone too. We run a bulk delete at the start of a transaction, or call em.clear() right after it, before we read any affected entity again.
2.7. Deleting a Newsletter That Still Has Subscribers
The database refuses to delete a row while another row still points to it with a foreign key. Our Newsletter mapping has no cascade, so remove() deletes only the newsletter row.
emf.runInTransaction(em -> em.remove(em.find(Newsletter.class, newsletterId)));
// delete from Newsletter where id=?
jakarta.persistence.RollbackException: Error while committing the transaction [could not execute statement [Referential integrity constraint violation: "FKIM1JGVWASLATCSEM95PKFC2FC: PUBLIC.SUBSCRIBER FOREIGN KEY(NEWSLETTER_ID) REFERENCES PUBLIC.NEWSLETTER(ID) (CAST(1 AS BIGINT))"; SQL statement:
delete from Newsletter where id=? [23503-252]] [delete from Newsletter where id=?]]
The cause is an org.hibernate.exception.ConstraintViolationException. The bulk query delete from Newsletter fails with the same violation, and it fails at once, at executeUpdate().
We delete the child rows first, then the parent, in one transaction.

emf.runInTransaction(em -> {
Newsletter javaWeekly = em.find(Newsletter.class, newsletterId);
em.createQuery("delete from Subscriber s where s.newsletter = :newsletter")
.setParameter("newsletter", javaWeekly)
.executeUpdate(); // delete from Subscriber s1_0 where s1_0.newsletter_id=?
em.remove(javaWeekly); // delete from Newsletter where id=?
});
When the children always belong to the parent, the mapping can delete them for us. CascadeType.REMOVE makes em.remove(javaWeekly) remove every subscriber first, and orphanRemoval also deletes a subscriber that is taken out of the collection. Both work only through remove(), never through a JPQL delete.
The complete project on GitHub runs every case of this section and prints the SQL, and its 24 JUnit tests check each result (mvn -q compile exec:java, mvn test).
3. Deleting Entities FAQs
3.1. What Replaced Session.delete() in Hibernate 7?
Hibernate 6.0 deprecated Session.delete(), and Hibernate 7 removed it. We call remove(), on the Session or on the EntityManager. In Hibernate 7, Session extends EntityManager, so both calls run the same code.
session.delete(subscriber); // Hibernate 5: does not compile in 7.x
Session session = em.unwrap(Session.class);
session.remove(session.find(Subscriber.class, alexId)); // delete from Subscriber where id=?
3.2. Can Session.remove() Delete a Detached Entity?
No. In Hibernate 7.4, Session.remove() throws the same IllegalArgumentException as em.remove(), with the message “Given entity is not associated with the persistence context”. We use the merge() or getReference() fix from section 2.4.
3.3. How Do I Delete an Entity by Id Without Loading It?
We have two options, and they behave differently when the row does not exist.
| Code | SQL | Row with id 99 does not exist |
|---|---|---|
| em.remove(em.getReference(Subscriber.class, 99L)) | SELECT (because of the callback), then DELETE | EntityNotFoundException: No row with the given identifier exists |
| em.createQuery(“delete from Subscriber s where s.id = :id”) | One DELETE | executeUpdate() returns 0 |
| em.remove(em.find(Subscriber.class, 99L)) | SELECT | IllegalArgumentException: Entity may not be null |
The JPQL query never loads the row and reports a missing row through its return value, so it is the cheapest option when no callback has to run.
3.4. Does a Bulk Delete See Entities I Have Not Flushed Yet?
Yes. With the default flush mode AUTO, Hibernate flushes pending changes before it runs the query. A subscriber persisted in the same transaction is inserted first and then deleted.
javaWeekly.addSubscriber(chris); // new Subscriber("Chris", false)
em.persist(chris);
int deleted = em.createQuery("delete from Subscriber s where s.confirmed = false")
.executeUpdate(); // insert into Subscriber ..., then the delete
// returns 3
So we do not need em.flush() before the query. The stale objects from section 2.6 are a separate problem, and flush() does not fix them.
3.5. Should I Use Soft Delete Instead?
A soft delete keeps the row and sets a flag column instead of running a DELETE. Hibernate 6.4 added @SoftDelete for it, and every query then skips the flagged rows. We choose it when deleted data must stay for audits or for an undo button, for example when a subscriber leaves but we keep the history. It also avoids the foreign key error, because no row disappears. For data that must leave the database, such as a user’s request to erase personal data, we still need a real DELETE.
4. Conclusion
The method em.remove() deletes one managed entity at flush and runs its callbacks and cascades, while a JPQL delete removes many rows with one statement and skips both.
We remove detached objects through merge() or getReference(), and we clear the persistence context after a bulk delete. Child rows go before their parent, either with an explicit delete or with a cascade.
5. References
- Jakarta Persistence 3.2 specification: Removal
- Jakarta Persistence 3.2 specification: Bulk Update and Delete Operations
- EntityManager JavaDoc (Jakarta Persistence 3.2)
- Hibernate ORM 7.4 User Guide: Deleting (removing) entities
- Hibernate ORM 7.4 User Guide: HQL/JPQL for UPDATE and DELETE
- Hibernate ORM 7.0 Migration Guide: Session#delete
Happy Learning !!