JPA Specification instance change after use?

33 Views Asked by At

i am using a heavily modified version of the code in Carl Mapada's Advanced Search and Filtering article and it was actually working out very well for me until i had to add pageable. and like many others using Speccification with Pageable i had a problem with the count query. so i wrote a CustomRepository and overrode the readPage so it doesn't count. all good, paging is working but except i believe i need a separate count query if i want to show something like "1 to 100 of 1482 results." (the current total is just the total from the paging.)

so i went down the path of trying to figure out a way to get a total count with my on the fly queries built with the SpecificationBuilder implementation i have. i have not been successful for the last day or so but going to the extremes to try to solve my problem i noticed something interesting...

if in my service i call a repository method with the same Specification instance twice then i get an error (even without pageable)...

MyEntitySpecBuilder builder = new MyEntitySpecBuilder(filter, specs);
Specification<MyEntity> spec = builder.build();
List<MyEntity> entities1 = myEntityRepository.findAll(spec);
List<MyEntity> entities2 = myEntityRepository.findAll(spec);

JpaSystemException: unable to location property named xyz on blah.blah.EntityClassAttribute occurs on the second call to findAll(...)

yet, if i do this, there is no error...

MyEntitySpecBuilder builder = new MyEntitySpecBuilder(filter, specs);
Specification<MyEntity> spec = builder.build();
List<MyEntity> entities1 = myEntityRepository.findAll(spec)
List<MyEntity> entities2 = myEntityRepository.findAll(builder.build());

the only reason i stumbled upon that is that i was trying to get a custom count query going but using the same Specification instance as i use for getting the page info but it wasn't working so i decided to just start with having the service do two different queries with the same Specification instance (though inefficient, it was a start) and i stumbled upon this.

so here are my questions...

  1. if i have a situation where multiple Specification parts are used to create a query (as in the Advanced Search and Filtering example) and i want a count, is there a suggested way to do it, hopefully in a CustomRepository (and not in a Service)? i was thinking of having a custom repository method where i pass the specification builder and get two separate instances of the specification in the respotory method, one for getting the count and one for getting the results.
  2. but my main question now is... is the Specification instance being modified in some way after it is used the first time or why are two separate instances working one after another but not the same instance?
1

There are 1 best solutions below

0
user2052618 On

here is the solution i went with for my second question. this is something put together as opposed to finding it in a search so i'm not sure if there is a better way but it works. i forgot to note that i am using spring data jpa 2.2.7 and so maybe the upgraded spring version works differently (we are slow to upgrade libs a lot.)

// CustomRepositoryImpl.java (public methods here are in CustomRepository.java)
@NoRepositoryBean
public class CustomRepositoryImpl<T> extends SimpleJpaRepository<T, Long>
    implements CustomRepository<T, Long {

    private EntityManager em;

    public CustomRepositoryImpl(JpaMetamodelEntityInformation<T, Long> entityInfo, EntityManager em) {
        super(entityInfo, em);
        this.em = em;
    }

    public CustomImpl(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        this.em = em;
    }


    public Page<T> findAll(SpecificationBuilder<T> builder, Pageable pageable, EntityGraphType entityGraphType, String entityGraphName) {

        // Note: in my case, if spec was declared here and used for the count & the query
        // there would be an error but no error when done this way...

        long count = this.count(builder.build());
        //OR for more control...
        // TypedQuery<Long> countQuery = getCountQuery(builder.build(), getDomainClass());
        // countQuery.setHint(EntityGraphType.FETCH.getKey(), countEntityGraphNameCouldBePassedInToMethodIfDesired);
        // long count = countQuery.getSingleResult();

        // need a diff instance of the Specification for some reason
        Specification<T> spec = builder.build();
        TypedQuery<T> query = getQuery(spec, Sort.unsorted());
        query.setHint(entityGraphType.getKey(), em.getEntityGraph(entityGraph));
        return readPage(query, pageable, (int) count);
    }

    public Page<T> findAll(SpecificationBuilder<T> builder, Pageable pageable, String entityGraphName) {  
        return this.findAll(builder, pageable, EnitityGraphType.FETCH, entityGraphName);
    }

    private <S extends T> Page<S> readPage(TypedQuery<S> query, Pageable pageable, int count) {
        if(pageable.isPaged()) {
            query.setFirstResult((int) pageable.getOffset());
            query.setMaxResults(pageable.getPageSize());
         }

         return PageableExecutionUtils.getPage(query.getResultList(), pageable, () -> count);
    }
}


// SpecificationBuilder.java
public interface SpecificationBuilder<T> {
    Specification<T> build();
}



// MyEntitySpecificationBuilder.java
// search for Advanced Search and Filtering on the internet for more details
public class MyEntitySpecificationBuilder implements SpecificationBuilder<MyEntity> {

    public Specification<MyEntity> build() {
        ...
    }
}