18

I used JPA CriteriaQuery to build my dynamic Query and pass in a Spring Data Pageable object:

sort=name,desc

At the backend I have a method in my Repository to support dynamic query:

public Page<User> findByCriteria(String username, Pageable page) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> iRoot = cq.from(User.class);
    List<Predicate> predicates = new ArrayList<Predicate>();

    if (StringUtils.isNotEmpty(username)) {
        predicates.add(cb.like(cb.lower(iRoot.<String>get("username")), "%" + username.toLowerCase() + "%"));
    }

    Predicate[] predArray = new Predicate[predicates.size()];
    predicates.toArray(predArray);

    cq.where(predArray);

    TypedQuery<User> query = em.createQuery(cq);

    int totalRows = query.getResultList().size();

    query.setFirstResult(page.getPageNumber() * page.getPageSize());
    query.setMaxResults(page.getPageSize());

    Page<User> result = new PageImpl<User>(query.getResultList(), page, totalRows);

    return result;
}

Note: I put only one param for demo purpose.

However the returned data is unsorted therefore I would like to ask is that any way to implement Pageable in CriteriaQuery.

Neil Stockton
  • 11,383
  • 3
  • 34
  • 29
Javatar
  • 623
  • 3
  • 8
  • 20
  • 6
    Getting the full result set only to fetch its size seems wasteful - transfer all that data and just discard it. Instead, what method calls should be made in JPA to tell the query to fetch the count only? – chrisinmtown Nov 26 '18 at 14:00
  • #count total rows `CriteriaQuery cq = cb.createQuery(Long.class); cq.select(cb.count(cq.from(User.class))); Long totalRows = entityManager.createQuery(cq).getSingleResult();` – Shashikant Sharma Jul 12 '23 at 09:39

5 Answers5

14

You can add sorting using QueryUtils from spring: query.orderBy(QueryUtils.toOrders(pageable.getSort(), root, builder));

7

In your criteria query i see no sort information I'd write in this way:

     CriteriaBuilder cb = em.getCriteriaBuilder();
     CriteriaQuery<User> cq = cb.createQuery(User.class);
     Root<User> iRoot = cq.from(User.class);
     List<Predicate> predicates = new ArrayList<Predicate>();

     if (StringUtils.isNotEmpty(username)) {
         predicates.add(cb.like(cb.lower(iRoot.<String>get("username")), "%" + username.toLowerCase() + "%"));
     }

     Predicate[] predArray = new Predicate[predicates.size()];
     predicates.toArray(predArray);

     cq.where(predArray);

     List<Order> orders = new ArrayList<Order>(2);
     orders.add(cb.asc(iRoot.get("name")));
     orders.add(cb.asc(iRoot.get("desc")));

     cq.orderBy(orders);
     TypedQuery<User> query = em.createQuery(cq);
     Page<User> result = new PageImpl<User>(query.getResultList(), page, totalRows);

     return result;

I didn't test it but it should work

Angelo

Abdullah Khan
  • 12,010
  • 6
  • 65
  • 78
Angelo Immediata
  • 6,635
  • 4
  • 33
  • 65
  • But how to make it dynamic? It is hardcoded in your codes and I suppose that the Pageable will do sorting automatically? – Javatar Jan 17 '17 at 08:55
  • Pageable will not do the sorting automatically; it must be the query in doing it. If you want it dinamically you should pass the Order (or an equivalent DTO) to the method findByCriteria params – Angelo Immediata Jan 17 '17 at 09:05
  • Thanks, I have found a solution by manually do the sort by using Pageable,getSort(). – Javatar Jan 23 '17 at 01:45
  • @Javatar I'm curious on how you succeded to use the Pageable.getSort() to do something dynamic. I tried `cq.where( cb.and(predicates) ).orderBy( pageable.getSort().stream().collect(Collectors.toList()) );` where cq is my CriteriaQuery. It won't work as Orders list from Pageable is a springframework object, and CriteriaQuery API is waiting an Hibernate object :( (EDIT : ok I read again your comment and you said 'manually' nvm) – Alex Nov 22 '18 at 11:03
  • 1
    That two-value string "name,desc" in Spring-speak means "sort by name in descending order." The code above attempts to sort on a column named "desc" which I expect will break. – chrisinmtown Nov 26 '18 at 17:02
  • @chrisinmtown The value "name,desc" is actually asking Spring to sort the resultset by using the name AND the desc. – Javatar Nov 28 '18 at 03:00
  • @javatar thanks for the reply but please check your facts, for example https://stackoverflow.com/questions/33018127/spring-data-rest-sort-by-multiple-properties – chrisinmtown Nov 28 '18 at 13:13
6

Both Op question and Angelo's answer won't work in a real-life scenario. If that table/query returns thousand of records this approach will perform slowly and you could also could create memory issues.

Why?

  • int totalRows = query.getResultList().size();
  • new PageImpl(query.getResultList(), page, totalRows);

both resultsets will run same query twice and mantain a full table/query data in those colections, just to be counted or sliced, which makes no sense when you need to show few records in your front-end.

My suggestion for Spring Data is to extend JpaSpecificationExecutor interface in your repository

@Repository
public interface UserRepository extends JpaRepository<User, Integer>,
        JpaSpecificationExecutor {
}

This interface will enable your repository to use findAll(Specification, Pageable) method.

After that, all is quite easy. You only need to create a Java closure to inject predicates into a dynamic jpa query.

public Page<User> listUser( String name, int pageNumber, int pageSize ) {

Sort sort = Sort.by("id").ascending();
Pageable pageable = PageRequest.of(pageNumber, pageSize, sort);


return this.reservationRepository.findAll((root, query, builder) -> {
            List<Predicate> predicates = new ArrayList<>();
            if name!= null ) {
                predicates.add(builder.equal(root.get("name"), name));
            }
            // More simple/complex conditions can be added to 
            // predicates collection if needed.

            return builder.and(predicates.toArray(new Predicate[]{}));

        }, pageable);

}

