Skip to content

Prevent early EntityManager access to avoid conflicts with Hibernate-native multi-tenancy #3425

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
mnnayeck opened this issue Apr 12, 2024 · 11 comments
Assignees
Labels
type: enhancement A general enhancement

Comments

@mnnayeck
Copy link

mnnayeck commented Apr 12, 2024

I have configured Hibernate's native multitenancy in the attached persistence-tenant.txt.
As you can see:

<property name="hibernate.multiTenancy" value="DATABASE" /> <property name="hibernate.multi_tenant_connection_provider" value="mu.maccs.ffa.dao.spi.MultiTenantConnectionProviderImpl" /> <property name="hibernate.tenant_identifier_resolver" value="mu.maccs.ffa.dao.spi.MultiTenantIdentifierResolverImpl" />

I am using a simple JPARepository class as attached.

The problem is that on bootstrapping, org.springframework.data.jpa.repository.support.JpaRepositoryFactory's constructor makes the following call:
this.extractor = PersistenceProvider.fromEntityManager(entityManager);

which actually creates a hibernate sessionImpl to determine the PersistenceProvider to use in the JPARepository.

This is wrong because since my application is not executing any database call on boot, a Hibernate session creation is FORCED (which in turns creates a connection with the underlying datasource for that particular tenant). This line is the culprit:
Class<?> entityManagerType = em.getDelegate().getClass();

Because of this, I am forced to set a default tenantIdentifier in MultiTenantIdentifierResolverImpl which isn't right. There should be NO tenant resolution on creation of JpaRepositoryFactory.
persistence-tenant.txt
UserRepository.txt
MultiTenantConnectionProviderImpl.txt
MultiTenantIdentifierResolverImpl.txt

Log Trace:

2024-04-13 00:19:20,870 [main] TRACE org.springframework.beans.factory.support.DefaultListableBeanFactory.invokeInitMethods:1831 - Invoking afterPropertiesSet() on bean with name 'userRepository'
2024-04-13 00:19:20,884 [main] TRACE org.springframework.core.io.support.SpringFactoriesLoader.load:202 - Loaded [org.springframework.data.util.CustomCollectionRegistrar] names: [org.springframework.data.util.CustomCollections.VavrCollections, org.springframework.data.util.CustomCollections.EclipseCollections]
2024-04-13 00:19:37,009 [main] DEBUG org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke:310 - Creating new EntityManager for shared EntityManager invocation
2024-04-13 00:19:37,183 [main] TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl.openSession:1363 - Opening Hibernate Session.  tenant=root
2024-04-13 00:19:39,034 [main] TRACE org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService:234 - Initializing service [role=org.hibernate.stat.spi.StatisticsImplementor]
2024-04-13 00:19:39,446 [main] TRACE org.hibernate.internal.SessionImpl.<init>:272 - Opened Session [23989d6b-324a-4a3d-b4f2-3a358f5ff536] at timestamp: 1712953179446
2024-04-13 00:19:41,112 [main] TRACE org.hibernate.internal.SessionImpl.closeWithoutOpenChecks:396 - Closing session [23989d6b-324a-4a3d-b4f2-3a358f5ff536]
2024-04-13 00:19:41,117 [main] TRACE org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.close:152 - Closing JDBC container [org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl@67c020c8]
2024-04-13 00:19:41,123 [main] TRACE org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl.releaseResources:329 - Releasing JDBC resources
2024-04-13 00:19:41,133 [main] TRACE org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.close:256 - Closing logical connection
2024-04-13 00:19:41,139 [main] TRACE org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.close:263 - Logical connection closed
2024-04-13 00:19:41,146 [main] DEBUG org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke:310 - Creating new EntityManager for shared EntityManager invocation
2024-04-13 00:19:41,151 [main] TRACE org.hibernate.internal.SessionFactoryImpl$SessionBuilderImpl.openSession:1363 - Opening Hibernate Session.  tenant=root
2024-04-13 00:19:41,160 [main] TRACE org.hibernate.internal.SessionImpl.<init>:272 - Opened Session [52517467-b3ab-49aa-8efb-832f41f21f60] at timestamp: 1712953181160
2024-04-13 00:19:41,166 [main] TRACE org.hibernate.internal.SessionImpl.closeWithoutOpenChecks:396 - Closing session [52517467-b3ab-49aa-8efb-832f41f21f60]
2024-04-13 00:19:41,172 [main] TRACE org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.close:152 - Closing JDBC container [org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl@66464f27]
2024-04-13 00:19:41,177 [main] TRACE org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl.releaseResources:329 - Releasing JDBC resources
2024-04-13 00:19:41,182 [main] TRACE org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.close:256 - Closing logical connection
2024-04-13 00:19:41,189 [main] TRACE org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.close:263 - Logical connection closed
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Apr 12, 2024
@mnnayeck mnnayeck changed the title Using natirce Hibernate multinenancy with Spring Data JPA breaks on application boot. Using native Hibernate multitenancy with Spring Data JPA breaks on application boot. Apr 12, 2024
@mp911de
Copy link
Member

