Dominik Bartholdi opened SPR-16518 and commented

in the context of #5615, setFilterName and setFilterNames methods have been added to HibernateAccessor to support enabling Hibernate Filters for Hibernate 3. Unfortunate there seems no equivalent way to enable Hibernate Filters when using Spring Data Repositories with Hibernate 5.

Sure, I can create a pointcut that matches all repositories:

@Pointcut("execution(* org.springframework.data.repository.Repository+.*(..))")
public void activateTenantFilter() throws Throwable {
     Optional<Long> tenantId = TenantUtil.getCurrentTenantId();
     tenantId.ifPresent(id -> {
          Session session = entityManager.unwrap(Session.class);
          Filter filter = session.enableFilter("TENANT_FILTER");
          filter.setParameter("tenantId", id);
     });
}

But this means the aspect is executed way to many times - e.g. if I have a service calling multiple repository methods.

Another way would be to define a custom annotation (e.g. @EnableTenantFilter) and annotate service methods with it to enable the filter based on a Pointcut specific for this Annotation and do the same as in above, but this could lead to the same issue as above.

A nicer way would be to define a Pointcut for org.hibernate.SessionBuilder.openSession, but as SessionBuilder is not exposed as a Bean, but this requires Load Time Weaving :(

@AfterReturning(pointcut = "execution(* org.hibernate.SessionBuilder.openSession(..))", returning = "session")
    public void forceFilter(JoinPoint joinPoint, Object session) {
          ...
          Filter filter = session.enableFilter("TENANT_FILTER");
          filter.setParameter("tenantId", id);
    }

Also DATACMNS-293 would provide a way to implement such a filter (even independent of Hibernate), but it does not seem to be merged anytime soon :(

A way to ease this, would be some kind of a listener that is called whenever a new Hibernate Session is created and allows me to enable the Filter based on my criteria at that point. An important point is that I need to be able to pass parameters to the filter, otherwise it does not make a lot of sense.

Maybe I have missed something and this already exists...


No further details from SPR-16518

Comment From: spring-projects-issues

Juergen Hoeller commented

Oliver Drotbohm, what's your take on this from a Spring Data perspective?

Comment From: spring-projects-issues

Dominik Bartholdi commented

Juergen Hoeller is there any chance to make this easier?

Comment From: bugy

Hi, I'd like to note that AOP approach (the first example from Dominik) with repositories doesn't work for spring.jpa.open-in-view = false (which is recommended).

So it seems that the only option is aspectj load time weaving. And this is very cumbersome for spring boot applications.

Are there any other ways to intercept sessions and enable filters?

Comment From: andrei-ivanov

See #25125

Comment From: jofatmofn

@bugy, Looked at your code in #25125. Sorry for the naive question - Could you please tell me how to use this in my application. Which class to put your code and what additional changes do I need to make to enable the filter for all the repository methods of selective repositories? Thanks.

Comment From: bugy

Hi @jofatmofn, this is just a normal spring @Configuration bean creation, i.e. you just put this method in a class like that and it will work:

@Configuration
public class HibernateFilterConfig {
    @Bean
    @ConditionalOnMissingBean
    public PlatformTransactionManager transactionManager(
            ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
        // ... code from #25125
    }
}

Your entities should be annotated with proper field annotations:

@FilterDef(name = MY_FILTER, parameters = @ParamDef(name = MY_PARAM, type = "string"))
@Filters(
        @Filter(name = MY_FILTER, condition = MyEntity.FILTERED_FIELD + " = :" + MY_PARAM)
)

And I think that's it (if I didn't forget anything).

Unfortunately, this is not customizable by a repository. But usually, repositories correspond to some particular entities, so you have to add filters only to those entities, which you are interested in. I think it's fine to always set a filter in the HibernateFilterConfig. If entities don't use it, it will be simply ignored

If you really want to customize based on a repository, I believe you have to go AOP way, with load time weaving and java agent.

Comment From: jofatmofn

Hi @bugy, Thanks for the quick response.

My objective is to achieve Multi-tenancy. I think I am badly missing a key point - Are these solutions suitable for shared database, shared schema scenario? I don't intend to hijack this thread, if it is not appropriate. Could you please confirm this point alone. Thanks.

  • In case of AOP, the advice on repositories (enabling the filter and setting tenant id) runs before the advice on RestController method which identifies the tenant id.
  • In case of post-processing transactional EntityManager instances, transactionManager runs at the start of the Spring Boot. Again the tenant identification happens only at individual RestController methods.

Comment From: bugy

Hi @jofatmofn, I'm also using it for multitenancy, with a shared DB and schema. For AOP: it depends on where you add the advice. If you add them on repository methods (see the first example in this ticket), then it should work, since repository methods are invoked within you rest controller. For transaction customizer: for me, it's invoked just before the query goes to a database, i.e. definitely after RestController code. But it can depend on spring.jpa.open-in-view = false config (which I have, and which is not default one). I believe, that with open-in-view = true, transaction is open before RestController is invoked. But in this case, you can use the first example from this thread, I guess.

Comment From: jofatmofn

Thanks @bugy, I have raised a SO question for my issue at https://stackoverflow.com/questions/62387223/why-createentitymanager-is-called-before-my-rest-controller-method.

Comment From: M-Devloo

Hi @jofatmofn, I have implemented and documented this in a standalone Spring Boot application except the TenantId is applied through a received JWT token. But the same concept can be applied with your tenantId. https://github.com/M-Devloo/Spring-boot-auth0-discriminator-multitenancy

This project is built to tackle the discriminator based multi tenancy problem.

Comment From: saloniudani

Hi @M-Devloo @bugy I am also trying to setup multitenancy, with a shared DB and schema. I am using AOP on repositories. It works fine for REST APIs. But I am struggling with below 2 scenarios: 0)When in a controller a CompletableFuture.runAsync method is called to do the processing. The tenant filters don't get enabled within that Async method. 1)When a method is called with Spring Scheduler, the tenant filter doesn't get enabled.

