Skip to content

Commit b6d41e0

Browse files
committed
Spring Data JPA doesn't boot when using Hibernate with multi-tenancy
Issue: spring-projects#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 52a5e31 commit b6d41e0

16 files changed

+232
-16
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
* @author Jens Schauder
5757
* @author Greg Turnquist
5858
* @author Yuriy Tsarkov
59+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
5960
*/
6061
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment {
6162

@@ -316,15 +317,15 @@ public static PersistenceProvider fromEntityManager(EntityManager em) {
316317
}
317318

318319
/**
319-
* Determines the {@link PersistenceProvider} from the given {@link EntityManager}. If no special one can be
320+
* Determines the {@link PersistenceProvider} from the given {@link EntityManagerFactory}. If no special one can be
320321
* determined {@link #GENERIC_JPA} will be returned.
321322
*
322323
* @param emf must not be {@literal null}.
323324
* @return will never be {@literal null}.
324325
*/
325326
public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) {
326327

327-
Assert.notNull(emf, "EntityManager must not be null");
328+
Assert.notNull(emf, "EntityManagerFactory must not be null");
328329

329330
Class<?> entityManagerType = emf.getPersistenceUnitUtil().getClass();
330331
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
* @author Wonchul Heo
7171
* @author Julia Lee
7272
* @author Yanming Zhou
73+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
7374
*/
7475
public abstract class AbstractJpaQuery implements RepositoryQuery {
7576

@@ -95,7 +96,7 @@ public AbstractJpaQuery(JpaQueryMethod method, EntityManager em) {
9596
this.method = method;
9697
this.em = em;
9798
this.metamodel = JpaMetamodel.of(em.getMetamodel());
98-
this.provider = PersistenceProvider.fromEntityManager(em);
99+
this.provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory());
99100
this.execution = Lazy.of(() -> {
100101

101102
if (method.isStreamQuery()) {

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
* @author Réda Housni Alaoui
6868
* @author Gabriel Basilio
6969
* @author Greg Turnquist
70+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
7071
*/
7172
public class JpaRepositoryFactory extends RepositoryFactorySupport {
7273

@@ -91,7 +92,8 @@ public JpaRepositoryFactory(EntityManager entityManager) {
9192
Assert.notNull(entityManager, "EntityManager must not be null");
9293

9394
this.entityManager = entityManager;
94-
PersistenceProvider extractor = PersistenceProvider.fromEntityManager(entityManager);
95+
PersistenceProvider extractor = PersistenceProvider
96+
.fromEntityManagerFactory(entityManager.getEntityManagerFactory());
9597
this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
9698
this.entityPathResolver = SimpleEntityPathResolver.INSTANCE;
9799
this.queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor);

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/Querydsl.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import com.querydsl.jpa.JPQLTemplates;
4141
import com.querydsl.jpa.impl.AbstractJPAQuery;
4242
import com.querydsl.jpa.impl.JPAQuery;
43-
import org.jspecify.annotations.Nullable;
4443

4544
/**
4645
* Helper instance to ease access to Querydsl JPA query API.
@@ -51,6 +50,7 @@
5150
* @author Christoph Strobl
5251
* @author Marcus Voltolim
5352
* @author Donghun Shin
53+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
5454
*/
5555
public class Querydsl {
5656

@@ -70,7 +70,7 @@ public Querydsl(EntityManager em, PathBuilder<?> builder) {
7070
Assert.notNull(builder, "PathBuilder must not be null");
7171

7272
this.em = em;
73-
this.provider = PersistenceProvider.fromEntityManager(em);
73+
this.provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory());
7474
this.builder = builder;
7575
}
7676

@@ -87,7 +87,8 @@ public <T> AbstractJPAQuery<T, JPAQuery<T>> createQuery() {
8787
* Obtains the {@link JPQLTemplates} for the configured {@link EntityManager}. Can return {@literal null} to use the
8888
* default templates.
8989
*
90-
* @return the {@link JPQLTemplates} for the configured {@link EntityManager}, {@link JPQLTemplates#DEFAULT} by default.
90+
* @return the {@link JPQLTemplates} for the configured {@link EntityManager}, {@link JPQLTemplates#DEFAULT} by
91+
* default.
9192
* @since 3.5
9293
*/
9394
public JPQLTemplates getTemplates() {

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
* @author Diego Krupitza
105105
* @author Seol-JY
106106
* @author Joshua Chen
107+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
107108
*/
108109
@Repository
109110
@Transactional(readOnly = true)
@@ -138,7 +139,7 @@ public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityM
138139

139140
this.entityInformation = entityInformation;
140141
this.entityManager = entityManager;
141-
this.provider = PersistenceProvider.fromEntityManager(entityManager);
142+
this.provider = PersistenceProvider.fromEntityManagerFactory(entityManager.getEntityManagerFactory());
142143
this.projectionFactory = new SpelAwareProxyProjectionFactory();
143144
}
144145

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import jakarta.persistence.EntityManager;
2424
import jakarta.persistence.EntityManagerFactory;
2525
import jakarta.persistence.LockModeType;
26+
import jakarta.persistence.PersistenceUnitUtil;
2627
import jakarta.persistence.TypedQuery;
2728
import jakarta.persistence.criteria.CriteriaBuilder;
2829
import jakarta.persistence.criteria.CriteriaQuery;
@@ -47,13 +48,15 @@
4748
*
4849
* @author Oliver Gierke
4950
* @author Thomas Darimont
51+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
5052
*/
5153
@ExtendWith(MockitoExtension.class)
5254
@MockitoSettings(strictness = Strictness.LENIENT)
5355
class CrudMethodMetadataUnitTests {
5456

5557
@Mock EntityManager em;
5658
@Mock EntityManagerFactory emf;
59+
@Mock PersistenceUnitUtil persistenceUnitUtil;
5760
@Mock CriteriaBuilder builder;
5861
@Mock CriteriaQuery<Role> criteriaQuery;
5962
@Mock JpaEntityInformation<Role, Integer> information;
@@ -72,6 +75,7 @@ void setUp() {
7275
when(em.getDelegate()).thenReturn(em);
7376
when(em.getEntityManagerFactory()).thenReturn(emf);
7477
when(emf.createEntityManager()).thenReturn(em);
78+
when(emf.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil);
7579

7680
JpaRepositoryFactory factory = new JpaRepositoryFactory(em) {
7781
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository;
17+
18+
import java.util.Optional;
19+
20+
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
21+
import org.jspecify.annotations.Nullable;
22+
23+
/**
24+
* {@code CurrentTenantIdentifierResolver} instance for testing
25+
*
26+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
27+
*/
28+
public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver<String> {
29+
private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>();
30+
31+
public static void setTenantIdentifier(String tenantIdentifier) {
32+
CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier);
33+
}
34+
35+
public static void removeTenantIdentifier() {
36+
CURRENT_TENANT_IDENTIFIER.remove();
37+
}
38+
39+
@Override
40+
public String resolveCurrentTenantIdentifier() {
41+
return Optional.ofNullable(CURRENT_TENANT_IDENTIFIER.get())
42+
.orElseThrow(() -> new IllegalArgumentException("Could not resolve current tenant identifier"));
43+
}
44+
45+
@Override
46+
public boolean validateExistingCurrentSessions() {
47+
return true;
48+
}
49+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2011-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.assertj.core.api.Assumptions.*;
20+
21+
import java.util.List;
22+
23+
import org.junit.jupiter.api.AfterEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.context.annotation.ComponentScan;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.context.annotation.FilterType;
30+
import org.springframework.context.annotation.ImportResource;
31+
import org.springframework.data.jpa.domain.sample.Role;
32+
import org.springframework.data.jpa.provider.PersistenceProvider;
33+
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
34+
import org.springframework.data.jpa.repository.sample.RoleRepository;
35+
import org.springframework.test.context.ContextConfiguration;
36+
import org.springframework.test.context.junit.jupiter.SpringExtension;
37+
import org.springframework.transaction.annotation.Transactional;
38+
39+
import jakarta.persistence.EntityManager;
40+
41+
/**
42+
* Tests for repositories that use multi-tenancy. This tests verifies that repositories can be created an injected
43+
* despite not having a tenant available at creation time
44+
*
45+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
46+
*/
47+
@ExtendWith(SpringExtension.class)
48+
@ContextConfiguration()
49+
class HibernateMultitenancyTests {
50+
51+
@Autowired RoleRepository roleRepository;
52+
@Autowired EntityManager em;
53+
54+
@AfterEach
55+
void tearDown() {
56+
HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier();
57+
}
58+
59+
@Test
60+
void testPersistenceProviderFromFactoryWithoutTenant() {
61+
PersistenceProvider provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory());
62+
assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE);
63+
}
64+
65+
@Test
66+
void testRepositoryWithTenant() {
67+
HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id");
68+
assertThatNoException().isThrownBy(() -> roleRepository.findAll());
69+
}
70+
71+
@Test
72+
void testRepositoryWithoutTenantFails() {
73+
assertThatThrownBy(() -> roleRepository.findAll()).isInstanceOf(RuntimeException.class);
74+
}
75+
76+
@Transactional
77+
List<Role> insertAndQuery() {
78+
roleRepository.save(new Role("DRUMMER"));
79+
roleRepository.flush();
80+
return roleRepository.findAll();
81+
}
82+
83+
@ImportResource({ "classpath:multitenancy-test.xml" })
84+
@Configuration
85+
@EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true,
86+
includeFilters = @ComponentScan.Filter(classes = { RoleRepository.class }, type = FilterType.ASSIGNABLE_TYPE))
87+
static class TestConfig {}
88+
}

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

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

2020
import jakarta.persistence.EntityManager;
21+
import jakarta.persistence.EntityManagerFactory;
22+
import jakarta.persistence.PersistenceUnitUtil;
2123
import jakarta.persistence.metamodel.Metamodel;
2224

2325
import java.lang.reflect.Method;
@@ -52,6 +54,7 @@
5254
*
5355
* @author Christoph Strobl
5456
* @author Mark Paluch
57+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
5558
*/
5659
class AbstractStringBasedJpaQueryUnitTests {
5760

@@ -137,10 +140,14 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu
137140
public EntityManager get() {
138141

139142
EntityManager em = Mockito.mock(EntityManager.class);
143+
EntityManagerFactory emf = Mockito.mock(EntityManagerFactory.class);
144+
PersistenceUnitUtil puu = Mockito.mock(PersistenceUnitUtil.class);
140145

141146
Metamodel meta = mock(Metamodel.class);
142147
when(em.getMetamodel()).thenReturn(meta);
143148
when(em.getDelegate()).thenReturn(new Object()); // some generic jpa
149+
when(em.getEntityManagerFactory()).thenReturn(emf);
150+
when(emf.getPersistenceUnitUtil()).thenReturn(puu); // some generic jpa
144151

145152
return em;
146153
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import jakarta.persistence.EntityManager;
2222
import jakarta.persistence.EntityManagerFactory;
23+
import jakarta.persistence.PersistenceUnitUtil;
2324
import jakarta.persistence.metamodel.Metamodel;
2425

2526
import java.lang.reflect.Method;
@@ -58,6 +59,7 @@
5859
* @author Jens Schauder
5960
* @author Réda Housni Alaoui
6061
* @author Greg Turnquist
62+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
6163
*/
6264
@ExtendWith(MockitoExtension.class)
6365
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -68,6 +70,7 @@ class JpaQueryLookupStrategyUnitTests {
6870

6971
@Mock EntityManager em;
7072
@Mock EntityManagerFactory emf;
73+
@Mock PersistenceUnitUtil puu;
7174
@Mock QueryExtractor extractor;
7275
@Mock NamedQueries namedQueries;
7376
@Mock Metamodel metamodel;
@@ -81,6 +84,7 @@ void setUp() {
8184
when(em.getMetamodel()).thenReturn(metamodel);
8285
when(em.getEntityManagerFactory()).thenReturn(emf);
8386
when(emf.createEntityManager()).thenReturn(em);
87+
when(emf.getPersistenceUnitUtil()).thenReturn(puu);
8488
when(em.getDelegate()).thenReturn(em);
8589
queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor);
8690
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import jakarta.persistence.EntityManager;
2222
import jakarta.persistence.EntityManagerFactory;
23+
import jakarta.persistence.PersistenceUnitUtil;
2324
import jakarta.persistence.TypedQuery;
2425
import jakarta.persistence.metamodel.Metamodel;
2526

@@ -36,7 +37,6 @@
3637
import org.springframework.data.domain.Page;
3738
import org.springframework.data.domain.Pageable;
3839
import org.springframework.data.jpa.provider.QueryExtractor;
39-
import org.springframework.data.jpa.repository.QueryRewriter;
4040
import org.springframework.data.projection.ProjectionFactory;
4141
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
4242
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -51,6 +51,7 @@
5151
* @author Thomas Darimont
5252
* @author Mark Paluch
5353
* @author Erik Pellizzon
54+
* @author Ariel Morelli Andres (Atlassian US, Inc.)
5455
*/
5556
@ExtendWith(MockitoExtension.class)
5657
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -63,6 +64,7 @@ class NamedQueryUnitTests {
6364
@Mock QueryExtractor extractor;
6465
@Mock EntityManager em;
6566
@Mock EntityManagerFactory emf;
67+
@Mock PersistenceUnitUtil puu;
6668
@Mock Metamodel metamodel;
6769

6870
private ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
@@ -84,6 +86,7 @@ void setUp() throws SecurityException, NoSuchMethodException {
8486
when(em.getEntityManagerFactory()).thenReturn(emf);
8587
when(em.getDelegate()).thenReturn(em);
8688
when(emf.createEntityManager()).thenReturn(em);
89+
when(emf.getPersistenceUnitUtil()).thenReturn(puu);
8790
}
8891

8992
@Test

0 commit comments

Comments
 (0)