mp911de commented Apr 15, 2024

JpaRepositoryFactory isn't tied to multi-tenancy identifiers. Upon application startup we need to obtain the EntityManager for various initialization tasks, verification of queries and introspection of managed types to properly start up the repository infrastructure.

At some point, this has to happen. Since repositories aren't tied to tenant information, what would be an alternative?

@mp911de mp911de added the status: waiting-for-feedback We need additional information before we can continue label Apr 15, 2024
@mnnayeck
Copy link
Author

I understand and agree with your point. I just think there must be another way to retrieve the extractor
The line causing problem at his point is
Class<?> entityManagerType = em.getDelegate().getClass();
which actually makes a connection to the database (which forces the developer to specify a tenant identifier for the connection to be successful to the datasource).

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 15, 2024
@mp911de
Copy link
Member

mp911de commented Apr 15, 2024

Since you're already looking into the issue, care to investigate an approach that would work for you?

@mp911de mp911de added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Apr 15, 2024
@odrotbohm
Copy link
Member

I briefly thought about switching to checking the EMF for its type rather than the delegate, but it turns out both Hibernate's implementation of ….getEntityManagerFactory() and ….getDelegate() call checkOpen(). I am assuming that that call is triggering the opening of a connection, as getDelegate() simply returns this.

To me there are two aspects to this:

  1. Unless there's a way for us to detect the actual JPA persistence provider from an EntityManager instance, it's gonna be hard for us to fix the problem. We could inspect Spring configuration setup, but that would couple our detection logic to more to expecting developers to set up their JPA infrastructure through it. While that's probably given for most of our user base, it might not be the case for all.
  2. If we were able to delay the call to the EntityManager in the constructor of the repository factory, would that really help? As Mark pointed out, at some point we will have to interact with the EntityManager to validate queries. If any of those steps requires a connection to be established (I don't know whether that's true or not) you'd still have to answer the question of how to obtain a tenant identifier from the initialization process in general. How would you like to provide that?

We could still give the EMF inspection a try though as Spring wraps the EM into a proxy that delegates calls to getEntityManagerFactory() to return the Spring-managed EMF directly.

@mnnayeck
Copy link
Author

Hello Olivier,

Regarding the second point, I don't think you can the call to the EntityManager. This means that you would need to delay it until the first call to the persistence layer done by the application.

Regarding your first point, the problem is getDelegate() which is implementation specific. I checked for Hibernate and EclipseLink - both call checkOpen() to simply open a connection before returning 'this'.

Hibernate:
@Override public Object getDelegate() { checkOpen(); return this; }

EclipseLink:
@Override public Object getDelegate() { try { verifyOpen(); return this; } catch (RuntimeException e) { setRollbackOnly(); throw e; } }

Honestly, I think we need to update the JPA spec to add em.getDelegateClass() method. This would be much much cleaner.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 19, 2024
@mnnayeck
Copy link
Author

I briefly tried to use em.getMetaModel() to try resolve the issue. but those also make a call to checkOpen() - both in EclipseLink as well as Hibernate. That's a dead end.

