Skip to content

Commit d1492e3

Browse files
committed
Spring Data JPA doesn't boot when using Hibernate with multi-tenancy
Issue: #3425 When Hibernate is configured with multi-tenancy, upon startup Spring JPA calls PersistenceProvider.fromEntityManager(entityManager) which initalizes Hibernate Session. This requires a tenant to be present and may produce failures. Signed-off-by: Ariel Morelli Andres <[email protected]>
1 parent 7e87a62 commit d1492e3

File tree

5 files changed

+63
-47
lines changed

5 files changed

+63
-47
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/JpaClassUtils.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.data.jpa.provider;
1717

18-
import jakarta.persistence.EntityManager;
18+
import jakarta.persistence.EntityManagerFactory;
1919
import jakarta.persistence.metamodel.Metamodel;
2020

2121
import org.springframework.lang.Nullable;
@@ -30,6 +30,7 @@
3030
* @author Jens Schauder
3131
* @author Donghun Shin
3232
* @author Greg Turnquist
33+
* @author Ariel Morelli Andres, Atlassian US, Inc
3334
*/
3435
abstract class JpaClassUtils {
3536

@@ -39,19 +40,15 @@ abstract class JpaClassUtils {
3940
private JpaClassUtils() {}
4041

4142
/**
42-
* Returns whether the given {@link EntityManager} is of the given type.
43+
* Returns whether the given {@link EntityManagerFactory} is of the given type.
4344
*
44-
* @param em must not be {@literal null}.
45-
* @param type the fully qualified expected {@link EntityManager} type, must not be {@literal null} or empty.
46-
* @return whether the given {@code EntityManager} is of the given type.
45+
* @param factory must not be {@literal null}.
46+
* @param type the fully qualified expected {@link EntityManagerFactory} type, must not be {@literal null} or empty.
47+
* @return whether the given {@code EntityManagerFactory} is of the given type.
4748
*/
48-
public static boolean isEntityManagerOfType(EntityManager em, String type) {
49+
public static boolean isEntityManagerFactoryOfType(EntityManagerFactory factory, String type) {
4950

50-
EntityManager entityManagerToUse = em.getDelegate() instanceof EntityManager delegate //
51-
? delegate //
52-
: em;
53-
54-
return isOfType(entityManagerToUse, type, entityManagerToUse.getClass().getClassLoader());
51+
return isOfType(factory, type, factory.getClass().getClassLoader());
5552
}
5653

5754
public static boolean isMetamodelOfType(Metamodel metamodel, String type) {

spring-data-jpa/src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
import static org.springframework.data.jpa.provider.PersistenceProvider.Constants.*;
2020

2121
import jakarta.persistence.EntityManager;
22+
import jakarta.persistence.EntityManagerFactory;
2223
import jakarta.persistence.Query;
2324
import jakarta.persistence.metamodel.IdentifiableType;
2425
import jakarta.persistence.metamodel.Metamodel;
2526
import jakarta.persistence.metamodel.SingularAttribute;
2627

28+
import java.lang.reflect.Proxy;
2729
import java.util.Collection;
2830
import java.util.Collections;
2931
import java.util.List;
3032
import java.util.NoSuchElementException;
33+
import java.util.Objects;
3134
import java.util.Set;
3235

3336
import org.eclipse.persistence.config.QueryHints;
@@ -52,6 +55,7 @@
5255
* @author Jens Schauder
5356
* @author Greg Turnquist
5457
* @author Yuriy Tsarkov
58+
* @author Ariel Morelli Andres, Atlassian US, Inc
5559
*/
5660
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment {
5761

@@ -64,7 +68,7 @@ public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, Quer
6468
* @see <a href="https://github.com/spring-projects/spring-data-jpa/issues/846">DATAJPA-444</a>
6569
*/
6670
HIBERNATE(//
67-
Collections.singletonList(HIBERNATE_ENTITY_MANAGER_INTERFACE), //
71+
Collections.singletonList(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE), //
6872
Collections.singletonList(HIBERNATE_JPA_METAMODEL_TYPE)) {
6973

7074
@Override
@@ -114,7 +118,7 @@ public String getCommentHintKey() {
114118
/**
115119
* EclipseLink persistence provider.
116120
*/
117-
ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_INTERFACE),
121+
ECLIPSELINK(Collections.singleton(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE),
118122
Collections.singleton(ECLIPSELINK_JPA_METAMODEL_TYPE)) {
119123

120124
@Override
@@ -152,7 +156,7 @@ public String getCommentHintValue(String comment) {
152156
/**
153157
* Unknown special provider. Use standard JPA.
154158
*/
155-
GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_INTERFACE), Collections.emptySet()) {
159+
GENERIC_JPA(Collections.singleton(GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE), Collections.emptySet()) {
156160

157161
@Nullable
158162
@Override
@@ -199,25 +203,25 @@ public String getCommentHintKey() {
199203
private static final Collection<PersistenceProvider> ALL = List.of(HIBERNATE, ECLIPSELINK, GENERIC_JPA);
200204

201205
private static final ConcurrentReferenceHashMap<Class<?>, PersistenceProvider> CACHE = new ConcurrentReferenceHashMap<>();
202-
private final Iterable<String> entityManagerClassNames;
206+
private final Iterable<String> entityManagerFactoryClassNames;
203207
private final Iterable<String> metamodelClassNames;
204208

205209
private final boolean present;
206210

207211
/**
208212
* Creates a new {@link PersistenceProvider}.
209213
*
210-
* @param entityManagerClassNames the names of the provider specific {@link EntityManager} implementations. Must not
211-
* be {@literal null} or empty.
214+
* @param entityManagerFactoryClassNames the names of the provider specific {@link EntityManagerFactory}
215+
* implementations. Must not be {@literal null} or empty.
212216
* @param metamodelClassNames must not be {@literal null}.
213217
*/
214-
PersistenceProvider(Iterable<String> entityManagerClassNames, Iterable<String> metamodelClassNames) {
218+
PersistenceProvider(Iterable<String> entityManagerFactoryClassNames, Iterable<String> metamodelClassNames) {
215219

216-
this.entityManagerClassNames = entityManagerClassNames;
220+
this.entityManagerFactoryClassNames = entityManagerFactoryClassNames;
217221
this.metamodelClassNames = metamodelClassNames;
218222

219223
boolean present = false;
220-
for (String entityManagerClassName : entityManagerClassNames) {
224+
for (String entityManagerClassName : this.entityManagerFactoryClassNames) {
221225

222226
if (ClassUtils.isPresent(entityManagerClassName, PersistenceProvider.class.getClassLoader())) {
223227
present = true;
@@ -250,23 +254,26 @@ private static PersistenceProvider cacheAndReturn(Class<?> type, PersistenceProv
250254
public static PersistenceProvider fromEntityManager(EntityManager em) {
251255

252256
Assert.notNull(em, "EntityManager must not be null");
253-
254-
Class<?> entityManagerType = em.getDelegate().getClass();
255-
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);
257+
EntityManagerFactory entityManagerFactory = em.getEntityManagerFactory();
258+
EntityManagerFactory targetEntityManagerFactory = Proxy.isProxyClass(entityManagerFactory.getClass())
259+
? Objects.requireNonNullElse(entityManagerFactory.unwrap(null), entityManagerFactory)
260+
: entityManagerFactory;
261+
Class<? extends EntityManagerFactory> entityManagerFactoryType = targetEntityManagerFactory.getClass();
262+
PersistenceProvider cachedProvider = CACHE.get(entityManagerFactoryType);
256263

257264
if (cachedProvider != null) {
258265
return cachedProvider;
259266
}
260267

261268
for (PersistenceProvider provider : ALL) {
262-
for (String entityManagerClassName : provider.entityManagerClassNames) {
263-
if (isEntityManagerOfType(em, entityManagerClassName)) {
264-
return cacheAndReturn(entityManagerType, provider);
269+
for (String entityManagerClassName : provider.entityManagerFactoryClassNames) {
270+
if (isEntityManagerFactoryOfType(targetEntityManagerFactory, entityManagerClassName)) {
271+
return cacheAndReturn(entityManagerFactoryType, provider);
265272
}
266273
}
267274
}
268275

269-
return cacheAndReturn(entityManagerType, GENERIC_JPA);
276+
return cacheAndReturn(entityManagerFactoryType, GENERIC_JPA);
270277
}
271278

272279
/**
@@ -354,10 +361,10 @@ public boolean isPresent() {
354361
*/
355362
interface Constants {
356363

357-
String GENERIC_JPA_ENTITY_MANAGER_INTERFACE = "jakarta.persistence.EntityManager";
358-
String ECLIPSELINK_ENTITY_MANAGER_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManager";
364+
String GENERIC_JPA_ENTITY_MANAGER_FACTORY_INTERFACE = "jakarta.persistence.EntityManagerFactory";
365+
String ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE = "org.eclipse.persistence.jpa.JpaEntityManagerFactory";
359366
// needed as Spring only exposes that interface via the EM proxy
360-
String HIBERNATE_ENTITY_MANAGER_INTERFACE = "org.hibernate.engine.spi.SessionImplementor";
367+
String HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE = "org.hibernate.SessionFactory";
361368

362369
String HIBERNATE_JPA_METAMODEL_TYPE = "org.hibernate.metamodel.model.domain.JpaMetamodel";
363370
String ECLIPSELINK_JPA_METAMODEL_TYPE = "org.eclipse.persistence.internal.jpa.metamodel.MetamodelImpl";

spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUnitTests.java

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121

2222
import jakarta.persistence.EntityManager;
2323

24+
import java.lang.reflect.Proxy;
2425
import java.util.Arrays;
2526
import java.util.Map;
2627

28+
import jakarta.persistence.EntityManagerFactory;
2729
import org.assertj.core.api.Assumptions;
2830
import org.hibernate.Version;
2931
import org.junit.jupiter.api.BeforeEach;
@@ -42,6 +44,7 @@
4244
* @author Thomas Darimont
4345
* @author Oliver Gierke
4446
* @author Jens Schauder
47+
* @author Ariel Morelli Andres, Atlassian US, Inc
4548
*/
4649
class PersistenceProviderUnitTests {
4750

@@ -61,15 +64,15 @@ void detectsEclipseLinkPersistenceProvider() throws Exception {
6164

6265
shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa");
6366

64-
EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE);
67+
EntityManager em = mockEntityManagerWithProviderFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE);
6568

6669
assertThat(fromEntityManager(em)).isEqualTo(ECLIPSELINK);
6770
}
6871

6972
@Test
7073
void fallbackToGenericJpaForUnknownPersistenceProvider() throws Exception {
7174

72-
EntityManager em = mockProviderSpecificEntityManagerInterface("foo.bar.unknown.jpa.JpaEntityManager");
75+
EntityManager em = mockEntityManagerWithProviderFactoryInterface("foo.bar.unknown.jpa.JpaEntityManager");
7376

7477
assertThat(fromEntityManager(em)).isEqualTo(GENERIC_JPA);
7578
}
@@ -81,33 +84,37 @@ void detectsHibernatePersistenceProviderForHibernateVersion52() throws Exception
8184

8285
shadowingClassLoader.excludePackage("org.hibernate");
8386

84-
EntityManager em = mockProviderSpecificEntityManagerInterface(HIBERNATE_ENTITY_MANAGER_INTERFACE);
87+
EntityManager em = mockEntityManagerWithProviderFactoryInterface(HIBERNATE_ENTITY_MANAGER_FACTORY_INTERFACE);
8588

8689
assertThat(fromEntityManager(em)).isEqualTo(HIBERNATE);
8790
}
8891

89-
@Test // DATAJPA-1379
90-
void detectsProviderFromProxiedEntityManager() throws Exception {
92+
@Test
93+
void detectsProviderFromProxiedEntityManagerFactory() throws Exception {
9194

9295
shadowingClassLoader.excludePackage("org.eclipse.persistence.jpa");
9396

94-
EntityManager em = mockProviderSpecificEntityManagerInterface(ECLIPSELINK_ENTITY_MANAGER_INTERFACE);
95-
97+
EntityManager em = mockEntityManagerWithProviderFactoryInterface(ECLIPSELINK_ENTITY_MANAGER_FACTORY_INTERFACE);
98+
EntityManagerFactory proxiedFactory = (EntityManagerFactory) Proxy.newProxyInstance(getClass().getClassLoader(),
99+
em.getEntityManagerFactory().getClass().getInterfaces(), (proxy, method, args) -> switch (method.getName()) {
100+
case "unwrap" -> args[0] == null ? em.getEntityManagerFactory() : this;
101+
default -> method.invoke(em.getEntityManagerFactory(), args);
102+
});
96103
EntityManager emProxy = Mockito.mock(EntityManager.class);
97-
Mockito.when(emProxy.getDelegate()).thenReturn(em);
104+
Mockito.when(emProxy.getEntityManagerFactory()).thenReturn(proxiedFactory);
98105

99106
assertThat(fromEntityManager(emProxy)).isEqualTo(ECLIPSELINK);
100107
}
101108

102-
private EntityManager mockProviderSpecificEntityManagerInterface(String interfaceName) throws ClassNotFoundException {
109+
private EntityManager mockEntityManagerWithProviderFactoryInterface(String factoryInterfaceName)
110+
throws ClassNotFoundException {
103111

104-
Class<?> providerSpecificEntityManagerInterface = InterfaceGenerator.generate(interfaceName, shadowingClassLoader,
105-
EntityManager.class);
112+
Class<?> providerSpecificEntityManagerFactoryInterface = InterfaceGenerator.generate(factoryInterfaceName,
113+
shadowingClassLoader, EntityManagerFactory.class);
106114

107-
EntityManager em = (EntityManager) Mockito.mock(providerSpecificEntityManagerInterface);
108-
Mockito.when(em.getDelegate()).thenReturn(em); // delegate is used to determine the classloader of the provider
109-
// specific interface, therefore we return the proxied
110-
// EntityManager.
115+
EntityManagerFactory factory = (EntityManagerFactory) Mockito.mock(providerSpecificEntityManagerFactoryInterface);
116+
EntityManager em = Mockito.mock(EntityManager.class);
117+
Mockito.when(em.getEntityManagerFactory()).thenReturn(factory);
111118

112119
return em;
113120
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQueryUnitTests.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.mockito.Mockito.*;
1919

2020
import jakarta.persistence.EntityManager;
21+
import jakarta.persistence.EntityManagerFactory;
2122
import jakarta.persistence.metamodel.Metamodel;
2223

2324
import java.lang.reflect.Method;
@@ -53,6 +54,7 @@
5354
*
5455
* @author Christoph Strobl
5556
* @author Mark Paluch
57+
* @author Ariel Morelli Andres, Atlassian US, Inc
5658
*/
5759
class AbstractStringBasedJpaQueryUnitTests {
5860

@@ -135,15 +137,16 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu
135137
public EntityManager get() {
136138

137139
EntityManager em = Mockito.mock(EntityManager.class);
140+
EntityManagerFactory factory = Mockito.mock(EntityManagerFactory.class);
138141

139142
Metamodel meta = mock(Metamodel.class);
140143
when(em.getMetamodel()).thenReturn(meta);
141144
when(em.getDelegate()).thenReturn(new Object()); // some generic jpa
145+
when(em.getEntityManagerFactory()).thenReturn(factory); // some generic jpa
142146

143147
return em;
144148
}
145-
}.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class),
146-
ValueExpressionDelegate.create());
149+
}.get(), queryString, countQueryString, Mockito.mock(QueryRewriter.class), ValueExpressionDelegate.create());
147150

148151
this.targetMethod = targetMethod;
149152
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/SimpleJpaRepositoryUnitTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
* @author Jens Schauder
6161
* @author Greg Turnquist
6262
* @author Yanming Zhou
63+
* @author Ariel Morelli Andres, Atlassian US, Inc
6364
*/
6465
@ExtendWith(MockitoExtension.class)
6566
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -84,6 +85,7 @@ class SimpleJpaRepositoryUnitTests {
8485
void setUp() {
8586

8687
when(em.getDelegate()).thenReturn(em);
88+
when(em.getEntityManagerFactory()).thenReturn(entityManagerFactory);
8789

8890
when(information.getJavaType()).thenReturn(User.class);
8991
when(em.getCriteriaBuilder()).thenReturn(builder);

0 commit comments

Comments
 (0)