user11567166
user11567166

Reputation: 1

Hibernate filter not getting applied within Async process

I have a hibernate filter defined on my entities which I am injecting using aspect for all repositories which implements my TenantableRepository.

The issue is that the filter is not getting injected in the repository calls executed within CompletetableFuture method. But filter is getting injected properly outside it. I understand that the threads are different but aspect is being called both the times though.

I want the filter to get enabled from all flows. API or async processes.

Here is the Aspect I have defined to work on all TenantableRepositories' methods starting with find.

@Aspect
@Component
@Slf4j
public class TenantFilterAspect {

    @PersistenceContext
    private EntityManager entityManager;

    @Before("execution(* com.demo.repository.TenantableRepository+.find*(..))")
    public void beforeFindOfTenantableRepository() {
        log.info("Called aspect");
        entityManager
                .unwrap(Session.class)
                .enableFilter(Tenantable.TENANT_FILTER_NAME)
                .setParameter(Tenantable.TENANT_COLUMN, TenantContext.getTenantId());
    }
}

I have a controller to test the flow. Here I am making 2 repository calls. One in main thread and one under async CompletetableFuture method.

import org.springframework.beans.factory.annotation.Autowired;

@RestController
@Slf4j
@RequestMapping(value = "/v1/api/test", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public class TestController {

    @Autowired
    MyEntityRepository myEntityRepository;

    @RequestMapping(value = "/aysnc", method = RequestMethod.GET, consumes = MediaType.ALL_VALUE)
    public @ResponseBody
    ResponseEntity<APIResponse> testAsync(HttpServletRequest httpServletRequest) throws InterruptedException {
        Optional<MyEntity> entity = myEntityRepository.findByFirstName("Firstname");
        if(!entity.isEmpty()){
            log.info("1. Main: found entity:{}",entity.get());
        }
        CompletableFuture.runAsync(this::callAsyncMethod);
        return ResponseEntity.status(HttpStatus.OK)
                .body("Ok");
    }

    public void callAsyncMethod() {
        Optional<MyEntity> entity = myEntityRepository.findByFirstName("Firstname");
        if(!entity.isEmpty()){
            log.info("2. Async: found entity:{}",entity.get());
        }
    }

}

The issue is that the filter is not getting injected in the repo call under async CompletetableFuture method callAsyncMethod(). But filter is getting injected properly in first repo call before async method. I understand that the threads are different but aspect is being called both the times. I get the log printed but the filter is still not enabled.

What am I doing wrong here?

Upvotes: 0

Views: 658

Answers (1)

M. Deinum
M. Deinum

Reputation: 125202

Spring Boot by default enables the Open Entitymanager In View pattern. Which means for the whole duration of the HTTP Request there is 1 single EntityManager available. This EntityManager is bound to the request handling thread using a ThreadLocal.

Now when executing the first findByFirstName it will operate on this shared EntityManager.

Your second call, which is done on a different thread doesn't see this shared EntityManager. As there is also no transactional context, which would create another thread bound EntityManager, the Aspect will get a fresh EntityManager. However the EntityManager used by the Aspect is discarded right after that because, as mentioned, no transactional context. Next it reaches the actual repository method, which doesn't see an active EntityManager either and gets a fresh one as well (without any filters applied because those were applied to another EntityManager instance).

When you set the spring.jpa.open-in-view property to false I the same will happen with the first call to findByFirstName as that disables the shared EntityManager.

The problem with this code is that this should be called from inside an @Transactional service method. The @Transactional will lead to the creation of a shared (thread-bound) EntityManager for the duration of the transaction.

@Service
public MyEntityService {

  private final MyEntityRepository repo;

  public MyEntityService(MyEntityRepository repo) {
    this.repo = repo;
  }
  

  @Transactional
  public Optional<MyEntity> findByFirstname(String firstname) {
    return repo.findByFirstName(firstname);
  }
}

Now if you would inject this service into your controller (as you should have done anyway), it will work due to the shared EntityManager for each call due to the @Transactional.

Upvotes: 0

Related Questions