As you can see here, this solution supports Sorting/Filtering and database side pagination.

0

A complete example that uses JpaSpecificationExecutor and encapsulate it in a class:

First define the repository as an extension of JpaRepository and JpaSpecificationExecutor for SomeEntity:

interface SomeEntityRepositoryPaged extends JpaRepository<SomeEntity, Integer>, JpaSpecificationExecutor<SomeEntity> {
 Page<SomeEntity> findAll(Specification<SomeEntity> spec, Pageable pageable);

}

FindByCriteria class:

class FindByCriteria{
     FindByCriteria() {
            ApplicationContext appCtx = ApplicationContextUtils.getApplicationContext();
            repository = appCtx.getBean(SomeEntityRepositoryPaged.class);
        }
    
        private SomeEntityRepositoryPaged repository;
        
        public Page<SomeEntity> searchBy(Client client, SearchParams params,Pageable page){
            return repository.findAll(getCriteria(params), page);
        }
        private Specification<SomeEntity> getCriteria(SearchParams params){
              return (paymentRoot, query, cb)-> 
              { 
                    List<Predicate> predicates = new ArrayList<>(5);
            
                    filterByParams(params, cb, paymentRoot, predicates);

                    query.where(predicates.toArray(new Predicate[] {}));

                    //IMPORTANT: the order specify should be deterministic, meaning rows can never change order in 2 subsequent calls.
                    
                    List<Order> orders = new ArrayList<>(1);
                    orders.add(cb.asc(paymentRoot.get("date")));
                    
                    query.orderBy(orders);
                    
                    return  null;
              };
            }

        private void filterByParams(SearchParams params, CriteriaBuilder cb, Root<SomeEntity> payment,
                List<Predicate> predicates) {
        
            if (params.getInitialDate() != null)
                predicates.add(cb.greaterThan(payment.get("paymentDate"), params.getInitialDate()));

            if (params.getFinalDate() != null)
                predicates.add(cb.lessThanOrEqualTo(payment.get("paymentDate"), params.getFinalDate()));

            if (params.getState() != null)
                predicates.add(cb.equal(payment.get("state"), params.getState()));
            //... add here any criteria needed bases on params object
        }
    }

}

To invoke simple call:

@Test pagedQueryTest
{
     var page = PageRequest.of(2, 10);
     var params = new SearchParams (...);
     var list = new FindByCriteria().searchBy(params, page);
     assert...
     //The generated native query should be optimized.
     //For mssql the native query should end with "offset ? rows fetch first ? rows only" 
     //and that means the store engine will only fetch the rows after first parameter (=page*size), and to the max of the page size.
}
MiguelSlv
  • 14,067
  • 15
  • 102
  • 169
-1
 CriteriaBuilder cb = em.getCriteriaBuilder();
 CriteriaQuery<User> cq = cb.createQuery(User.class);
 Root<User> iRoot = cq.from(User.class);
 List<Predicate> predicates = new ArrayList<Predicate>();

 if (StringUtils.isNotEmpty(username)) {
     predicates.add(cb.like(cb.lower(iRoot.<String>get("username")), "%" + 
     username.toLowerCase() + "%"));
 }


 Predicate[] predArray = new Predicate[predicates.size()];
 predicates.toArray(predArray);

 cq.where(predArray);

 cq.orderBy(cb.desc(user.get("name")));

 TypedQuery<User> query = em.createQuery(cq);
 Page<User> result = new PageImpl<User>(query.getResultList(), page, totalRows);

 return result;