Skip to content

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

@mnnayeck

Description

@mnnayeck

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

Activity

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.[/+] on Apr 12, 2024
mp911de

mp911de commented on Apr 15, 2024

@mp911de
Member

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?

mnnayeck

mnnayeck commented on Apr 15, 2024

@mnnayeck
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).

mp911de

mp911de commented on Apr 15, 2024

@mp911de
Member

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

odrotbohm

odrotbohm commented on Apr 17, 2024

@odrotbohm
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

mnnayeck commented on Apr 19, 2024

@mnnayeck
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.

mnnayeck

mnnayeck commented on Apr 19, 2024

@mnnayeck
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.

31 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Relationships

None yet

    Participants

    @odrotbohm@mp911de@mnnayeck@spring-projects-issues@ari-spf

    Issue actions

      Prevent early `EntityManager` access to avoid conflicts with Hibernate-native multi-tenancy · Issue #3425 · spring-projects/spring-data-jpa