diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java index 59ff2462fdd..f4aff717f09 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java @@ -48,6 +48,7 @@ import org.springframework.transaction.TransactionSystemException; import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.transaction.support.DelegatingTransactionDefinition; +import org.springframework.transaction.support.ResourceTransactionDefinition; import org.springframework.transaction.support.ResourceTransactionManager; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -398,12 +399,7 @@ public class JpaTransactionManager extends AbstractPlatformTransactionManager // Delegate to JpaDialect for actual transaction begin. final int timeoutToUse = determineTimeout(definition); Object transactionData = getJpaDialect().beginTransaction(em, - new DelegatingTransactionDefinition(definition) { - @Override - public int getTimeout() { - return timeoutToUse; - } - }); + new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder())); txObject.setTransactionData(transactionData); // Register transaction timeout. @@ -741,6 +737,35 @@ public class JpaTransactionManager extends AbstractPlatformTransactionManager } + /** + * JPA-specific transaction definition to be passed to {@link JpaDialect#beginTransaction}. + * @since 5.1 + */ + private static class JpaTransactionDefinition extends DelegatingTransactionDefinition + implements ResourceTransactionDefinition { + + private final int timeout; + + private final boolean localResource; + + public JpaTransactionDefinition(TransactionDefinition targetDefinition, int timeout, boolean localResource) { + super(targetDefinition); + this.timeout = timeout; + this.localResource = localResource; + } + + @Override + public int getTimeout() { + return this.timeout; + } + + @Override + public boolean isLocalResource() { + return this.localResource; + } + } + + /** * Holder for suspended resources. * Used internally by {@code doSuspend} and {@code doResume}. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java index 462ce8c5ed0..c11a3f10395 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java @@ -67,6 +67,7 @@ import org.springframework.orm.jpa.JpaSystemException; import org.springframework.transaction.InvalidIsolationLevelException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.ResourceTransactionDefinition; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -165,6 +166,15 @@ public class HibernateJpaDialect extends DefaultJpaDialect { // Adapt flush mode and store previous isolation level, if any. FlushMode previousFlushMode = prepareFlushMode(session, definition.isReadOnly()); + if (definition instanceof ResourceTransactionDefinition && + ((ResourceTransactionDefinition) definition).isLocalResource()) { + // As of 5.1, we explicitly optimize for a transaction-local EntityManager, + // aligned with native HibernateTransactionManager behavior. + previousFlushMode = null; + if (definition.isReadOnly()) { + session.setDefaultReadOnly(true); + } + } return new SessionTransactionData(session, previousFlushMode, preparedCon, previousIsolationLevel); } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryIntegrationTests.java index 25e709e01e8..acab5b0c570 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryIntegrationTests.java @@ -60,7 +60,7 @@ public abstract class AbstractEntityManagerFactoryIntegrationTests { protected PlatformTransactionManager transactionManager; - protected TransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); + protected DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); protected TransactionStatus transactionStatus; diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryIntegrationTests.java index 85bea60131f..0def31e2512 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryIntegrationTests.java @@ -18,6 +18,7 @@ package org.springframework.orm.jpa.hibernate; import javax.persistence.EntityManager; +import org.hibernate.FlushMode; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.junit.Test; @@ -70,4 +71,16 @@ public class HibernateEntityManagerFactoryIntegrationTests extends AbstractConta assertSame(em.getDelegate(), proxy.getDelegate()); } + @Test // SPR-16956 + public void testReadOnly() { + assertSame(FlushMode.AUTO, sharedEntityManager.unwrap(Session.class).getHibernateFlushMode()); + assertFalse(sharedEntityManager.unwrap(Session.class).isDefaultReadOnly()); + endTransaction(); + + this.transactionDefinition.setReadOnly(true); + startNewTransaction(); + assertSame(FlushMode.MANUAL, sharedEntityManager.unwrap(Session.class).getHibernateFlushMode()); + assertTrue(sharedEntityManager.unwrap(Session.class).isDefaultReadOnly()); + } + } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java index 796448bc424..bd80f31e095 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java @@ -18,6 +18,7 @@ package org.springframework.orm.jpa.hibernate; import java.util.List; +import org.hibernate.FlushMode; import org.hibernate.SessionFactory; import org.hibernate.query.Query; import org.junit.Test; @@ -82,4 +83,16 @@ public class HibernateNativeEntityManagerFactoryIntegrationTests extends Abstrac assertSame(applicationContext, people.get(0).postLoaded); } + @Test // SPR-16956 + public void testReadOnly() { + assertSame(FlushMode.AUTO, sessionFactory.getCurrentSession().getHibernateFlushMode()); + assertFalse(sessionFactory.getCurrentSession().isDefaultReadOnly()); + endTransaction(); + + this.transactionDefinition.setReadOnly(true); + startNewTransaction(); + assertSame(FlushMode.MANUAL, sessionFactory.getCurrentSession().getHibernateFlushMode()); + assertTrue(sessionFactory.getCurrentSession().isDefaultReadOnly()); + } + } diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/ResourceTransactionDefinition.java b/spring-tx/src/main/java/org/springframework/transaction/support/ResourceTransactionDefinition.java new file mode 100644 index 00000000000..c5c2d396ec2 --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/support/ResourceTransactionDefinition.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.transaction.support; + +import org.springframework.transaction.TransactionDefinition; + +/** + * Extended variant of {@link TransactionDefinition}, indicating a resource transaction + * and in particular whether the transactional resource is ready for local optimizations. + * + * @author Juergen Hoeller + * @since 5.1 + * @see ResourceTransactionManager + */ +public interface ResourceTransactionDefinition extends TransactionDefinition { + + /** + * Determine whether the transactional resource is ready for local optimizations. + * @return {@code true} if the resource is known to be entirely transaction-local, + * not affecting any operations outside of the scope of the current transaction + * @see #isReadOnly() + */ + boolean isLocalResource(); + +}