0

I am using spring boot 3 with Jpa Specifications for build a dynamic query, Paging and Fetch Join together in order to avoid N+1 query problem. But pagination is applied at application level in memory and not a database level with query as it should be.

My User entity is

@Entity
@Table(name = "users")
public class User implements UserDetails, Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    
    @Column(name = "names")
    private String names;
    
    
    @ManyToMany(cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE
    })
    @JoinTable(name = "users_roles",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles = new HashSet<>();

...
}

My User repository is

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

My UserGateway uses UserRepository.findAll(Specification<T> spec, Pageable pageable)

@Repository
@RequiredArgsConstructor
public class UserGatewayImpl implements UserGateway {


    private final UserRepository userRepository;


    @Override
    public Page<User> findByParams(UserSearchCommand searchCriteria, Pageable pageable) {

        var userSpecification = buildCriteria(searchCriteria);

        return userRepository.findAll(userSpecification, pageable);
    }
}

My build criteria. In order to avoid "org.hibernate.query.SemanticException: query specified join fetching, but the owner of the fetched association was not present in the select list" exception because JPA executes 2 queries, I check resultType to know if it is the count query. Like in this answer Fetch join with paging solution

    private Specification<User> buildCriteria(UserSearchCommand queryCriteria) {

        return (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (Long.class != criteriaQuery.getResultType() && 
                    long.class != criteriaQuery.getResultType()) {
                root.fetch("roles", JoinType.LEFT);
            } else {
                root.join("roles", JoinType.LEFT);
            }


            if (nonNull(queryCriteria.getNames())) {
                predicates
                        .add(criteriaBuilder.and(
                                criteriaBuilder.equal(root.get("names"), queryCriteria.getNames())));
            }

            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }

That seems to work. The pagination "works". But when we take a look at the console and check the query generated by hibernate I see that the pagination is not being applied at the database level in query. Pagination is applied at application level in memory.

Generated query is

...
    from
        users u1_0 
    left join
        (users_roles r1_0 
    join
        roles r1_1 
            on r1_1.id=r1_0.role_id) 
                on u1_0.id=r1_0.user_id 
        where
            1=1 
        order by
            u1_0.id desc

I want to use JPA specifications, paging and FETCH JOIN together but paging is applied at application level in memory. Paging should be applied at database level in query.

Andres Grisales
  • 21
  • 1
  • 1
  • 3

1 Answers1

0

Hibernate can not really do any better in this case, because when a collection is fetch joined, it can't apply the SQL limit/offset clauses, because the SQL cardinality does not match the entity result cardinality.

You can try using Blaze-Persistence on top, which also comes with a Spring Data integration, that automatically transforms queries to a form which allows efficient pagination. Give it a try. After the initial setup, you only need to replace @EnableJpaRepositories with @EnableBlazeRepositories.

Christian Beikov
  • 15,141
  • 2
  • 32
  • 58