@mp911de mp911de added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided labels Apr 19, 2024
@mp911de
Copy link
Member

mp911de commented Apr 19, 2024

For the time being we do not have a workaround that would work reliably, hence closing that ticket as it isn't actionable.

@mp911de mp911de closed this as not planned Won't fix, can't repro, duplicate, stale Apr 19, 2024
@ari-spf
Copy link
Contributor

ari-spf commented May 7, 2025

@mp911de what about using the EntityManagerFactory instead of the EntityManager to avoid requesting a tenant?

I'm imagining something like this: tenantive-idea.patch

edit: code is not fully formatted, but at least tests pass

If you guys agree with the idea, I can try to put together a PR.

@ari-spf
Copy link
Contributor

ari-spf commented May 8, 2025

The reasoning for my above idea is based on the fact that the proxy that Spring injects for the EntityManager does not get initialized upon calling the getEntityManagerFactory method:

Image

ari-spf added a commit to ari-spf/spring-data-jpa that referenced this issue May 15, 2025
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]>
ari-spf added a commit to ari-spf/spring-data-jpa that referenced this issue May 15, 2025
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]>
@ari-spf
Copy link
Contributor

ari-spf commented May 15, 2025

@mp911de @odrotbohm: I've submitted a PR #3885
I'd appreciate if you can take a look

ari-spf added a commit to ari-spf/spring-data-jpa that referenced this issue May 23, 2025
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]>
ari-spf added a commit to ari-spf/spring-data-jpa that referenced this issue Jun 4, 2025
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]>
ari-spf added a commit to ari-spf/spring-data-jpa that referenced this issue Jun 4, 2025
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]>
@mp911de mp911de changed the title Using native Hibernate multitenancy with Spring Data JPA breaks on application boot. Using native Hibernate multitenancy with Spring Data JPA breaks on application boot Jun 5, 2025
@mp911de mp911de added type: enhancement A general enhancement and removed status: declined A suggestion or change that we don't feel we should currently apply labels Jun 5, 2025
@mp911de mp911de reopened this Jun 5, 2025
@mp911de mp911de added this to the 3.5.1 (2025.0.1) milestone Jun 5, 2025
@mp911de mp911de self-assigned this Jun 5, 2025
@mp911de mp911de changed the title Using native Hibernate multitenancy with Spring Data JPA breaks on application boot Using Hibernate-native multi-tenancy with Spring Data JPA breaks on application boot Jun 6, 2025
@mp911de mp911de changed the title Using Hibernate-native multi-tenancy with Spring Data JPA breaks on application boot Early EntityManager access conflicts with Hibernate-native multi-tenancy Jun 6, 2025
@mp911de mp911de changed the title Early EntityManager access conflicts with Hibernate-native multi-tenancy Prevent early EntityManager access to avoid conflicts with Hibernate-native multi-tenancy Jun 6, 2025
@mp911de
Copy link
Member

mp911de commented Jun 6, 2025

We can delay EntityManager access when determining PersistenceProvider. However, for named query lookup and query validation of methods annotated with @Query, you we still run into EntityManager access that we cannot disable. Especially with Hibernate, looking up a named query is quite an adventure because Hibernate doesn't stick to EntityManagerFactory.getNamedQueries(Class) semantics regarding type assignability, rather they check the exact type and for subclasses of a query, that lookup cannot be easily done.

In any case, #3885 helps with reducing early access.

@mp911de mp911de closed this as completed in ab40236 Jun 6, 2025
mp911de added a commit that referenced this issue Jun 6, 2025
Revise PersistenceProvider detection to a EntityManagerFactory-based variant, considering EntityManagerFactory proxying.

See: #3425
Original pull request: #3885
mp911de added a commit that referenced this issue Jun 6, 2025
Signed-off-by: Ariel Morelli Andres <[email protected]>

Closes: #3425
Original pull request: #3885
mp911de added a commit that referenced this issue Jun 6, 2025
Revise PersistenceProvider detection to a EntityManagerFactory-based variant, considering EntityManagerFactory proxying.

See: #3425
Original pull request: #3885
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants