A JPA native UPDATE query changes rows with an SQL UPDATE statement that we write ourselves, without loading any entity first. We pass the SQL to em.createNativeQuery(), or declare it once with @NamedNativeQuery, and call executeUpdate(), which returns the number of changed rows.
We use a native UPDATE when one statement must change many rows, for example to raise the price of every active subscription plan, or when the SQL needs a feature that only our database has. Hibernate sends the statement as we wrote it, but it does not update the entities already loaded in memory, and that gap causes most bugs with native updates.
The following example declares a named native UPDATE on the Plan entity, and then runs an inline UPDATE and the named one inside transactions.
@Entity
@Table(name = "plan")
@NamedNativeQuery(name = "Plan.deactivateCheaperThan",
query = "update plan set active = false where active = true and monthly_price < :minPrice")
public class Plan { ... }
int raised = emf.callInTransaction(em -> em
.createNativeQuery("update plan set monthly_price = monthly_price + :amount where active = true")
.setParameter("amount", new BigDecimal("1.00"))
.executeUpdate()); // update plan set monthly_price = monthly_price + ? where active = true
// raised = 3
int deactivated = emf.callInTransaction(em -> em
.createNamedQuery("Plan.deactivateCheaperThan")
.setParameter("minPrice", new BigDecimal("10.00"))
.executeUpdate()); // deactivated = 1
Notice that both calls return a row count and no entity, because Hibernate never loads the plans it changes.
Next, we compare the native UPDATE with a JPQL bulk update and with dirty checking. After that, we build the plans example and fix the stale entities and cache entries that a native UPDATE leaves behind.
1. Native UPDATE vs JPQL Bulk Update vs Dirty Checking
JPA gives us several ways to change data. The usual one is dirty checking, which works on loaded entities. We load an entity and change a field, and Hibernate compares the object with the copy it took at load time and writes an UPDATE at flush (the moment Hibernate sends pending changes to the database, by default at commit). The other two ways are bulk statements that change many rows with one SQL statement.
- A JPQL bulk update uses entity and field names (update Plan p set p.monthlyPrice = …), and Hibernate translates it to SQL.
- A native update uses table and column names (update plan set monthly_price = …), and Hibernate does not translate it at all.
We ran all three on the same data and raised the price of the three active plans by 1.00.

