In this article I demonstrate how to enable JPA to produce SQL queries dynamically and execute them, for example:

select book_type, count(book.id) 
from book
where book.publish_date<=PARSEDATETIME('01-01-1900','dd-MM-yyy')
group by book.book_type

It is easy to use named queries and predefined method from JPA to query a database. However, this may not be enough. Some situations require generation of SQL queries during runtime, e.g. filtering or aggregation based on user-specified fields.

We start with a basic entity:

@Entity
@Table(name = "Book")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String author;

    @Column
    private String title;

    @Column
    private Date publishDate;
    
    @Enumerated(EnumType.STRING)
    private BookType bookType;
}

The following repository definition allows us to access and to query the db:

@Repository
public interface BookRepo extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book>, GroupByRepository{
}

Extension of JpaSpecificationExecutor interface adds a support of JPA Specifications. This allows us to dynamically generate where-statements in the SQL query.

The interface GroupByRepository adds the aggregation functionality. Here is the definition:

public interface GroupByRepository {
    Map<Object, Long> whereGroupBy(SingularAttribute singularAttribute, Specification where);
}

The only method of the interface takes an attribute of the entity as a group-by argument, and the mentioned JPA specification. Here is an implementation of the interface, inspired by a post from stackoverflow:

public class GroupByRepositoryImpl implements GroupByRepository {
    @Autowired
    private EntityManager entityManager;
    @Override
    public Map<Object, Long> whereGroupBy(SingularAttribute singularAttribute, Specification where) {
        final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        final CriteriaQuery<Tuple> query = criteriaBuilder.createQuery(Tuple.class);
        final Root<Book> root = query.from(Book.class);
        final Path<String> expression = root.get(singularAttribute);
        query.multiselect(expression, criteriaBuilder.count(root));
        query.select(criteriaBuilder.tuple(expression, criteriaBuilder.count(root)));
        query.where(where.toPredicate(root, query, criteriaBuilder));
        query.groupBy(expression);
        final List<Tuple> resultList = entityManager.createQuery(query).getResultList();
        return resultList.stream()
                .collect(toMap(
                        t -> t.get(0, singularAttribute.getJavaType()),
                        t -> t.get(1, Long.class))
                );
    }
}

The following JUnit test contains an example of how to use the method:

@RunWith(SpringRunner.class)
@SpringBootTest
public class GroupByRepositoryTest {
    @Autowired
    BookRepo bookRepo;

    @Test
    public void groupByTest() {
        final Date Century20th = new Date(-2199999999999l);
        System.out.println(Century20th.toString());
        Specification<Object> where = Specification.where(
                (root, query, cb) -> cb.lessThanOrEqualTo(root.<Date>get("publishDate"), Century20th)
        );
        Map<Object, Long> result = bookRepo.whereGroupBy(Book_.bookType, where);
        System.out.println(result);
        Assert.assertEquals(2, result.entrySet().size());
    }
}

I showed you how you can use JPA Specification API to generate type-safe SQL queries dynamically instead of using query strings.

The sources are available here. Hopefully, you find this post useful.