The static metamodel generator is a standard part of JPA.
We’ve actually already seen its handiwork in the code examples earlier: it’s the author of the class Book_
, which contains the static metamodel of the entity class Book
.
Hibernate Processor, the annotation processor formerly known as the Metamodel Generator, began its life as a code generator for what JPA calls a static metamodel. That is, it produces a typed model of the persistent classes in our program, giving us a type safe way to refer to their attributes in Java code. In particular, it lets us specify entity graphs and criteria queries in a completely type-safe way.
The history behind this thing is quite interesting.
Back when Java’s annotation processing API was brand spankin' new, the static metamodel for JPA was proposed by Gavin King for inclusion in JPA 2.0, as a way to achieve type safety in the nascent criteria query API.
It’s fair to say that, back in 2010, this API was not a runaway success.
Tools did not, at the time, feature robust support for annotation processors.
And all the explicit generic types made user code quite verbose and difficult to read.
(The need for an explicit reference to a CriteriaBuilder
instance also contributed verbosity to the criteria API.)
For years, Gavin counted this as one of his more embarrassing missteps.
But time has been kind to the static metamodel.
By now, all Java compilers, build tools, and IDEs have robust support for annotation processing, and Java’s local type inference (the var
keyword) eliminates the verbose generic types.
JPA’s CriteriaBuilder
and EntityGraph
APIs are still not quite perfect, but the imperfections aren’t related to static type safety or annotation processing.
The static metamodel itself is undeniably useful and elegant.
And it turns out that there was quite a lot of unlocked potential there. Since Hibernate 6.3 the Processor has started taking on a much bigger role. Today, it even contains a complete implementation of the Jakarta Data specification.
Now, you still don’t have to use the Hibernate Processor with Hibernate—the APIs we just mentioned still also accept plain strings—but we find that it works well with Gradle and integrates smoothly with our IDE, and the advantage in type-safety is compelling.
Tip
|
We’ve already seen how to set up the annotation processor in the Gradle build we saw earlier. For more details on how to integrate the Hibernate Processor, check out the {doc-user-guide-url}#tooling-modelgen[Static Metamodel Generator] section in the User Guide. |
Here’s an example of the sort of code that’s generated for an entity class, as mandated by the JPA specification:
@StaticMetamodel(Book.class)
public abstract class Book_ {
/**
* @see org.example.Book#isbn
**/
public static volatile SingularAttribute<Book, String> isbn;
/**
* @see org.example.Book#text
**/
public static volatile SingularAttribute<Book, String> text;
/**
* @see org.example.Book#title
**/
public static volatile SingularAttribute<Book, String> title;
/**
* @see org.example.Book#type
**/
public static volatile SingularAttribute<Book, Type> type;
/**
* @see org.example.Book#publicationDate
**/
public static volatile SingularAttribute<Book, LocalDate> publicationDate;
/**
* @see org.example.Book#publisher
**/
public static volatile SingularAttribute<Book, Publisher> publisher;
/**
* @see org.example.Book#authors
**/
public static volatile SetAttribute<Book, Author> authors;
public static final String ISBN = "isbn";
public static final String TEXT = "text";
public static final String TITLE = "title";
public static final String TYPE = "type";
public static final String PUBLICATION_DATE = "publicationDate";
public static final String PUBLISHER = "publisher";
public static final String AUTHORS = "authors";
}
For each attribute of the entity, the Book_
class has:
-
a
String
-valued constant likeTITLE
, and -
a typesafe reference like
title
to a metamodel object of typeAttribute
.
Tip
|
Hibernate Processor allows statically-typed access to elements of the JPA EntityType<Book> book = entityManagerFactory.getMetamodel().entity(Book.class);
SingularAttribute<Book,Long> id = book.getDeclaredId(Long.class) This is very useful for writing generic code in frameworks or libraries. For example, you could use it to create your own criteria query API. |
The JPA static metamodel for an entity also contains members representing the named queries and named entity graphs declared by @NamedQuery
, @NamedNativeQuery
, and @NamedEntityGraph
annotations of the entity class.
For example, if we had:
@CheckHQL // validate named queries at compile time
@NamedQuery(name = "findBooksByTitle",
query = "from Book where title like :title order by title")
@Entity
class Book { ... }
Then we may execute the query as follows:
var books =
entityManager.createNamedQuery(Queries_._findBooksByTitle_)
.setParameter("title", titlePattern)
.setPage(page)
.getResultList();
Notice that no typecast was required here, since the generated code embeds the return type of the query as a type argument of the JPA TypedQueryReference
:
/**
* @see #_findBooksByTitle_
**/
public static final String QUERY_FIND_BOOKS_BY_TITLE = "findBooksByTitle";
/**
* The query named {@value QUERY_FIND_BOOKS_BY_TITLE}
* <pre>
* from Book where title like :title order by title
* </pre>
*
* @see org.example.Book
**/
public static volatile TypedQueryReference<Book> _findBooksByTitle_;
Tip
|
Actually, Hibernate Processor doesn’t require that such annotations be applied to the entity class itself, as we already saw earlier. |
We’ve already been using metamodel references like Book_.authors
and Book.AUTHORS
in the previous chapters.
So now let’s see what else Hibernate Processor can do for us.
Note
|
The functionality we’re about to describe was developed before Jakarta Data took on its current shape, and directly triggered the apocalypse which lead to the final form of the specification. Therefore, there’s massive overlap between the functionality described in this chapter, and the functionality available via the Jakarta Data annotations. On the other hand, Jakarta Data can’t do everything described below, and in particular it doesn’t yet come with built-in support for stateful persistence contexts or reactive sessions. We’ve therefore opted not to rewrite this chapter in a Jakarta Data-centric way, and instead refer you to Introducing Hibernate Data Repositories. |
Automatic generation of finder methods and query methods is a relatively new feature of Hibernate Processor, and an extension to the functionality defined by the JPA specification. In this chapter, we’re going to explore these features.
We’re going to meet three different kinds of generated method:
-
a named query method has its signature and implementation generated directly from a
@NamedQuery
annotation, -
a query method has a signature that’s explicitly declared, and a generated implementation which executes a HQL or SQL query specified via a
@HQL
or@SQL
annotation, and -
a finder method annotated
@Find
has a signature that’s explicitly declared, and a generated implementation inferred from the parameter list.
We’re also going to see two ways that these methods can be called:
-
as static methods of a generated abstract class, or
-
as instance methods of an interface with a generated implementation which may even be injected.
To whet our appetites, let’s see how this works for a @NamedQuery
.
Caution
|
The functionality described in the rest of this chapter depends on the use of the annotations described in [entities]. Hibernate Processor is not currently able to generate finder methods and query methods for entities declared completely in XML, and it’s not able to validate HQL which queries such entities. (On the other hand, the O/R mappings may be specified in XML, since they’re not needed by the Processor.) |
The very simplest way to generate a query method is to put a @NamedQuery
annotation anywhere we like, with a name
beginning with the magical character #
.
Let’s just stick it on the Book
class:
@CheckHQL // validate the query at compile time
@NamedQuery(name = "#findByTitleAndType",
query = "select book from Book book where book.title like :title and book.type = :type")
@Entity
public class Book { ... }
Now the Processor adds the following method declaration to the metamodel class Book_
.
/**
* Execute named query {@value #QUERY_FIND_BY_TITLE_AND_TYPE} defined by annotation of {@link Book}.
**/
public static List<Book> findByTitleAndType(@Nonnull EntityManager entityManager, String title, Type type) {
return entityManager.createNamedQuery(QUERY_FIND_BY_TITLE_AND_TYPE)
.setParameter("title", title)
.setParameter("type", type)
.getResultList();
}
We can easily call this method from wherever we like, as long as we have access to an EntityManager
:
List<Book> books =
Book_.findByTitleAndType(entityManager, titlePattern, Type.BOOK);
Now, this is quite nice, but it’s a bit inflexible in various ways, and so this probably isn’t the best way to generate a query method.
The principal problem with generating the query method straight from the @NamedQuery
annotation is that it doesn’t let us explicitly specify the return type or parameter list.
In the case we just saw, Hibernate Processor does a reasonable job of inferring the query return type and parameter types, but we’re often going to need a bit more control.
The solution is to write down the signature of the query method explicitly, as an abstract method in Java.
We’ll need a place to put this method, and since our Book
entity isn’t an abstract class, we’ll just introduce a new interface for this purpose:
interface Queries {
@HQL("where title like :title and type = :type")
List<Book> findBooksByTitleAndType(String title, String type);
}
Instead of @NamedQuery
, which is a type-level annotation, we specify the HQL query using the new @HQL
annotation, which we place directly on the query method.
This results in the following generated code in the Queries_
class:
@StaticMetamodel(Queries.class)
public abstract class Queries_ {
/**
* Execute the query {@value #FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type}.
*
* @see org.example.Queries#findBooksByTitleAndType(String,Type)
**/
public static List<Book> findBooksByTitleAndType(@Nonnull EntityManager entityManager, String title, Type type) {
return entityManager.createQuery(FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type, Book.class)
.setParameter("title", title)
.setParameter("type", type)
.getResultList();
}
static final String FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type =
"where title like :title and type = :type";
}
Notice that the signature differs just slightly from the one we wrote down in the Queries
interface: the Processor has prepended a parameter accepting EntityManager
to the parameter list.
If we want to explicitly specify the name and type of this parameter, we may declare it explicitly:
interface Queries {
@HQL("where title like :title and type = :type")
List<Book> findBooksByTitleAndType(StatelessSession session, String title, String type);
}
Hibernate Processor defaults to using EntityManager
as the session type, but other types are allowed:
-
Session
, -
StatelessSession
, or -
Mutiny.Session
orMutiny.StatelessSession
from Hibernate Reactive.
The real value of all this is in the checks which can now be done at compile time. Hibernate Processor verifies that the parameters of our abstract method declaration match the parameters of the HQL query, for example:
-
for a named parameter
:alice
, there must be a method parameter namedalice
with exactly the same type, or -
for an ordinal parameter
?2
, the second method parameter must have exactly the same type.
The query must also be syntactically legal and semantically well-typed, that is, the entities, attributes, and functions referenced in the query must actually exist and have compatible types. Hibernate Processor determines this by inspecting the annotations of the entity classes at compile time.
Note
|
The |
The @HQL
annotation has a friend named @SQL
which lets us specify a query written in native SQL instead of in HQL.
In this case there’s a lot less the Processor can do to check that the query is legal and well-typed.
We imagine you’re wondering whether a static
method is really the right thing to use here.
One thing not to like about what we’ve just seen is that we can’t transparently replace a generated static
function of the Queries_
class with an improved handwritten implementation without impacting clients.
Now, if our query is only called in one place, which is quite common, this isn’t going to be a big issue, and so we’re inclined to think the static
function is fine.
But if this function is called from many places, it’s probably better to promote it to an instance method of some class or interface. Fortunately, this is straightforward.
All we need to do is add an abstract getter method for the session object to our Queries
interface.
(And remove the session from the method parameter list.)
We may call this method anything we like:
interface Queries {
EntityManager entityManager();
@HQL("where title like :title and type = :type")
List<Book> findBooksByTitleAndType(String title, String type);
}
Here we’ve used EntityManager
as the session type, but other types are allowed, as we saw above.
Now Hibernate Processor does something a bit different:
@StaticMetamodel(Queries.class)
public class Queries_ implements Queries {
private final @Nonnull EntityManager entityManager;
public Queries_(@Nonnull EntityManager entityManager) {
this.entityManager = entityManager;
}
public @Nonnull EntityManager entityManager() {
return entityManager;
}
/**
* Execute the query {@value #FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type}.
*
* @see org.example.Queries#findBooksByTitleAndType(String,Type)
**/
@Override
public List<Book> findBooksByTitleAndType(String title, Type type) {
return entityManager.createQuery(FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type, Book.class)
.setParameter("title", title)
.setParameter("type", type)
.getResultList();
}
static final String FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type =
"where title like :title and type = :type";
}
The generated class Queries_
now implements the Queries
interface, and the generated query method implements our abstract method directly.
Of course, the protocol for calling the query method has to change:
Queries queries = new Queries_(entityManager);
List<Book> books = queries.findByTitleAndType(titlePattern, Type.BOOK);
If we ever need to swap out the generated query method with one we write by hand, without impacting clients, all we need to do is replace the abstract method with a default
method of the Queries
interface.
For example:
interface Queries {
EntityManager entityManager();
// handwritten method replacing previous generated implementation
default List<Book> findBooksByTitleAndType(String title, String type) {
entityManager()
.createQuery("where title like :title and type = :type", Book.class)
.setParameter("title", title)
.setParameter("type", type)
.setFlushMode(COMMIT)
.setMaxResults(100)
.getResultList();
}
}
What if we would like to inject a Queries
object instead of calling its constructor directly?
Tip
|
As you recall, we don’t think these things really need to be container-managed objects. But if you want them to be—if you’re allergic to calling constructors, for some reason—then:
Thus, the generated implementation of |
Is the Queries
interface starting to look a lot like a DAO-style repository object?
Well, perhaps.
You can certainly decide to use this facility to create a BookRepository
if that’s what you prefer.
But unlike a repository, our Queries
interface:
-
doesn’t attempt to hide the
EntityManager
from its clients, -
doesn’t implement or extend any framework-provided interface or abstract class, at least not unless you want to create such a framework yourself, and
-
isn’t restricted to service a particular entity class.
We can have as many or as few interfaces with query methods as we like. There’s no one-one-correspondence between these interfaces and entity types. This approach is so flexible that we don’t even really know what to call these "interfaces with query methods".
At this point, one usually begins to question whether it’s even necessary to write a query at all. Would it be possible to just infer the query from the method signature?
In some simple cases it’s indeed possible, and this is the purpose of finder methods.
A finder method is a method annotated @Find
.
For example:
@Find
Book getBook(String isbn);
A finder method may have multiple parameters:
@Find
List<Book> getBooksByTitle(String title, Type type);
The name of the finder method is arbitrary and carries no semantics. But:
-
the return type determines the entity class to be queried, and
-
the parameters of the method must match the fields of the entity class exactly, by both name and type.
Considering our first example, Book
has a persistent field String isbn
, so this finder method is legal.
If there were no field named isbn
in Book
, or if it had a different type, this method declaration would be rejected with a meaningful error at compile time.
Similarly, the second example is legal, since Book
has fields String title
and Type type
.
Important
|
You might notice that our solution to this problem is very different from the approach taken by others. In DAO-style repository frameworks, you’re asked to encode the semantics of the finder method into the name of the method. This idea came to Java from Ruby, and we think it doesn’t belong here. It’s completely unnatural in Java, and by almost any measure other than counting characters it’s objectively worse than just writing the query in a string literal. At least string literals accommodate whitespace and punctuation characters. Oh and, you know, it’s pretty useful to be able to rename a finder method without changing its semantics. 🙄 |
The code generated for this finder method depends on what kind of fields match the method parameters:
|
Uses |
All |
Uses |
Other persistent fields, or a mix of field types |
Uses a criteria query |
The generated code also depends on what kind of session we have, since the capabilities of stateless sessions, and of reactive sessions, differ slightly from the capabilities of regular stateful sessions.
With EntityManager
as the session type, we obtain:
/**
* Find {@link Book} by {@link Book#isbn isbn}.
*
* @see org.example.Dao#getBook(String)
**/
@Override
public Book getBook(@Nonnull String isbn) {
return entityManager.find(Book.class, isbn);
}
/**
* Find {@link Book} by {@link Book#title title} and {@link Book#type type}.
*
* @see org.example.Dao#getBooksByTitle(String,Type)
**/
@Override
public List<Book> getBooksByTitle(String title, Type type) {
var builder = entityManager.getEntityManagerFactory().getCriteriaBuilder();
var query = builder.createQuery(Book.class);
var entity = query.from(Book.class);
query.where(
title==null
? entity.get(Book_.title).isNull()
: builder.equal(entity.get(Book_.title), title),
type==null
? entity.get(Book_.type).isNull()
: builder.equal(entity.get(Book_.type), type)
);
return entityManager.createQuery(query).getResultList();
}
It’s even possible to match a parameter of a finder method against a property of an associated entity or embeddable.
The natural syntax would be a parameter declaration like String publisher.name
, but because that’s not legal Java, we can write it as String publisher$name
, taking advantage of a legal Java identifier character that nobody ever uses for anything else:
@Find
List<Book> getBooksByPublisherName(String publisher$name);
The @Pattern
annotation may be applied to a parameter of type String
, indicating that the argument is a wildcarded pattern which will be compared using like
.
@Find
List<Book> getBooksByTitle(@Pattern String title, Type type);
Even better, a parameter may be of type Range<T>
, where T
is the type of the matching field.
@Find
List<Book> getBooksByTitle(Range<String> title, Type type);
The Range
interface has a variety of static
methods the caller may use to construct different kinds of ranges.
For example, Range.pattern()
constructs a Range
representing a pattern.
List<Book> books =
// returns books with titles beginning with "hibernate"
queries.getBooksByTitle(Range.prefix("hibernate", false), type);
A finder method may specify fetch profiles, for example:
@Find(namedFetchProfiles=Book_.FETCH_WITH_AUTHORS)
Book getBookWithAuthors(String isbn);
This lets us declare which associations of Book
should be pre-fetched by annotating the Book
class.
Optionally, a query method—or a finder method which returns multiple results—may have additional "magic" parameters which do not map to query parameters:
Parameter type | Purpose | Example argument |
---|---|---|
|
Specifies a page of query results |
|
|
Specifies an entity attribute to order by, if |
|
|
Specifies entity attributes to order by, if |
|
|
Specifies a column to order by, if the query returns a projection list |
|
|
Specifies columns to order by, if the query returns a projection list |
|
|
Specifies a restriction used to filter query results |
|
Thus, if we redefine our earlier query method as follows:
interface Queries {
@HQL("from Book where title like :title and type = :type")
List<Book> findBooksByTitleAndType(String title, Type type,
Page page, Order<? super Book>... order);
}
Then we can call it like this:
List<Book> books =
Queries_.findBooksByTitleAndType(entityManager, titlePattern, Type.BOOK,
Page.page(RESULTS_PER_PAGE, page), Order.asc(Book_.isbn));
Alternatively, we could have written this query method as a finder method:
interface Queries {
@Find
List<Book> getBooksByTitle(String title, Type type,
Page page, Order<? super Book>... order);
}
Similarly, we may define a query method which accepts an arbitrary Restriction
:
interface Queries {
@Find
List<Book> findBooks(Restriction<? super Book> restriction, Order<? super Book>... order);
}
As we saw earlier, the Restriction
interface has a variety of static
methods for constructing restrictions.
List<Book> books =
// returns books with titles beginning with "hibernate", sorted by title
queries.findBooks(Restriction.startsWith(Book_.title, "hibernate", false),
Order.asc(Book_.title));
This gives some dynamic control over query execution.
We’ll see below that it’s even possible for the caller to gain direct control over the Query
object.
A generated query or finder method can make use of key-based pagination.
@Query("where publicationDate > :minDate")
KeyedResultList<Book> booksFromDate(Session session, LocalDate minDate, KeyedPage<Book> page);
Note that this method:
-
accepts a
KeyedPage
, and -
returns
KeyedResultList
.
Such a method may be used like this:
// obtain the first page of results
KeyedResultList<Book> first =
Queries_.booksFromDate(session, minDate,
Page.first(25).keyedBy(Order.asc(Book_.isbn)));
List<Book> firstPage = first.getResultList();
...
if (!firstPage.isLastPage()) {
// obtain the second page of results
KeyedResultList<Book> second =
Queries_.booksFromDate(session, minDate,
firstPage.getNextPage());
List<Book> secondPage = second.getResultList();
...
}
A query method doesn’t need to return List
.
It might return a single Book
.
@HQL("where isbn = :isbn")
Book findBookByIsbn(String isbn);
For a query with a projection list, Object[]
or List<Object[]>
is permitted:
@HQL("select isbn, title from Book where isbn = :isbn")
Object[] findBookAttributesByIsbn(String isbn);
But when there’s just one item in the select
list, the type of that item should be used:
@HQL("select title from Book where isbn = :isbn")
String getBookTitleByIsbn(String isbn);
@HQL("select local datetime")
LocalDateTime getServerDateTime();
A query which returns a selection list may have a query method which repackages the result as a record, as we saw in [projection-lists].
record IsbnTitle(String isbn, String title) {}
@HQL("select isbn, title from Book")
List<IsbnTitle> listIsbnAndTitleForEachBook(Page page);
A query method might even return TypedQuery
or SelectionQuery
:
@HQL("where title like :title")
SelectionQuery<Book> findBooksByTitle(String title);
This is extremely useful at times, since it allows the client to further manipulate the query:
List<Book> books =
Queries_.findBooksByTitle(entityManager, titlePattern)
.setOrder(Order.asc(Book_.title)) // order the results
.setPage(Page.page(RESULTS_PER_PAGE, page)) // return the given page of results
.setFlushMode(FlushModeType.COMMIT) // don't flush session before query execution
.setReadOnly(true) // load the entities in read-only mode
.setCacheStoreMode(CacheStoreMode.BYPASS) // don't cache the results
.setComment("Hello world!") // add a comment to the generated SQL
.getResultList();
An insert
, update
, or delete
query must return int
, boolean
, or void
.
@HQL("delete from Book")
int deleteAllBooks();
@HQL("update Book set discontinued = true where discontinued = false and isbn = :isbn")
boolean discontinueBook(String isbn);
@HQL("update Book set discontinued = true where isbn = :isbn")
void discontinueBook(String isbn);
On the other hand, finder methods are currently much more limited.
A finder method must return an entity type like Book
, or a list of the entity type, List<Book>
, for example.
Note
|
As you might expect, for a reactive session, all query methods and finder methods must return |
What if you just don’t like the ideas we’ve presented in this chapter, preferring to call the Session
or EntityManager
directly, but you still want compile-time validation for HQL?
Or what if you do like the ideas, but you’re working on a huge existing codebase full of code you don’t want to change?
Well, there’s a solution for you, too.
The Query Validator is a separate annotation processor that’s capable of type-checking HQL strings, not only in annotations, but even when they occur as arguments to createQuery()
, createSelectionQuery()
, or createMutationQuery()
. It’s even able to check calls to setParameter()
, with some restrictions.
The Query Validator works in javac
, Gradle, Maven, and the Eclipse Java Compiler.
Caution
|
Unlike Hibernate Processor, which is a completely bog-standard Java annotation processor based on only standard Java APIs, the Query Validator makes use of internal compiler APIs in |