Search code examples
spring-bootspring-data-jpaquerydsl

Spring Data Web Support for QueryDsl works sporadically upon application startup; customizer not always invoked


My project uses Spring Boot 2.1.3, Java 8, QueryDsl 4.2.1. (It is a mature critical project, and upgrading the SB version to the latest 2.4.4 is not feasible at the moment.)

Recently, I have added Spring Data web support to simplify binding request query parameters to the QueryDsl search predicate used in my Spring Data JPA repository methods - with the @QuerydslPredicate annotation in web controller methods. That, with the ability to customize the bindings for specific properties or whole classes of properties, seemed to work great... Until we discovered that the searches would randomly stop working.

After some investigation, I noticed that sometimes, after the application starts, the customize method implementation on my Spring Data JPA repository does not get called by the framework prior to the invocation of the controller method that expects the populated predicate. The queries, if they had any search params that rely on customized bindings, would fail. But here's the crazy part. I would stop and restart the application (without changing anything!), and everything would start working, the same query would yield the expected results.

Notably, restarting in the debug mode (in IntelliJ IDEA) would seem to load everything correctly (most of the time but, I think, not always), and the code in the "customize" method would get executed. I would stop and restart the application multiple times without changing anything or rebuilding. Sometimes it would work, and sometimes it would not! The same query/request. The behavior is the same in its inconsistency in different environments. I have ensured that the QueryDSL APT dependency (for annotation processing and code generation for the Q types) is declared with scope provided and not packed into the JAR.

I see that the Spring QueryDsl library is always included and available on the classpath, which is supposed to ensure the support for the @QuerydslPredicate annotation to be enabled automatically - per documentation. It seems that - depending on something random at startup - the framework is not finding the customizer implementation for the required domain type (my entity class.)

Once it works, it works. But I can never be sure that it will be enabled once the application is stopped and restarted. Anyone have any idea about what could possibly cause this bizarre behavior? Versions mismatch, perhaps? A bug in Spring Data Web Support for QueryDsl? Should I, perhaps, try using a different, earlier version of spring querydsl to go with my Spring Boot v2.1.3?

In the meantime I have to resort to building the predicates by hand in the service tier, but it would be nice to solve this mystery.


Solution

  • Found the problem, in case someone runs into a similar issue...

    In my controller method, I originally omitted the bindings argument for the @QuerydslPredicate annotation letting the framework find the QuerydslBinderCustomizerimplementation. I assumed that the framework would look for and find my Spring Data JPA repository implementation:

      public interface MyEntityRepository
        extends JpaRepository<MyEntity, Integer>, QuerydslPredicateExecutor<MyEntity>, QuerydslBinderCustomizer<QMyEntity> {
    
        @Override
        default void customize(QuerydslBindings bindings, QMyEntity myEntity) {
            ... // my bindings customizations
    }
    

    Unfortunately, there was another - legacy - repository interface for the same entity class. That second repository, obviously, had a different name and did not implement QuerydslPredicateExecutor, which I wrongly assumed would make the framework completely ignore it and always find the correct implementation - based on the aforementioned interface. Apparently, that is not how Spring does it, and it may be a bit of a flaw in the framework. The following is a snippet from Spring's QuerydslBindingsFactory:

        .orElseGet(() -> repositories.flatMap(it -> it.getRepositoryFor(domainType))//
                        .map(it -> it instanceof QuerydslBinderCustomizer ? (QuerydslBinderCustomizer<EntityPath<?>>) it : null)//
                        .orElse(NoOpCustomizer.INSTANCE));
    

    So, the framework simply grabs the first repo instance for the given domain type, and if it does not implement the QuerydslBinderCustomizer, throws the towel, and gives up looking any further. It is, of course, best to have only one repo class per entity type. However, in our case, the legacy class is still being used elsewhere and may not be removed just yet. Also, there are reasons why I'd want to keep this and the old implementations separate in different classes. So, in my case, depending on which instance was found first, things would either work or not. By explicitly specifying the customizer repository class in the bindings arg of the controller method's annotation, I fixed the issue:

        @GetMapping("/xyz")
        public List<MyEntity> findMyEntitiesByPredicate(
            @QuerydslPredicate(root = MyEntity.class, bindings = MyEntityRepository.class) Predicate predicate, ...) {...}