Hibernate Data Repositories is an implementation of Jakarta Data based on Hibernate ORM. For more information, see Hibernate Data Repositories on hibernate.org.

Several people have recently asked me how to do "specifications" in Hibernate or in Jakarta Data. I believe this idea comes from Spring Data, where a repository method may be passed a function that applies programmatic restrictions to the query result via the JPA Criteria API.

I’m not a massive fan of this approach, though I do see how it somewhat reduces the famous verbosity of the Criteria API, at least in simple cases. We have something much more exciting cooking in Jakarta Data 1.1, but you’ll have to wait a bit for that.

There are no built-in specifications in Jakarta Data. But fortunately, it’s really easy to implement this feature in your own program. Actually, this is a rather instructive exercise, that illustrates some of the flexibility of Hibernate Data Repositories.

First, we’re going to define a generic repository supertype. Spring calls this interface JpaSpecificationExecutor, but we’ll just call it JpaRepository.

public interface JpaRepository<T> {

    StatelessSession session();  // implemented by Jakarta Data

    Class<T> entityClass();  // implemented explicitly by subtypes

    ...

}
  1. The session() method is a standard resource accessor method returning the underlying Hibernate StatelessSession.

  2. The entityClass() method is one that each repository which extends JpaRepository is going to have to provide—​it just returns the entity class, Book.class or whatever.

In case (2) really bothers you, we can do something fancy with reflection.

The Java Reflection API makes working with generic types absolutely miserable, but the following ugly and fragile implementation of entityClass() works well enough for our immediate purposes:

@SuppressWarnings("unchecked")
default Class<T> entityClass() {
    var thisInterface = (ParameterizedType)
            getClass().getInterfaces()[0].getGenericInterfaces()[0];
    return (Class<T>) thisInterface.getActualTypeArguments()[0];
}

Now we need a find() method which accepts a "specification". We could define our own single-method Specification interface for this, but Java already has an interface that we can reuse: the unlovely BiFunction.

Let’s add the following method to JpaRepository.

default List<T> find(BiFunction<CriteriaBuilder, Root<T>, Predicate> specification) {
    var builder = session().getCriteriaBuilder();
    var query = builder.createQuery(entityClass());
    query.where(specification.apply(builder, query.from(entityClass())));
    return session().createQuery(query).getResultList();
}

Similarly, we can define a count() method:

default long count(BiFunction<CriteriaBuilder, Root<T>, Predicate> specification) {
    var builder = session().getCriteriaBuilder();
    var query = builder.createQuery(Long.class);
    query.where(specification.apply(builder, query.from(entityClass())))
            .select(builder.count());
    return session().createQuery(query).getSingleResult();
}

And we’re already done: that’s less than twenty lines of code.

Now let’s define a Jakarta Data repository which extends our interface:

@Repository
public interface Library extends JpaRepository<Book> {
    @Override
    default Class<Book> entityClass() {
        return Book.class;
    }
}

A client of the Library repository may call count() and find() as follows:

long count = library.count((builder, book) ->
        builder.like(book.get(Book_.title), "%" + title + "%"));
var books = library.find((builder, book) ->
        builder.and(builder.like(book.get(Book_.title), "%" + title + "%"),
                builder.lower(book.join(Book_.authors).get(Author_.name))
                        .equalTo(authorName.toLowerCase())));

As usual, I ask you to take note of the fact that everything here is completely typesafe, since we’re making use of the JPA static metamodel.

Of course, you can go ahead and define additional generic methods which work with such "specifications". As an exercise, I’ll leave you with the task of adding an overload of find() which lets the caller specify an ordering.

If you need to compose specifications, this function does the trick:

static <T> BiFunction<CriteriaBuilder, Root<T>, Predicate>
compose(BiFunction<CriteriaBuilder, Root<T>, Predicate> first,
        BiFunction<CriteriaBuilder, Root<T>, Predicate> second) {
    return (builder, root) -> builder.and(first.apply(builder, root), second.apply(builder, root));
}

Back to top