Skip to content

Avoid early EntityManager initialization during PersistenceProvider lookup #3885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
* @author Jens Schauder
* @author Greg Turnquist
* @author Yuriy Tsarkov
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
public enum PersistenceProvider implements QueryExtractor, ProxyIdAccessor, QueryComment {

Expand Down Expand Up @@ -316,15 +317,15 @@ public static PersistenceProvider fromEntityManager(EntityManager em) {
}

/**
* Determines the {@link PersistenceProvider} from the given {@link EntityManager}. If no special one can be
* Determines the {@link PersistenceProvider} from the given {@link EntityManagerFactory}. If no special one can be
* determined {@link #GENERIC_JPA} will be returned.
*
* @param emf must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static PersistenceProvider fromEntityManagerFactory(EntityManagerFactory emf) {

Assert.notNull(emf, "EntityManager must not be null");
Assert.notNull(emf, "EntityManagerFactory must not be null");

Class<?> entityManagerType = emf.getPersistenceUnitUtil().getClass();
PersistenceProvider cachedProvider = CACHE.get(entityManagerType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
* @author Wonchul Heo
* @author Julia Lee
* @author Yanming Zhou
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
public abstract class AbstractJpaQuery implements RepositoryQuery {

Expand All @@ -95,7 +96,7 @@ public AbstractJpaQuery(JpaQueryMethod method, EntityManager em) {
this.method = method;
this.em = em;
this.metamodel = JpaMetamodel.of(em.getMetamodel());
this.provider = PersistenceProvider.fromEntityManager(em);
this.provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory());
this.execution = Lazy.of(() -> {

if (method.isStreamQuery()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
* @author Réda Housni Alaoui
* @author Gabriel Basilio
* @author Greg Turnquist
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
public class JpaRepositoryFactory extends RepositoryFactorySupport {

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

this.entityManager = entityManager;
PersistenceProvider extractor = PersistenceProvider.fromEntityManager(entityManager);
PersistenceProvider extractor = PersistenceProvider
.fromEntityManagerFactory(entityManager.getEntityManagerFactory());
this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
this.entityPathResolver = SimpleEntityPathResolver.INSTANCE;
this.queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
import com.querydsl.jpa.JPQLTemplates;
import com.querydsl.jpa.impl.AbstractJPAQuery;
import com.querydsl.jpa.impl.JPAQuery;
import org.jspecify.annotations.Nullable;

/**
* Helper instance to ease access to Querydsl JPA query API.
Expand All @@ -51,6 +50,7 @@
* @author Christoph Strobl
* @author Marcus Voltolim
* @author Donghun Shin
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
public class Querydsl {

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

this.em = em;
this.provider = PersistenceProvider.fromEntityManager(em);
this.provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory());
this.builder = builder;
}

Expand All @@ -87,7 +87,8 @@ public <T> AbstractJPAQuery<T, JPAQuery<T>> createQuery() {
* Obtains the {@link JPQLTemplates} for the configured {@link EntityManager}. Can return {@literal null} to use the
* default templates.
*
* @return the {@link JPQLTemplates} for the configured {@link EntityManager}, {@link JPQLTemplates#DEFAULT} by default.
* @return the {@link JPQLTemplates} for the configured {@link EntityManager}, {@link JPQLTemplates#DEFAULT} by
* default.
* @since 3.5
*/
public JPQLTemplates getTemplates() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
* @author Diego Krupitza
* @author Seol-JY
* @author Joshua Chen
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
@Repository
@Transactional(readOnly = true)
Expand Down Expand Up @@ -138,7 +139,7 @@ public SimpleJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityM

this.entityInformation = entityInformation;
this.entityManager = entityManager;
this.provider = PersistenceProvider.fromEntityManager(entityManager);
this.provider = PersistenceProvider.fromEntityManagerFactory(entityManager.getEntityManagerFactory());
this.projectionFactory = new SpelAwareProxyProjectionFactory();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.LockModeType;
import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
Expand All @@ -47,13 +48,15 @@
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CrudMethodMetadataUnitTests {

@Mock EntityManager em;
@Mock EntityManagerFactory emf;
@Mock PersistenceUnitUtil persistenceUnitUtil;
@Mock CriteriaBuilder builder;
@Mock CriteriaQuery<Role> criteriaQuery;
@Mock JpaEntityInformation<Role, Integer> information;
Expand All @@ -72,6 +75,7 @@ void setUp() {
when(em.getDelegate()).thenReturn(em);
when(em.getEntityManagerFactory()).thenReturn(emf);
when(emf.createEntityManager()).thenReturn(em);
when(emf.getPersistenceUnitUtil()).thenReturn(persistenceUnitUtil);

JpaRepositoryFactory factory = new JpaRepositoryFactory(em) {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2011-2025 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.data.jpa.repository;

import java.util.Optional;

import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.jspecify.annotations.Nullable;

/**
* {@code CurrentTenantIdentifierResolver} instance for testing
*
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
public class HibernateCurrentTenantIdentifierResolver implements CurrentTenantIdentifierResolver<String> {
private static final ThreadLocal<@Nullable String> CURRENT_TENANT_IDENTIFIER = new ThreadLocal<>();

public static void setTenantIdentifier(String tenantIdentifier) {
CURRENT_TENANT_IDENTIFIER.set(tenantIdentifier);
}

public static void removeTenantIdentifier() {
CURRENT_TENANT_IDENTIFIER.remove();
}

@Override
public String resolveCurrentTenantIdentifier() {
return Optional.ofNullable(CURRENT_TENANT_IDENTIFIER.get())
.orElseThrow(() -> new IllegalArgumentException("Could not resolve current tenant identifier"));
}

@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2011-2025 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.data.jpa.repository;

import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.*;

import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ImportResource;
import org.springframework.data.jpa.domain.sample.Role;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.jpa.repository.sample.RoleRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import jakarta.persistence.EntityManager;

/**
* Tests for repositories that use multi-tenancy. This tests verifies that repositories can be created an injected
* despite not having a tenant available at creation time
*
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration()
class HibernateMultitenancyTests {

@Autowired RoleRepository roleRepository;
@Autowired EntityManager em;

@AfterEach
void tearDown() {
HibernateCurrentTenantIdentifierResolver.removeTenantIdentifier();
}

@Test
void testPersistenceProviderFromFactoryWithoutTenant() {
PersistenceProvider provider = PersistenceProvider.fromEntityManagerFactory(em.getEntityManagerFactory());
assumeThat(provider).isEqualTo(PersistenceProvider.HIBERNATE);
}

@Test
void testRepositoryWithTenant() {
HibernateCurrentTenantIdentifierResolver.setTenantIdentifier("tenant-id");
assertThatNoException().isThrownBy(() -> roleRepository.findAll());
}

@Test
void testRepositoryWithoutTenantFails() {
assertThatThrownBy(() -> roleRepository.findAll()).isInstanceOf(RuntimeException.class);
}

@Transactional
List<Role> insertAndQuery() {
roleRepository.save(new Role("DRUMMER"));
roleRepository.flush();
return roleRepository.findAll();
}

@ImportResource({ "classpath:multitenancy-test.xml" })
@Configuration
@EnableJpaRepositories(basePackageClasses = HibernateRepositoryTests.class, considerNestedRepositories = true,
includeFilters = @ComponentScan.Filter(classes = { RoleRepository.class }, type = FilterType.ASSIGNABLE_TYPE))
static class TestConfig {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import static org.mockito.Mockito.*;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.metamodel.Metamodel;

import java.lang.reflect.Method;
Expand Down Expand Up @@ -52,6 +54,7 @@
*
* @author Christoph Strobl
* @author Mark Paluch
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
class AbstractStringBasedJpaQueryUnitTests {

Expand Down Expand Up @@ -137,10 +140,14 @@ static class InvocationCapturingStringQueryStub extends AbstractStringBasedJpaQu
public EntityManager get() {

EntityManager em = Mockito.mock(EntityManager.class);
EntityManagerFactory emf = Mockito.mock(EntityManagerFactory.class);
PersistenceUnitUtil puu = Mockito.mock(PersistenceUnitUtil.class);

Metamodel meta = mock(Metamodel.class);
when(em.getMetamodel()).thenReturn(meta);
when(em.getDelegate()).thenReturn(new Object()); // some generic jpa
when(em.getEntityManagerFactory()).thenReturn(emf);
when(emf.getPersistenceUnitUtil()).thenReturn(puu); // some generic jpa

return em;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.metamodel.Metamodel;

import java.lang.reflect.Method;
Expand Down Expand Up @@ -58,6 +59,7 @@
* @author Jens Schauder
* @author Réda Housni Alaoui
* @author Greg Turnquist
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
Expand All @@ -68,6 +70,7 @@ class JpaQueryLookupStrategyUnitTests {

@Mock EntityManager em;
@Mock EntityManagerFactory emf;
@Mock PersistenceUnitUtil puu;
@Mock QueryExtractor extractor;
@Mock NamedQueries namedQueries;
@Mock Metamodel metamodel;
Expand All @@ -81,6 +84,7 @@ void setUp() {
when(em.getMetamodel()).thenReturn(metamodel);
when(em.getEntityManagerFactory()).thenReturn(emf);
when(emf.createEntityManager()).thenReturn(em);
when(emf.getPersistenceUnitUtil()).thenReturn(puu);
when(em.getDelegate()).thenReturn(em);
queryMethodFactory = new DefaultJpaQueryMethodFactory(extractor);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.metamodel.Metamodel;

Expand All @@ -36,7 +37,6 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.provider.QueryExtractor;
import org.springframework.data.jpa.repository.QueryRewriter;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
Expand All @@ -51,6 +51,7 @@
* @author Thomas Darimont
* @author Mark Paluch
* @author Erik Pellizzon
* @author Ariel Morelli Andres (Atlassian US, Inc.)
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
Expand All @@ -63,6 +64,7 @@ class NamedQueryUnitTests {
@Mock QueryExtractor extractor;
@Mock EntityManager em;
@Mock EntityManagerFactory emf;
@Mock PersistenceUnitUtil puu;
@Mock Metamodel metamodel;

private ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
Expand All @@ -84,6 +86,7 @@ void setUp() throws SecurityException, NoSuchMethodException {
when(em.getEntityManagerFactory()).thenReturn(emf);
when(em.getDelegate()).thenReturn(em);
when(emf.createEntityManager()).thenReturn(em);
when(emf.getPersistenceUnitUtil()).thenReturn(puu);
}

@Test
Expand Down
Loading