I believe it has something to do with transactional boundary but I am not able to figure out what is the issue exactly.

Comment From: bugy

Hi @saloniudani, I guess it's more related to where do you store the tenant id. I assume, that you use something similar to the code in the first message here and rely on TenantUtil. I don't know its implementation, but I'm pretty sure, that it depends on ThreadLocals, which won't work with different threads. For Scheduler it should be more or less easy to fix: just set a tenant explicitly before a scheduled job runs, and clear the tenant once it's completed. For Async, I don't know any easy way to propagate it, unfortunately. I guess one of the options would be to inject custom async executor, which will propagate tenant's thread local to the internal tasks before execution.

Comment From: saloniudani

Thanks @bugy . I have posted my issue with some code snippet on stackoverflow. Kindly take a look https://stackoverflow.com/questions/77149468/hibernate-filter-not-getting-applied-within-async-process?noredirect=1#comment136009175_77149468

For scheduler I am doing the same, explicitly setting and clearing tenant context. And for CompletetableFuture async process also tenant ID is available since I use InheritableThreadLocal to store it. So my issue is not that tenant ID is not available . But issue is that hibernate tenant filter is not getting applied to the session although aspect is being called each time.

Comment From: bugy

Hi @saloniudani I see. Unfortunately, I cannot help you much here. I don't even remember why it doesn't work with spring.jpa.open-in-view = false. But I'm pretty sure it's related. Because any code, working with repositories without a REST call has a very similar behavior to spring.jpa.open-in-view = false.

Comment From: M-Devloo

Hi @saloniudani,

Hibernate 6 now supports a new @TenantId annotation in the entity class which works together with the CurrentTenantIdentifierResolver strategy.

You can enable this by using hibernate.search.backend.multi_tenancy.strategy = discriminator That is, If you can update to Spring boot 3 & Spring framework 6 ofcourse

I believe that this is the new way to implement Discriminator based multi tenancy (when you use hibernate as ORM) My example project is still using Hibernate 5 (and is in desperate need of updating to Hibernate 6).

From Hibernate docs:

With the discriminator strategy, all documents from all tenants are stored in the same index. When indexing, a discriminator field holding the tenant ID is populated transparently for each document. When searching, a filter targeting the tenant ID field is added transparently to the search query to only return search hits for the current tenant.

Due to the filter is transparently added by Hibernate itself, we no longer require a hibernate filter to be set manually through Spring AOP (and unwrap the session)

Could this perhaps be a solution for your problem as you mentioned that you have the tenantID available in both scenario's (async & spring scheduler)

Comment From: saloniudani

Hi @M-Devloo , unfortunately I cannot upgrade right away to spring 3 due to some other dependencies.

But even with spring 3 and hibernate 6, I have few questions -Does it work for async process running with CompletableFuture or background process running with Spring Scheduler? I assume it should as it is adding tenant ID while opening new session itself. -Does it append tenantId to native queries? -What if we have some use cases where we want to query same Entity with and without tenantId filter?

Comment From: jhoeller

Closing this ticket for its original purpose since our native Hibernate 5 support and the entire orm.hibernate5 package are being phased out in favor of general JPA usage with Hibernate 6.x, remaining available in Spring Framework 6.x for the time being but not getting any further investment. Only HibernateJpaDialect and HibernateJpaVendorAdapter remain for strategic Hibernate support beyond that.