| Dirty checking | JPQL bulk update | Native UPDATE | |
|---|---|---|---|
| SQL for 3 active plans | 1 select, then 3 update … where id=? | 1 update plan p1_0 set monthly_price=(…) | 1 update plan set monthly_price = monthly_price + ? |
| Loaded entities | Hold the new values | Keep the old values | Keep the old values |
| @PreUpdate | Called 3 times | Not called | Not called |
| Second-level cache | Entries updated | Plan region evicted | All regions evicted, unless we declare the table |
| Can use SQL that only our database knows | No | No | Yes |
A native update is the right pick when the statement needs SQL that JPQL does not have, for example a database function, an UPDATE … FROM join, or a table without an entity. When the change fits in entity fields, the JPQL bulk update gives the same single statement and lets Hibernate know which table it touches.
2. Native UPDATE Query Example
A streaming service sells monthly subscription plans, and from time to time it changes prices and retires old plans. The runnable project on GitHub runs on Java 25 with Hibernate ORM 7.4.11 and an in-memory H2 database, plus a second-level cache (JCache with Caffeine) for section 2.7.
2.1. Subscription Plans
Each subscription plan has a name, a monthly price and an active flag. The service starts with four plans, and “Legacy HD” is no longer sold.
| id | name | monthly_price | active |
|---|---|---|---|
| 1 | Basic | 7.99 | true |
| 2 | Standard | 12.99 | true |
| 3 | Premium | 18.99 | true |
| 4 | Legacy HD | 9.99 | false |
A native query must use the table and column names from the database, so we map the field monthlyPrice to the column monthly_price with @Column and write monthly_price in the SQL.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(name = "monthly_price")
private BigDecimal monthlyPrice;
private boolean active;
A second entity, Addon (“Sports”, 5.00), is stored in its own addon table. We need Addon only in section 2.7, to see which cached data a native update throws away.
2.2. Running an UPDATE With createNativeQuery()
The method createNativeQuery() takes the SQL text, and executeUpdate() runs it. A named parameter starts with a colon and is bound by name.
int raised = em.createNativeQuery(
"update plan set monthly_price = monthly_price + :amount where active = true")
.setParameter("amount", new BigDecimal("1.00"))
.executeUpdate(); // update plan set monthly_price = monthly_price + ? where active = true
// raised = 3
// Basic 8.99, Standard 13.99, Premium 19.99, Legacy HD 9.99 (inactive, unchanged)
Hibernate replaced :amount with ? and sent the value as a JDBC parameter, so a value never becomes part of the SQL text. A positional parameter is a question mark with a number.
int renamed = em.createNativeQuery("update plan set name = ?1 where name = ?2")
.setParameter(1, "Legacy")
.setParameter(2, "Legacy HD")
.executeUpdate(); // update plan set name = ? where name = ?
// renamed = 1
We cannot mix named and positional parameters in one query. The returned int is the row count that the JDBC driver reports. A where clause that matches nothing returns 0, for example where name = ‘Ultra’, which gives us a cheap check that the row we meant to change exists.
2.3. Declaring the UPDATE With @NamedNativeQuery
Like JPQL named queries, a named native query is declared on an entity class, and no two queries in the persistence unit may share a name. The Plan entity carries two of them, written one above the other. The @NamedNativeQueries container from older code still compiles but is no longer required.
@NamedNativeQuery(name = "Plan.deactivateCheaperThan",
query = "update plan set active = false where active = true and monthly_price < :minPrice")
We look the query up with createNamedQuery() and run it the same way. After the price raise in section 2.2, only “Basic” (8.99) is active and under 10.00.
int deactivated = em.createNamedQuery("Plan.deactivateCheaperThan")
.setParameter("minPrice", new BigDecimal("10.00"))
.executeUpdate(); // update plan set active = false where active = true and monthly_price < ?
// deactivated = 1
Nothing in the annotation describes a result, because an UPDATE produces only a count. We do not need the old trick of mapping a “count” column with @SqlResultSetMapping, because executeUpdate() already returns the count.
2.4. Running It Inside a Transaction
When no transaction is active, executeUpdate() throws TransactionRequiredException. In the following code the EntityManager is open, but we never started a transaction.
try (EntityManager em = emf.createEntityManager()) {
int rows = em.createNativeQuery("update plan set active = true").executeUpdate(); // TransactionRequiredException
}
jakarta.persistence.TransactionRequiredException: No active transaction for update or delete query
We can give the statement a transaction in several ways.
- We call emf.runInTransaction(em -> …) when we do not need a result.
- We call emf.callInTransaction(em -> …) when we want the row count back.
- Older code calls em.getTransaction().begin() and commit(), and Spring code uses a @Transactional method.
2.5. Why a Loaded Entity Still Shows the Old Value
The cases from here on start again from the data in section 2.1. Every EntityManager keeps the entities it loaded in its persistence context, a map from id to object. The method em.find() returns the object from that map when it is there, without SQL. A native update changes the row in the table, but nothing changes the object in the map, so the managed entity keeps its old field values.

