diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java index a5ba314290c..feee4daddc6 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java @@ -21,6 +21,7 @@ import java.sql.SQLException; import java.util.Map; import jakarta.persistence.EntityManager; +import jakarta.persistence.FlushModeType; import jakarta.persistence.PersistenceException; import org.jspecify.annotations.Nullable; @@ -37,6 +38,9 @@ import org.springframework.transaction.TransactionException; *

Simply begins a standard JPA transaction in {@link #beginTransaction} and * performs standard exception translation through {@link EntityManagerFactoryUtils}. * + *

Supports JPA 4.0's {@code FlushModeType.EXPLICIT} for read-only transactions, + * if available. + * * @author Juergen Hoeller * @since 2.0 * @see JpaTransactionManager#setJpaDialect @@ -44,16 +48,31 @@ import org.springframework.transaction.TransactionException; @SuppressWarnings("serial") public class DefaultJpaDialect implements JpaDialect, Serializable { + // JPA 4.0 FlushModeType.EXPLICIT available? + private static final @Nullable FlushModeType FLUSH_MODE_EXPLICIT; + + static { + FlushModeType explicit; + try { + explicit = FlushModeType.valueOf("EXPLICIT"); + } + catch (IllegalArgumentException ex) { + explicit = null; + } + FLUSH_MODE_EXPLICIT = explicit; + } + + /** * This implementation invokes the standard JPA {@code Transaction.begin} * method. Throws an InvalidIsolationLevelException if a non-default isolation * level is set. - *

This implementation does not return any transaction data Object, since there - * is no state to be kept for a standard JPA transaction. Hence, subclasses do not - * have to care about the return value ({@code null}) of this implementation - * and are free to return their own transaction data Object. + *

This implementation returns transaction data for a flush mode reset + * if necessary, calling {@link #prepareFlushMode} accordingly. Can be reused + * in subclasses or alternatively replaced with custom flush mode handling. * @see jakarta.persistence.EntityTransaction#begin * @see org.springframework.transaction.InvalidIsolationLevelException + * @see #prepareFlushMode * @see #cleanupTransaction */ @Override @@ -71,23 +90,52 @@ public class DefaultJpaDialect implements JpaDialect, Serializable { } entityManager.getTransaction().begin(); - return null; + return prepareFlushMode(entityManager, definition.isReadOnly()); } + /** + * This implementation returns transaction data for a flush mode reset + * if necessary, calling {@link #prepareFlushMode} accordingly. + * @see #prepareFlushMode + */ @Override public @Nullable Object prepareTransaction(EntityManager entityManager, boolean readOnly, @Nullable String name) throws PersistenceException { + return prepareFlushMode(entityManager, readOnly); + } + + /** + * Prepare transaction data for a flush mode reset if necessary. + * Only applied for read-only transactions on JPA 4.0. + *

Used by {@link #beginTransaction} as well as {@link #prepareTransaction}. + * Can be reused in corresponding overridden methods in vendor-specific + * subclasses, or alternatively replaced with custom flush mode handling. + * @param entityManager the EntityManager to begin a JPA transaction on + * @param readOnly whether the transaction is supposed to be read-only + * @return transaction data for a flush mode reset, if necessary + * (to be returned from {@link #beginTransaction}/{@link #prepareTransaction} + * and subsequently passed into {@link #cleanupTransaction} after completion) + * @since 7.0.6 + */ + protected @Nullable Object prepareFlushMode(EntityManager entityManager, boolean readOnly) { + if (readOnly && FLUSH_MODE_EXPLICIT != null) { + FlushModeType previousFlushMode = entityManager.getFlushMode(); + entityManager.setFlushMode(FLUSH_MODE_EXPLICIT); + return new FlushModeTransactionData(entityManager, previousFlushMode); + } return null; } /** - * This implementation does nothing, since the default {@code beginTransaction} - * implementation does not require any cleanup. - * @see #beginTransaction + * This implementation resets the flush mode if necessary. + * @see #prepareFlushMode */ @Override public void cleanupTransaction(@Nullable Object transactionData) { + if (transactionData instanceof FlushModeTransactionData flushModeTransactionData) { + flushModeTransactionData.resetFlushMode(); + } } /** @@ -149,4 +197,21 @@ public class DefaultJpaDialect implements JpaDialect, Serializable { return EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex); } + + private static class FlushModeTransactionData { + + private final EntityManager entityManager; + + private final FlushModeType previousFlushMode; + + public FlushModeTransactionData(EntityManager entityManager, FlushModeType previousFlushMode) { + this.entityManager = entityManager; + this.previousFlushMode = previousFlushMode; + } + + public void resetFlushMode() { + this.entityManager.setFlushMode(this.previousFlushMode); + } + } + } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java index 038b93d7273..c2123c45cb5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java @@ -99,7 +99,7 @@ public interface JpaDialect extends PersistenceExceptionTranslator { * @param readOnly whether the transaction is supposed to be read-only * @param name the name of the transaction (if any) * @return an arbitrary object that holds transaction data, if any - * (to be passed into cleanupTransaction) + * (to be passed into {@link #cleanupTransaction}) * @throws jakarta.persistence.PersistenceException if thrown by JPA methods * @see #cleanupTransaction */ @@ -115,6 +115,7 @@ public interface JpaDialect extends PersistenceExceptionTranslator { * @param transactionData arbitrary object that holds transaction data, if any * (as returned by beginTransaction or prepareTransaction) * @see #beginTransaction + * @see #prepareTransaction * @see org.springframework.jdbc.datasource.DataSourceUtils#resetConnectionAfterTransaction */ void cleanupTransaction(@Nullable Object transactionData); diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java index 52fae0eeccc..c3b76fc627a 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java @@ -153,7 +153,8 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect { entityManager.getTransaction().begin(); } - return null; + // Reuse JPA 4.0 FlushModeType.EXPLICIT handling from superclass. + return prepareFlushMode(entityManager, definition.isReadOnly()); } @Override @@ -183,16 +184,18 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect { @Override public Connection getConnection() { - if (this.connection == null) { + Connection con = this.connection; + if (con == null) { transactionIsolationLock.lock(); try { - this.connection = this.entityManager.unwrap(Connection.class); + con = this.entityManager.unwrap(Connection.class); } finally { transactionIsolationLock.unlock(); } + this.connection = con; } - return this.connection; + return con; } }