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
...
}
-
The
session()
method is a standard resource accessor method returning the underlying HibernateStatelessSession
. -
The
entityClass()
method is one that each repository which extendsJpaRepository
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
|
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:
|