Plan premium = em.find(Plan.class, premiumId); // monthlyPrice = 18.99
em.createNativeQuery("update plan set monthly_price = :price where name = :name")
.setParameter("price", new BigDecimal("21.99"))
.setParameter("name", "Premium")
.executeUpdate(); // the row now holds 21.99
BigDecimal price = premium.getMonthlyPrice(); // 18.99, stale
boolean same = em.find(Plan.class, premiumId) == premium; // true, no SQL sent
We have two fixes. The call em.refresh(entity) reads one entity’s row again, whereas em.clear() detaches every loaded entity, so the next find() goes to the database.
em.refresh(premium); // select ... from plan p1_0 where p1_0.id=?
BigDecimal price = premium.getMonthlyPrice(); // 21.99
em.clear();
Plan fresh = em.find(Plan.class, premiumId); // select ... from plan p1_0 where p1_0.id=?
BigDecimal price = fresh.getMonthlyPrice(); // 21.99
boolean managed = em.contains(premium); // false, the old object is detached
The call clear() also throws away changes that were not flushed yet, so we call em.flush() before it when the transaction has pending changes. The call refresh() is the better choice when only a few known entities are affected.
2.6. How a Stale Entity Overwrites the Native Update
Reading a stale value is the smaller problem. If we change any field of the stale entity, dirty checking writes all its columns back at commit, including the old price.
emf.runInTransaction(em -> {
Plan premium = em.find(Plan.class, premiumId); // 18.99
em.createNativeQuery("update plan set monthly_price = 21.99 where name = 'Premium'")
.executeUpdate();
premium.setName("Premium 4K");
}); // update plan set active=?,monthly_price=?,name=? where id=?
// price in the table: 18.99, the 21.99 is gone
By default, Hibernate’s UPDATE sets every column to the value the object holds. Calling em.refresh(premium) before setName() loads 21.99 into the object first, so the commit writes 21.99 and the new name. The safest habit is to run the native update before loading the entities, or in its own transaction.
2.7. Flushing and Declaring the Affected Table
The opposite direction works without our help. In the default AUTO flush mode, Hibernate flushes pending entity changes before it runs a native update, so the SQL sees them. In the following code, Standard is marked inactive in memory, and the native statement already skips it.
em.find(Plan.class, standardId).setActive(false); // only in memory
int rows = em.createNativeQuery(
"update plan set monthly_price = monthly_price + 1 where active = true")
.executeUpdate();
// update plan set active=?,monthly_price=?,name=? where id=? (flush)
// update plan set monthly_price = monthly_price + 1 where active = true
// rows = 2, Standard is no longer counted
Hibernate does not parse our SQL, so it cannot know which tables the statement touches and assumes it may touch any of them. It flushes everything first, and after the commit it evicts every region of the second-level cache, the shared cache of entities and query results. With a cached Plan and Addon loaded before the update, both entries are gone.
em.createNativeQuery("update plan set active = true where name = 'Legacy HD'").executeUpdate();
boolean planCached = emf.getCache().contains(Plan.class, basicId); // false
boolean addonCached = emf.getCache().contains(Addon.class, sportsId); // false, though addon was not touched
Hibernate’s NativeQuery lets us declare the tables, which Hibernate calls query spaces. We unwrap the JPA query and add either the entity class or the table name.
- The method addSynchronizedEntityClass(Plan.class) declares the tables of an entity.
- The method addSynchronizedQuerySpace(“plan”) declares a table by name, also one without an entity.
em.createNativeQuery("update plan set active = false where name = 'Legacy HD'")
.unwrap(NativeQuery.class)
.addSynchronizedEntityClass(Plan.class)
.executeUpdate();
boolean planCached = emf.getCache().contains(Plan.class, basicId); // false, plan was changed
boolean addonCached = emf.getCache().contains(Addon.class, sportsId); // true, kept
A cached query result behaves the same way. The query cache holds the result of select a from Addon a (run with the hint org.hibernate.cacheable). After an undeclared native update, the next run is a cache miss, and after an update declared on Plan, it is a cache hit. Declaring the wrong table causes more harm than declaring nothing.

