Skip to content
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

Cannot use Specification<T> interface to create subquery predicates #2689

Open
pterjenvri opened this issue Nov 3, 2022 · 3 comments
Open
Labels
status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged

Comments

@pterjenvri
Copy link

I implemented numerous own Specification classes that implement the interface called Specification<T> in the org.springframework.data.jpa.domain package. This Specification<T> interface has a Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder); interface function which has to be implemented along with implementing the interface. This toPredicate function has 3 parameters, the Root<T>, the CriteriaQuery<?>, and the Criteriabuilder. As long as I want to create the Predicate object for a CriteriaQuery object it works fine. However I have cases, when I would like to use my own Specification classes to create Predicate objects for subqueries. As I have found out this is currently impossible because the toPredicate function requires CriteriaQuery object as its second parameter instead of requiring the common parent of CriteriaQuery<T> and Subquery<T> that could be AbstractQuery<T>. It would be nice if Specification<T> interface could be used to create Predicate objects for subqueries too. Right now I have to duplicate the code written in my own Specification classes if I would like to use it for subquery predicates.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Nov 3, 2022
@mp911de
Copy link
Member

mp911de commented Nov 7, 2022

Can you provide us with a bit more context along with a bit of code how you intend to use Specification so we get a better understanding?

@mp911de mp911de added the status: waiting-for-feedback We need additional information before we can continue label Nov 7, 2022
@pterjenvri
Copy link
Author

Yes. So I have multiple implementations of the Specification<T> interface. To make it easier let's say I have 2 classes that implement the interface.

Specification class 1:

public class SpecificationClass1<T> implements Specification<T> {
    // here are some private fields

    // here are multiple constructors which initialize the private fields
    
    // and here is the implementation of the interface method called toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder)
    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        Predicate predicate = criteriabuilder.equal(...
        
        // so here I create the Predicate object which I will return
        // I have multiple if statements here, because I would like different predicates based on the values of the private fields
    
        return predicate;
    }

}

Specification class 2:

public class SpecificationClass2<T extends SomeClass> implements Specification<T> {
    // this class almost looks like the previous one, so
    // here are some private fields

    // here is a constructor
    
    // and here is the implementation of the interface method called toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder)
    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        Predicate predicate = criteriabuilder.equal(...
        
        // so here I create the Predicate object which I will return
        // I have multiple if statements here, because I would like different predicates based on the values of the private fields
    
        return predicate;
    }

}

So these are my 2 Specification classes which implement the Specification<T> interface.

Now, in the Provider class I have a query built with Criteria API:

    public Page<ViewModel> findAll(/* some params */)
    CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    CriteriaQuery<SomeClass> query = criteriaBuilder.createQuery(SomeClass.class);
    Root<SomeEntity> root = query.from(SomeEntity.class);
    
    // here I have a subquery for aggregating something, and in the select clause I select the result of the subQuery, and then even sorting is possible based on the result of this aggregation subquery
    
    query.multiselect(/* fields to select */);
    
    query.groupBy(/* field */);
    
    // and here comes the creation of the Predicate object
    
    // Specification1
    SpecificationClass1<SomeEntity> specificationClass1 = new SpecificationClass1<>(/* variables to pass to the constructor */);
    
    // Specification2
    SpecificationClass2<SomeEntity> specificationClass2 = new SpecificationClass2<>(/* variables to pass to the constructor */);
    
    // then I use the and method of the Specification<T> interface to combine the two specifications
    Specification<SomeEntity> specifications = specificationClass1.and(specificationClass2);
    
    // then I call the toPredicate function on the combined specification
    Predicate predicate = specifications.toPredicate(root, query, criteriaBuilder);
    
    // and I pass this predicate object to the where clause of the query
    query.where(predicate)
    
    // applying sorting
    
    // execute the query and calling helper functions to make the result of the query pageable

Until this time, everything works perfectly. But this query is complex enough to use 2 different specification classes and needs to be paginated and so on. To make this pageable I need to know how many rows fit the criteria.

So I need a query, which basically does a COUNT(*) and to do that I need a subQuery, because in one of the Specification classes I have having clause (and the query is grouped) and without using a subQuery I cannot get proper result.

An other issue here, is that JPQL and also Criteria API does not support counting the number of rows returned by a subQuery, but this can be easily handled by using IN clause.

So I do the following:

    private long countRows(Specification<SomeEntity> specification) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> query = criteriaBuilder.createQuery(Long.class);
        Root<SomeEntity> root = query.from(SomeEntity.class);
        
        // create the subQuery
        SubQuery<SomeEntity> subQuery = query.subquery(SomeEntity.class);
        Root<SomeEntity> subRoot = subQuery.from(SomeEntity.class);
        subQuery.select(subRoot);
        
        // and create exactly the same predicate like in the findAll query but now for a SubQuery
        // I need the number of rows when calling the helper method at the end of findAll function so this function is called from findAll
        // so I can pass the Specification<SomeEntity> specification as parameter
        // at this point this specification is the combination of two specifications
        // it would be very nice if it could be used here
        // so what I would like to do is
        
        // calling the toPredicate function on the received specification as parameter passing SubQuery object
        Predicate predicate = specification.toPredicate(subRoot, subQuery, criteriaBuilder);
        
        // ...
}

And at this point comes the problem. The interface method in the Specification<T> interface looks like: Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder); so it requires CriteriaQuery object as its second parameter, so I cannot pass a SubQuery here. That's why Specification interface cannot be used to create predicates for subqueries. The only way to handle this is duplicate the code written in the Specification classes which means I need to copy the code from the Specification classes to this count query function.

If the Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder); function of the Specification<T> interface would be more flexible, code duplication would not be needed.

This is very long, this is my whole problem. But to make it short, the problem is that if I create a Specification class which implements the Specification<T> interface I can only use it for CriteriaQueries. I cannot use it for SubQueries, so at this point code duplication cannot be avoided.

@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 Nov 8, 2022
@Sipleman
Copy link

For future references, this could be approached by using a "dummy" query to create Predicate:

CriteriaQuery<Object> dummyQuery = cb.createQuery();
Predicate predicate = specification.toPredicate(subRoot, dummyQuery, criteriaBuilder);
if (predicate != null) {
    subQuery.where(predicate);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged
Projects
None yet
Development

No branches or pull requests

4 participants