From 1a0b127027f33bd54e53a7e654073da593c55faa Mon Sep 17 00:00:00 2001 From: Ilia Sazonov Date: Sun, 22 Dec 2024 15:28:10 +0300 Subject: [PATCH 1/5] Split connection holder removal and jdbc connection release Even though those two actions trigger under the same condition, they don't have to be executed in one uninterrupted block of code. Connection only can be released if connection holder was successfully removed, but that's all --- .../org/springframework/orm/jpa/JpaTransactionManager.java | 3 +++ 1 file changed, 3 insertions(+) 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 9b8d020f877b..8bd3c068bf16 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 @@ -623,6 +623,9 @@ protected void doCleanupAfterCompletion(Object transaction) { // Remove the JDBC connection holder from the thread, if exposed. if (getDataSource() != null && txObject.hasConnectionHolder()) { TransactionSynchronizationManager.unbindResource(getDataSource()); + } + // Give JpaDialect it's chance to release JDBC connection + if (getDataSource() != null && txObject.hasConnectionHolder()) { ConnectionHandle conHandle = txObject.getConnectionHolder().getConnectionHandle(); if (conHandle != null) { try { From 43d40adaa8adf5a700809dfcd46d9e39c5f6ff61 Mon Sep 17 00:00:00 2001 From: Ilia Sazonov Date: Sun, 22 Dec 2024 16:52:20 +0300 Subject: [PATCH 2/5] Release JDBC connection after transaction cleanup Releasing connection before transaction cleanup may break connection reset. It certainly does in case of Hibernate, because HibernateJpaDialect needs active connection to reset isolation level and read only flag. New unit test simply makes sure cleanup and connection release happen in expected order. --- .../orm/jpa/JpaTransactionManager.java | 5 +- ...nsactionManagerConnectionReleaseTests.java | 119 ++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerConnectionReleaseTests.java 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 8bd3c068bf16..b89282ebd887 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 @@ -624,6 +624,9 @@ protected void doCleanupAfterCompletion(Object transaction) { if (getDataSource() != null && txObject.hasConnectionHolder()) { TransactionSynchronizationManager.unbindResource(getDataSource()); } + + getJpaDialect().cleanupTransaction(txObject.getTransactionData()); + // Give JpaDialect it's chance to release JDBC connection if (getDataSource() != null && txObject.hasConnectionHolder()) { ConnectionHandle conHandle = txObject.getConnectionHolder().getConnectionHandle(); @@ -639,8 +642,6 @@ protected void doCleanupAfterCompletion(Object transaction) { } } - getJpaDialect().cleanupTransaction(txObject.getTransactionData()); - // Remove the entity manager holder from the thread. if (txObject.isNewEntityManagerHolder()) { EntityManager em = txObject.getEntityManagerHolder().getEntityManager(); diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerConnectionReleaseTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerConnectionReleaseTests.java new file mode 100644 index 000000000000..54ffa7179a42 --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerConnectionReleaseTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.orm.jpa; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EntityTransaction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.springframework.jdbc.datasource.ConnectionHandle; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * @author Ilia Sazonov + */ +class JpaTransactionManagerConnectionReleaseTests { + + private EntityManagerFactory factory = mock(); + + private EntityManager manager = mock(); + + private EntityTransaction tx = mock(); + + private JpaTransactionManager tm = new JpaTransactionManager(factory); + + private TransactionTemplate tt = new TransactionTemplate(tm); + + private DataSource ds = mock(); + + private ConnectionHandle connHandle = mock(); + + private JpaDialect jpaDialect = spy(new DefaultJpaDialect()); + + + @BeforeEach + void setup() throws SQLException { + given(factory.createEntityManager()).willReturn(manager); + given(manager.getTransaction()).willReturn(tx); + given(manager.isOpen()).willReturn(true); + given(jpaDialect.getJdbcConnection(same(manager), anyBoolean())).willReturn(connHandle); + tm.setJpaDialect(jpaDialect); + tm.setDataSource(ds); + } + + @AfterEach + void verifyTransactionSynchronizationManagerState() { + assertThat(TransactionSynchronizationManager.getResourceMap()).isEmpty(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + assertThat(TransactionSynchronizationManager.isCurrentTransactionReadOnly()).isFalse(); + assertThat(TransactionSynchronizationManager.isActualTransactionActive()).isFalse(); + } + + @Test + void testConnectionIsReleasedAfterTransactionCleanup() throws SQLException { + given(manager.getTransaction()).willReturn(tx); + + final List l = new ArrayList<>(); + l.add("test"); + + assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + TransactionSynchronizationManager.bindResource(factory, new EntityManagerHolder(manager)); + + try { + Object result = tt.execute(status -> { + assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isTrue(); + EntityManagerFactoryUtils.getTransactionalEntityManager(factory); + return l; + }); + assertThat(result).isSameAs(l); + + assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + } + finally { + TransactionSynchronizationManager.unbindResource(factory); + } + + verify(tx).begin(); + verify(tx).commit(); + + InOrder cleanupBeforeRelease = inOrder(jpaDialect); + cleanupBeforeRelease.verify(jpaDialect).cleanupTransaction(any()); + cleanupBeforeRelease.verify(jpaDialect).releaseJdbcConnection(same(connHandle), same(manager)); + } +} From 7c22fde4c06e639614d97e13415c3bddb6950b77 Mon Sep 17 00:00:00 2001 From: Ilia Sazonov Date: Sun, 22 Dec 2024 17:06:44 +0300 Subject: [PATCH 3/5] Only release JDBC connection if entity manager was pre-bound In case of pre-bound entity manager it makes sense to release connection after transaction (in some cases at least). But if entity manager is not pre-bound, then it is going to be closed immediately after transaction cleanup. And that releases the connection. So there's no point in releasing the connection manually, if it's going to be released next moment anyway. --- .../orm/jpa/JpaTransactionManager.java | 29 +++++++++---------- ...nsactionManagerConnectionReleaseTests.java | 28 ++++++++++++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) 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 b89282ebd887..d3e01617ffc7 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 @@ -627,21 +627,6 @@ protected void doCleanupAfterCompletion(Object transaction) { getJpaDialect().cleanupTransaction(txObject.getTransactionData()); - // Give JpaDialect it's chance to release JDBC connection - if (getDataSource() != null && txObject.hasConnectionHolder()) { - ConnectionHandle conHandle = txObject.getConnectionHolder().getConnectionHandle(); - if (conHandle != null) { - try { - getJpaDialect().releaseJdbcConnection(conHandle, - txObject.getEntityManagerHolder().getEntityManager()); - } - catch (Throwable ex) { - // Just log it, to keep a transaction-related exception. - logger.error("Failed to release JDBC connection after transaction", ex); - } - } - } - // Remove the entity manager holder from the thread. if (txObject.isNewEntityManagerHolder()) { EntityManager em = txObject.getEntityManagerHolder().getEntityManager(); @@ -652,6 +637,20 @@ protected void doCleanupAfterCompletion(Object transaction) { } else { logger.debug("Not closing pre-bound JPA EntityManager after transaction"); + // Give JpaDialect it's chance to release JDBC connection + if (getDataSource() != null && txObject.hasConnectionHolder()) { + ConnectionHandle conHandle = txObject.getConnectionHolder().getConnectionHandle(); + if (conHandle != null) { + try { + getJpaDialect().releaseJdbcConnection(conHandle, + txObject.getEntityManagerHolder().getEntityManager()); + } + catch (Throwable ex) { + // Just log it, to keep a transaction-related exception. + logger.error("Failed to release JDBC connection after transaction", ex); + } + } + } } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerConnectionReleaseTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerConnectionReleaseTests.java index 54ffa7179a42..284c1fa448f6 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerConnectionReleaseTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/JpaTransactionManagerConnectionReleaseTests.java @@ -39,6 +39,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -116,4 +117,31 @@ void testConnectionIsReleasedAfterTransactionCleanup() throws SQLException { cleanupBeforeRelease.verify(jpaDialect).cleanupTransaction(any()); cleanupBeforeRelease.verify(jpaDialect).releaseJdbcConnection(same(connHandle), same(manager)); } + + @Test + void testConnectionIsNotReleasedIfSesionIsClosing() throws SQLException { + given(manager.getTransaction()).willReturn(tx); + + final List l = new ArrayList<>(); + l.add("test"); + + assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + + Object result = tt.execute(status -> { + assertThat(TransactionSynchronizationManager.hasResource(factory)).isTrue(); + EntityManagerFactoryUtils.getTransactionalEntityManager(factory).flush(); + return l; + }); + assertThat(result).isSameAs(l); + + assertThat(TransactionSynchronizationManager.hasResource(factory)).isFalse(); + assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse(); + + verify(tx).begin(); + verify(tx).commit(); + + verify(jpaDialect).cleanupTransaction(any()); + verify(jpaDialect, never()).releaseJdbcConnection(any(), any()); + } } From 1d73e4b53078fc9125289c7d8745d2b25cbe6399 Mon Sep 17 00:00:00 2001 From: Ilia Sazonov Date: Sun, 22 Dec 2024 18:19:45 +0300 Subject: [PATCH 4/5] Allow Hibernate users release connections after transaction in case of open-in-view This commit introduces a new setting - releaseConnectionAfterTransaction, which controls if HibernateJpaDialect#releaseJdbcConnection will release connections when called. ReleaseJdbcConnection method is only called if session is pre-bound, so the new setting actually doesn't do anything unless open-in-view is on. And in case if open-in-view is on, turning releaseConnectionAfterTransaction on might help with connection pool starvation. --- .../orm/jpa/vendor/HibernateJpaDialect.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) 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 cc62cde226e6..eac604fbabfb 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 @@ -47,6 +47,7 @@ import org.hibernate.exception.JDBCConnectionException; import org.hibernate.exception.LockAcquisitionException; import org.hibernate.exception.SQLGrammarException; +import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor; import org.jspecify.annotations.Nullable; import org.springframework.dao.CannotAcquireLockException; @@ -89,6 +90,8 @@ public class HibernateJpaDialect extends DefaultJpaDialect { boolean prepareConnection = true; + boolean releaseConnectionAfterTransaction = false; + private @Nullable SQLExceptionTranslator jdbcExceptionTranslator; private @Nullable SQLExceptionTranslator transactionExceptionTranslator = new SQLExceptionSubclassTranslator(); @@ -118,6 +121,34 @@ public void setPrepareConnection(boolean prepareConnection) { this.prepareConnection = prepareConnection; } + /** + * Set, whether to release connection after transaction is commited or rolled + * back. Only works for pre-bound sessions. + *

This setting is needed to work around the fact, that it is not possible + * to use {@link org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode#DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION} + * handling mode without sacrificing setting non-default isolation levels + * or read-only flag. Releasing connection might prevent connection pool + * starvation if open-in-view is on. + *

Default is "false" for backward compatibility. If you turn this flag + * on it still does nothing unless session is pre-bound (most likely if + * open-in-view) is off. If session is pre-bound and the flag is on, then + * after transaction is finished (successfully or not), underlying JDBC + * connection will be released and acquired later according to {@link org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode} + * (is set to {@link org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode#DELAYED_ACQUISITION_AND_HOLD} + * with {@link HibernateJpaVendorAdapter#getJpaPropertyMap()} if you don't + * specify it yourself). + *

Please pay attention, that this setting doesn't affect how Hibernate + * handles connections, which were acquired on-demand, to lazily load + * collections outside of transaction context. + *

Specifically, connections, acquired to serialize entities, returned + * by rest controller method will only be closed, after serialization is + * complete. Hibernate will not acquire and release connections for each + * lazy field loading. + */ + public void setReleaseConnectionAfterTransaction(boolean releaseConnectionAfterTransaction) { + this.releaseConnectionAfterTransaction = releaseConnectionAfterTransaction; + } + /** * Set the JDBC exception translator for Hibernate exception translation purposes. *

Applied to any detected {@link java.sql.SQLException} root cause of a Hibernate @@ -231,6 +262,16 @@ public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean r return new HibernateConnectionHandle(session); } + @Override + public void releaseJdbcConnection(ConnectionHandle conHandle, EntityManager em) throws PersistenceException, SQLException { + if (releaseConnectionAfterTransaction) { + final LogicalConnectionImplementor logicalConnection = getSession(em).getJdbcCoordinator().getLogicalConnection(); + if (logicalConnection.isPhysicallyConnected()) { + logicalConnection.manualDisconnect(); + } + } + } + @Override public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { if (ex instanceof HibernateException hibernateEx) { From 2805432d3be507c064e82f935befedcf72abef04 Mon Sep 17 00:00:00 2001 From: Ilia Sazonov Date: Sun, 22 Dec 2024 18:54:17 +0300 Subject: [PATCH 5/5] Pass releaseConnectionAfterTransaction setting via HibernateJpaVendorAdapter Users don't create HibernateJpaDialect themselves, HibernateJpaVendorAdapter does that. So, this change is needed so that releaseConnectionAfterTransaction could be configured via application settings. --- .../jpa/vendor/HibernateJpaVendorAdapter.java | 28 +++++++++ ...ctionAfterTransactionIntegrationTests.java | 62 +++++++++++++++++++ ...nate-manager-release-after-transaction.xml | 36 +++++++++++ 3 files changed, 126 insertions(+) create mode 100644 spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryReleaseConnectionAfterTransactionIntegrationTests.java create mode 100644 spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-release-after-transaction.xml diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java index b8561cd3450c..106935580345 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java @@ -109,6 +109,34 @@ public void setPrepareConnection(boolean prepareConnection) { this.jpaDialect.setPrepareConnection(prepareConnection); } + /** + * Set, whether to release connection after transaction is commited or rolled + * back. Only works for pre-bound sessions. + *

This setting is needed to work around the fact, that it is not possible + * to use {@link org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode#DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION} + * handling mode without sacrificing setting non-default isolation levels + * or read-only flag. Releasing connection might prevent connection pool + * starvation if open-in-view is on. + *

Default is "false" for backward compatibility. If you turn this flag + * on it still does nothing unless session is pre-bound (most likely if + * open-in-view) is off. If session is pre-bound and the flag is on, then + * after transaction is finished (successfully or not), underlying JDBC + * connection will be released and acquired later according to {@link org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode} + * (is set to {@link org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode#DELAYED_ACQUISITION_AND_HOLD} + * with {@link HibernateJpaVendorAdapter#getJpaPropertyMap()} if you don't + * specify it yourself). + *

Please pay attention, that this setting doesn't affect how Hibernate + * handles connections, which were acquired on-demand, to lazily load + * collections outside of transaction context. + *

Specifically, connections, acquired to serialize entities, returned + * by rest controller method will only be closed, after serialization is + * complete. Hibernate will not acquire and release connections for each + * lazy field loading. + */ + public void setReleaseConnectionAfterTransaction(boolean releaseConnectionAfterTransaction) { + this.jpaDialect.setReleaseConnectionAfterTransaction(releaseConnectionAfterTransaction); + } + @Override public PersistenceProvider getPersistenceProvider() { diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryReleaseConnectionAfterTransactionIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryReleaseConnectionAfterTransactionIntegrationTests.java new file mode 100644 index 000000000000..8c91cf9338c7 --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateEntityManagerFactoryReleaseConnectionAfterTransactionIntegrationTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.orm.jpa.hibernate; + +import jakarta.persistence.EntityManager; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor; +import org.junit.jupiter.api.Test; +import org.springframework.orm.jpa.AbstractContainerEntityManagerFactoryIntegrationTests; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link org.springframework.orm.jpa.vendor.HibernateJpaDialect#releaseConnectionAfterTransaction} + * + * @author Ilia Sazonov + */ +class HibernateEntityManagerFactoryReleaseConnectionAfterTransactionIntegrationTests extends AbstractContainerEntityManagerFactoryIntegrationTests { + + @Override + protected String[] getConfigLocations() { + return new String[] {"/org/springframework/orm/jpa/hibernate/hibernate-manager-release-after-transaction.xml", + "/org/springframework/orm/jpa/memdb.xml", "/org/springframework/orm/jpa/inject.xml"}; + } + + @Test + public void testReleaseConnectionAfterTransaction() { + endTransaction(); + + try (EntityManager em = entityManagerFactory.createEntityManager()) { + EntityManagerHolder emHolder = new EntityManagerHolder(em); + TransactionSynchronizationManager.bindResource(entityManagerFactory, emHolder); + + startNewTransaction(); + endTransaction(); + + assertThat(em.isOpen()).isTrue(); + final LogicalConnectionImplementor logicalConnection = em.unwrap(SessionImplementor.class) + .getJdbcCoordinator().getLogicalConnection(); + assertThat(logicalConnection.isPhysicallyConnected()).isFalse(); + + } finally { + TransactionSynchronizationManager.unbindResource(entityManagerFactory); + } + } +} diff --git a/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-release-after-transaction.xml b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-release-after-transaction.xml new file mode 100644 index 000000000000..82ede15cfba6 --- /dev/null +++ b/spring-orm/src/test/resources/org/springframework/orm/jpa/hibernate/hibernate-manager-release-after-transaction.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + org.springframework.orm.hibernate5.SpringSessionContext + org.hibernate.cache.HashtableCacheProvider + + + + + + + + + + + + + + + +