From e4bd1344e23301d2516d2ed5ec739941d385ac37 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 4 Jun 2023 18:12:40 +0200 Subject: [PATCH] Set and reset shared isolation value within synchronized transaction begin Since EclipseLink applies a custom transaction isolation value to its shared DatabasePlatform instance, we need to immediately restore the original value after the current value got picked up for JDBC Connection access inside of EclipseLink. In order to not interfere with concurrent transactions, we need to use synchronization around the transaction begin sequence in such a case. Closes gh-29997 --- .../orm/jpa/vendor/EclipseLinkJpaDialect.java | 41 ++++++++++++++----- ...rEntityManagerFactoryIntegrationTests.java | 18 ++++++-- 2 files changed, 45 insertions(+), 14 deletions(-) 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 30db932f16..4c0ab302aa 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -22,6 +22,7 @@ import java.sql.SQLException; import javax.persistence.EntityManager; import javax.persistence.PersistenceException; +import org.eclipse.persistence.sessions.DatabaseLogin; import org.eclipse.persistence.sessions.UnitOfWork; import org.springframework.jdbc.datasource.ConnectionHandle; @@ -78,19 +79,37 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect { public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException { - if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + int currentIsolationLevel = definition.getIsolationLevel(); + if (currentIsolationLevel != TransactionDefinition.ISOLATION_DEFAULT) { // Pass custom isolation level on to EclipseLink's DatabaseLogin configuration - // (since Spring 4.1.2) + // (since Spring 4.1.2 / revised in 5.3.28) UnitOfWork uow = entityManager.unwrap(UnitOfWork.class); - uow.getLogin().setTransactionIsolation(definition.getIsolationLevel()); + DatabaseLogin databaseLogin = uow.getLogin(); + // Synchronize on shared DatabaseLogin instance (-> concurrent transactions) + synchronized (databaseLogin) { + int originalIsolationLevel = databaseLogin.getTransactionIsolation(); + // Apply current isolation level value, if necessary. + if (currentIsolationLevel != originalIsolationLevel) { + databaseLogin.setTransactionIsolation(currentIsolationLevel); + } + // Transaction begin including enforced JDBC Connection access + // (picking up current isolation level from DatabaseLogin) + entityManager.getTransaction().begin(); + uow.beginEarlyTransaction(); + entityManager.unwrap(Connection.class); + // Restore original isolation level value, if necessary. + if (currentIsolationLevel != originalIsolationLevel) { + databaseLogin.setTransactionIsolation(originalIsolationLevel); + } + } } - - entityManager.getTransaction().begin(); - - if (!definition.isReadOnly() && !this.lazyDatabaseTransaction) { - // Begin an early transaction to force EclipseLink to get a JDBC Connection - // so that Spring can manage transactions with JDBC as well as EclipseLink. - entityManager.unwrap(UnitOfWork.class).beginEarlyTransaction(); + else { + entityManager.getTransaction().begin(); + if (!definition.isReadOnly() && !this.lazyDatabaseTransaction) { + // Begin an early transaction to force EclipseLink to get a JDBC Connection + // so that Spring can manage transactions with JDBC as well as EclipseLink. + entityManager.unwrap(UnitOfWork.class).beginEarlyTransaction(); + } } return null; diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java index 81c7b0b912..c403a9aa68 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -27,8 +27,10 @@ import javax.persistence.Query; import org.junit.jupiter.api.Test; import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.orm.jpa.domain.DriversLicense; import org.springframework.orm.jpa.domain.Person; +import org.springframework.transaction.TransactionDefinition; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; @@ -114,24 +116,34 @@ public abstract class AbstractContainerEntityManagerFactoryIntegrationTests } @Test - public void testLazyLoading() { + public void testLazyLoading() throws Exception { try { Person tony = new Person(); tony.setFirstName("Tony"); tony.setLastName("Blair"); tony.setDriversLicense(new DriversLicense("8439DK")); sharedEntityManager.persist(tony); + assertThat(DataSourceUtils.getConnection(jdbcTemplate.getDataSource()).getTransactionIsolation()) + .isEqualTo(TransactionDefinition.ISOLATION_READ_COMMITTED); setComplete(); endTransaction(); + transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); startNewTransaction(); + assertThat(DataSourceUtils.getConnection(jdbcTemplate.getDataSource()).getTransactionIsolation()) + .isEqualTo(TransactionDefinition.ISOLATION_SERIALIZABLE); sharedEntityManager.clear(); Person newTony = entityManagerFactory.createEntityManager().getReference(Person.class, tony.getId()); assertThat(tony).isNotSameAs(newTony); endTransaction(); - assertThat(newTony.getDriversLicense()).isNotNull(); + transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT); + startNewTransaction(); + assertThat(DataSourceUtils.getConnection(jdbcTemplate.getDataSource()).getTransactionIsolation()) + .isEqualTo(TransactionDefinition.ISOLATION_READ_COMMITTED); + endTransaction(); + assertThat(newTony.getDriversLicense()).isNotNull(); newTony.getDriversLicense().getSerialNumber(); } finally {