em.find(Plan.class, standardId).setActive(false);
int rows = em.createNativeQuery(
"update plan set monthly_price = monthly_price + 1 where active = true")
.unwrap(NativeQuery.class)
.addSynchronizedQuerySpace("addon")
.executeUpdate(); // no flush before it, rows = 3
// at commit: update plan set active=?,monthly_price=?,name=? where id=?
// Standard is back to 12.99, its raise is lost
The cache suffers too. When a native update declared on addon sets Basic to 8.49, Hibernate does not evict the Plan region, and em.find() in the next transaction returns the cached 7.99.
A named native query declares its tables with a query hint. The constant HibernateHints.HINT_NATIVE_SPACES is the string org.hibernate.query.native.spaces.
@NamedNativeQuery(name = "Plan.setPrice",
query = "update plan set monthly_price = :price where name = :name",
hints = @QueryHint(name = HibernateHints.HINT_NATIVE_SPACES, value = "plan"))
int updated = em.createNamedQuery("Plan.setPrice")
.setParameter("price", new BigDecimal("9.49"))
.setParameter("name", "Basic")
.executeUpdate(); // 1, evicts only the Plan region
When the application uses the second-level cache, every native update should declare its tables. Without a cache, the declaration still helps, because Hibernate then flushes only when pending changes touch the declared tables.
3. Native UPDATE Query FAQs
3.1. What Do clearAutomatically and flushAutomatically Do in Spring Data JPA?
A Spring Data repository runs a native update through @Query(nativeQuery = true) plus @Modifying. The two flags of @Modifying map to the calls from section 2.5. The flag flushAutomatically calls em.flush() before the statement, and clearAutomatically calls em.clear() after it, so the next read loads fresh entities.
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query(value = "update plan set monthly_price = monthly_price + :amount where active = true",
nativeQuery = true)
int raiseActivePrices(@Param("amount") BigDecimal amount);
The method returns the row count and needs @Transactional. Our example project uses Hibernate without Spring, so the repository is not part of it.
3.2. Can I Call getResultList() for an UPDATE?
No. The method getResultList() asks the JDBC driver for rows, and an UPDATE returns none. H2 rejects the call, and Hibernate wraps the error in a GenericJDBCException.
org.hibernate.exception.GenericJDBCException: JDBC exception executing SQL [Method is only allowed for a query. Use execute or executeUpdate instead of executeQuery; SQL statement:
update plan set active = true [90002-252]] [update plan set active = true]
The opposite mistake, executeUpdate() on a select, fails with “Method is not allowed for a query. Use execute or executeQuery instead of executeUpdate”. The rule is short. We call executeUpdate() for update, insert and delete, and either getResultList() or getSingleResult() for select.
3.3. Does a Native UPDATE Run @PreUpdate Callbacks?
No. Entity callbacks belong to dirty checking, and a native update has no entity object to call them on. A counter in the @PreUpdate method of Plan shows the difference.
@PreUpdate
void beforeUpdate() {
preUpdateCalls++;
}
// dirty checking on 3 active plans -> preUpdateCalls = 3
// JPQL bulk update + native update -> preUpdateCalls = 0
When a callback sets a column such as a last-modified time, the native SQL must set that column itself. The same rule holds for a native delete, which skips @PreRemove and cascades.
3.4. Should I Use a Native UPDATE for Many Rows?
Yes, when one where clause selects the rows and the new value is an SQL expression. The database does the work in one statement, while dirty checking loads every entity and sends one UPDATE per row, even with JDBC batching, which only groups those statements into fewer round trips. When each row needs a different value computed in Java, dirty checking with batching is the right tool.
4. Conclusion
A native update is one SQL statement that we create with createNativeQuery() or @NamedNativeQuery and run with executeUpdate() inside a transaction, and it gives back the row count. Hibernate flushes pending changes before it, but it does not touch the entities already loaded, so we refresh or clear them before reading or changing them again.
With a second-level cache, we declare the changed table with addSynchronizedEntityClass(), addSynchronizedQuerySpace() or the org.hibernate.query.native.spaces hint.
5. References
- Query.executeUpdate() JavaDoc (Jakarta Persistence 3.2)
- NamedNativeQuery JavaDoc (Jakarta Persistence 3.2)
- Hibernate ORM 7.4 User Guide: Native SQL Queries
- Hibernate ORM 7.4 User Guide: AUTO flush on native SQL query
- SynchronizeableQuery JavaDoc (Hibernate 7.4)
- Spring Data JPA: Modifying Queries
- H2 Database: UPDATE command
Happy Learning !!
sir,
How to do a AbstractWizardFormController by using annotations. dont use any other framworks like webflow. Thank you in advance
I will check and reply soon.
This SA Thread talks about most close solution using annotations.