Preface
Jakarta Data is a new specification for repositories. A repository, in this context, means an interface exposing a typesafe API for interacting with a datastore. Jakarta Data is designed to accommodate a diverse range of database technologies, from relational databases, to document databases, to key-value stores and more.
Hibernate Data Repositories is an implementation of Jakarta Data targeting relational databases and backed by Hibernate ORM. Entity classes are mapped using the familiar annotations defined by Jakarta Persistence, and queries may be written in the Hibernate Query Language, a superset of the Jakarta Persistence Query Language (JPQL). On the other hand, the programming model for interacting with the database is quite different in Jakarta Data from the model you might be used to from Jakarta Persistence.
Therefore, this document will show you a different way to use Hibernate.
The coverage of Jakarta Data is intentionally inexhaustive. If exhaustion is sought, this document should be read in conjunction with the specification, which we’ve worked hard to keep readable.
1. Programming model
Jakarta Data and Jakarta Persistence both represent data in a typesafe way, using entity classes. Since Hibernate’s implementation of Jakarta Data is backed by access to a relational database, these entity classes are mapped using the annotations defined by Jakarta Persistence.
For example:
@Entity
public class Book {
@Id
String isbn;
@Basic(optional = false)
String title;
LocalDate publicationDate;
@Basic(optional = false)
String text;
@Enumerated(STRING)
@Basic(optional = false)
Type type = Type.Book;
@ManyToOne(optional = false, fetch = LAZY)
Publisher publisher;
@ManyToMany(mappedBy = Author_.BOOKS)
Set<Author> authors;
...
}
@Entity
public class Author {
@Id
String ssn;
@Basic(optional = false)
String name;
Address address;
@ManyToMany
Set<Book> books;
}
For more information about mapping entities, see the Introduction to Hibernate 6.
Jakarta Data also works with entities defined using similarly-named annotations defined by Jakarta NoSQL.
But in this document we’re using Hibernate Data Repositories, so all mapping annotations should be understood to be the ones defined in |
Furthermore, queries may be expressed in HQL, Hibernate’s superset of the Jakarta Persistence Query Language (JPQL).
The Jakarta Data specification defines a simple subset of JPQL called, appropriately, JDQL. JDQL is mostly relevant to non-relational datastores; an implementation of Jakarta Data backed by access to relational data is normally expected to support a much larger subset of JPQL. Indeed, Hibernate Data Repositories supports a superset of JPQL. So, even though we put rather a large amount of effort into advocating, designing, and specifying JDQL, we won’t talk much about it here. For information about JDQL, please consult chapter 5 of the Jakarta Data specification. |
To learn more about HQL and JPQL, see the Guide to Hibernate Query Language.
This is where the similarity between Jakarta Persistence and Jakarta Data ends. The following table contrasts the two programming models.
Persistence | Data | |
---|---|---|
Persistence contexts |
Stateful |
Stateless |
Gateway |
|
User-written |
Underlying implementation |
||
Persistence operations |
Generic methods like |
Typesafe user-written methods annotated |
SQL execution |
During flush |
Immediate |
Updates |
Usually implicit (dirty checking during flush) |
Always explicit (by calling |
Operation cascading |
Depends on |
Never |
Lazy fetching |
Implicit |
Explicit using |
Validation of JPQL |
Runtime |
Compile time |
The fundamental difference here that Jakarta Data does not feature stateful persistence contexts. Among other consequences:
-
entity instances are always detached, and so
-
updates require an explicit operation, and
-
there’s no transparent lazy association fetching.
It’s important to understand that a repository in Hibernate Data Repositories is backed by a StatelessSession
, not by a Jakarta Persistence EntityManager
.
There’s only one portable way to fetch an association in Jakarta Data, and that’s by using a JPQL |
A future release of Jakarta Data will feature repositories backed by Jakarta Persistence stateful persistence contexts, but this functionality did not make the cut for Jakarta Data 1.0. |
The second big difference is that instead of providing a generic interface like EntityManager
that’s capable of performing persistence operations for any entity class, Jakarta Data requires that each interaction with the database go via a user-written method specific to just one entity type. The method is marked with annotations allowing Hibernate to fill in the method implementation.
For example, whereas Jakarta Persistence defines the methods find()
and persist()
of EntityManager
, in Jakarta Data the application programmer is required to write an interface like the following:
@Repository
interface Library {
@Find
Book book(String isbn);
@Insert
void add(Book book);
}
This is our first example of a repository.
1.1. Repository interfaces
A repository interface is an interface written by you, the application programmer, and annotated @Repository
.
The implementation of the repository interface is provided by a Jakarta Data provider, in our case, by Hibernate Data Repositories.
The Jakarta Data specification does not say how this should work, but in Hibernate Data Repositories, the implementation is generated by an annotation processor.
In fact, you might already be using this annotation processor: it’s just HibernateProcessor
from the now-inaptly-named hibernate-jpamodelgen
module.
That’s right, this fancy thing I’m calling Hibernate Data Repositories is really just a new feature of Hibernate’s venerable static metamodel generator. If you’re already using the JPA static metamodel in your project, you already have Jakarta Data at your fingertips. If you don’t, we’ll see how to set it up in the next chapter. |
Of course, a Jakarta Data provider can’t generate an implementation of any arbitrary method. Therefore, the methods of a repository interface must fall into one of the following categories:
-
lifecycle methods annotated
@Insert
,@Update
,@Delete
, or@Save
, -
automatic query methods annotated
@Find
, -
annotated query methods annotated
@Query
or@SQL
, and
For users migrating from Spring Data, Jakarta Data also provides a Query by Method Name facility. We don’t recommend this approach for new code, since it leads to extremely verbose and unnatural method names for anything but the most trivial examples. |
We’ll discuss each of these kinds of method soon. But first we need to ask a more basic question: how are persistence operations organized into repositories, and how do repository interfaces relate to entity types? The—perhaps surprising—answer is: it’s completely up to you.
1.2. Organizing persistence operations
Jakarta Data lets you freely assign persistence operations to repositories according to your own preference.
In particular, Jakarta Data does not require that a repository interface inherit a built-in supertype declaring the basic "CRUD" operations, and so it’s not necessary to have a separate repository interface for each entity.
You’re permitted, for example, to have a single Library
interface instead of BookRepository
, AuthorRepository
, and PublisherRepository
.
Thus, the whole programming model is much more flexible than older approaches such as Spring Data, which require a repository interface per entity class, or, at least, per so-called "aggregate".
The concept of an "aggregate" makes sense in something like a document database. But relational data does not have aggregates, and you should avoid attempting to shoehorn your relational tables into this inappropriate way of thinking about data. |
As a convenience, especially for users migrating from older frameworks, Jakarta Data does define the BasicRepository
and CrudRepository
interfaces, and you can use them if you like.
But in Jakarta Data there’s not much special about these interfaces; their operations are declared using the same annotations you’ll use to declare methods of your own repositories.
This older, less-flexible approach is illustrated in the following example.
// old way
@Repository
interface BookRepository
extends CrudRepository<Book,String> {
// query methods
...
}
@Repository
interface AuthorRepository
extends CrudRepository<Author,String> {
// query methods
...
}
We won’t see BasicRepository
and CrudRepository
again in this document, because they’re not necessary, and because they implement the older, less-flexible way of doing things.
Instead, our repositories will often group together operations dealing with several related entities, even when the entities don’t have a single "root".
This situation is extremely common in relational data models.
In our example, Book
and Author
are related by a @ManyToMany
association, and are both "roots".
// new way
@Repository
interface Publishing {
@Find
Book book(String isbn);
@Find
Author author(String ssn);
@Insert
void publish(Book book);
@Insert
void create(Author author);
// query methods
...
}
Now let’s walk through the different kinds of method that a repository interface might declare, beginning with the easiest kind. If the following summary is insufficient, you’ll find more detailed information about repositories in chapter 4 of the Jakarta Data specification, and in the Javadoc of the relevant annotations.
1.3. Default methods
A default
method is one you implement yourself, and there’s nothing special about it.
@Repository
interface Library {
default void hello() {
System.out.println("Hello, World!");
}
}
This doesn’t look very useful, at least not unless there’s some way to interact with the database from a default
method.
For that, we’ll need to add a resource accessor method.
1.4. Resource accessor methods
A resource accessor method is one which exposes access to an underlying implementation type.
Currently, Hibernate Data Repositories only supports one such type: StatelessSession
.
So a resource accessor method is just any abstract method which returns StatelessSession
.
The name of the method doesn’t matter.
StatelessSession session();
This method returns the StatelessSession
backing the repository.
Usually, a resource accessor method is called from a
This is very useful when we need to gain direct access to the |
A resource accessor method is also useful when we need to lazily fetch an association.
|
Usually, of course, we want Jakarta Data to take care of interacting with the StatelessSession
.
1.5. Lifecycle methods
Jakarta Data 1.0 defines four built-in lifecycle annotations, which map perfectly to the basic operations of the Hibernate StatelessSession
:
-
@Insert
maps toinsert()
, -
@Update
maps toupdate()
, -
@Delete
maps todelete()
, and -
@Save
maps toupsert()
.
The basic operations of StatelessSession — insert() , update() , delete() , and upsert() — do not have matching CascadeType s, and so these operations are never cascaded to associated entities.
|
A lifecycle method usually accepts an instance of an entity type, and is usually declared void
.
@Insert
void add(Book book);
Alternatively, it may accept a list or array of entities. (A variadic parameter is considered an array.)
@Insert
void add(Book... books);
A future release of Jakarta Data might expand the list of built-in lifecycle annotations.
In particular, we’re hoping to add |
Repositories wouldn’t be useful at all if this was all they could do. Jakarta Data really starts to shine when we start to use it to express queries.
1.6. Automatic query methods
An automatic query method is usually annotated @Find
.
The simplest automatic query method is one which retrieves an entity instance by its unique identifier.
@Find
Book book(String isbn);
The name of the parameter identifies that this is a lookup by primary key (the isbn
field is annotated @Id
in Book
) and so this method will be implemented to call the get()
method of StatelessSession
.
If the parameter name does not match any field of the returned entity type, or if the type of the parameter does not match the type of the matching field, |
If there is no Book
with the given isbn
in the database, the method throws EmptyResultException
.
There’s two ways around this if that’s not what we want:
-
declare the method to return
Optional
, or -
annotate the method
@jakarta.annotation.Nullable
.
The first option is blessed by the specification:
@Find
Optional<Book> book(String isbn);
The second option is an extension provided by Hibernate:
@Find @Nullable
Book book(String isbn);
An automatic query method might return multiple results. In this case, the return type must be an array or list of the entity type.
@Find
List<Book> book(String title);
Usually, arguments to a parameter of an automatic query method must match exactly with the field of an entity.
However, Hibernate provides the @Pattern
annotation to allow for "fuzzy" matching using like
.
@Find
List<Book> books(@Pattern String title);
Furthermore, if the parameter type is a list or array of the entity field type, the resulting query has an in
condition.
@Find
List<Book> books(String[] ibsn);
Or course, an automatic query method might have multiple parameters.
@Find
List<Book> book(@Pattern String title, Year yearPublished);
In this case, every argument must match the corresponding field of the entity.
The _
character in a parameter name may be used to navigate associations:
@Find
List<Book> booksPublishedBy(String publisher_name);
However, once our query starts to involve multiple entities, it’s usually better to use an annotated query method.
The @OrderBy
annotation allows results to be sorted.
@Find
@OrderBy("title")
@OrderBy("publisher.name")
List<Book> book(@Pattern String title, Year yearPublished);
This might not look very typesafe at first glance, but—amazingly—the content of the @OrderBy
annotation is completely validated at compile time, as we will see below.
Automatic query methods are great and convenient for very simple queries. For anything that’s not extremely simple, we’re much better off writing a query in JPQL.
1.7. Annotated query methods
An annotated query method is declared using:
The @Query
annotation is defined to accept JPQL, JDQL, or anything in between.
In Hibernate Data Repositories, it accepts arbitrary HQL.
There’s no strong reason to use |
Consider the following example:
@Query("where title like :pattern order by title, isbn")
List<Book> booksByTitle(String pattern);
You might notice that:
-
The
from
clause is not required in JDQL, and is inferred from the return type of the repository method. -
Since Jakarta Persistence 3.2, neither the
select
cause nor entity aliases (identification variables) are required in JPQL, finally standardizing a very old feature of HQL.
This allows simple queries to be written in a very compact form.
Method parameters are automatically matched to ordinal or named parameters of the query.
In the previous example, pattern
matches :pattern
.
In the following variation, the first method parameter matches ?1
.
@Query("where title like ?1 order by title, isbn")
List<Book> booksByTitle(String pattern);
You might be imagining that the JPQL query specified within the @Query
annotation cannot be validated at compile time, but this is not the case.
HibernateProcessor
is not only capable of validating the syntax of the query, but it even typechecks the query completely.
This is much better than passing a string to the createQuery()
method of EntityManager
, and it’s probably the top reason to use Jakarta Data with Hibernate.
When a query returns more than one object, the nicest thing to do is package each result as an instance of a Java record
type.
For example, we might define a record
holding some fields of Book
and Author
.
record AuthorBookSummary(String isbn, String ssn, String authorName, String title) {}
We need to specify that the values in the select
clause should be packaged as instances of AuthorBookSummary
.
The JPQL specification provides the select new
construct for this.
@Query("select new AuthorBookRecord(b.isbn, a.ssn, a.name, b.title " +
"from Author a join books b " +
"where title like :pattern")
List<AuthorBookSummary> summariesForTitle(@Pattern String pattern);
Note that the from
clause is required here, since it’s impossible to infer the queried entity type from the return type of the repository method.
Since this is quite verbose, Hibernate doesn’t require the use of
|
An annotated query method may even perform an update
, delete
, or insert
statement.
@Query("delete from Book " +
"where extract(year from publicationDate) < :year")
int deleteOldBooks(int year);
The method must be declared void
, or return int
or long
.
The return value is the number of affected records.
Finally, a native SQL query may be specified using @SQL
.
@SQL("select title from books where title like :pattern order by title, isbn")
List<String> booksByTitle(String pattern);
Unfortunately, native SQL queries cannot be validated at compile time, so if there’s anything wrong with our SQL, we won’t find out until we run our program.
1.8. @By
and @Param
Query methods match method parameters to entity fields or query parameters by name. Occasionally, this is inconvenient, resulting in less natural method parameter names. Let’s reconsider an example we already saw above:
@Find
List<Book> books(String[] ibsn);
Here, because the parameter name must match the field isbn
of Book
, we couldn’t call it isbns
, plural.
The @By
annotation lets us work around this problem:
@Find
List<Book> books(@By("isbn") String[] ibsns);
Naturally, the name and type of the parameter are still checked at compile time; there’s no loss of typesafety here, despite the string.
The @Param
annotation is significantly less useful, since we can always rename our HQL query parameter to match the method parameter, or, at worst, use an ordinal parameter instead.
2. Configuration and integration
Getting started with Hibernate Data Repositories involves the following steps:
-
set up a project with Hibernate ORM and
HibernateProcessor
, -
configure a persistence unit,
-
make sure a
StatelessSession
for that persistence unit is available for injection, and then -
inject a repository using CDI or some other implementation of
jakarta.inject
.
2.1. Project setup
We definitely need the following dependencies in our project:
Dependency | Explanation |
---|---|
|
The Jakarta Data API |
|
Hibernate ORM |
|
The annotation processor itself |
And we’ll need to pick a JDBC driver:
Database | Driver dependency |
---|---|
PostgreSQL or CockroachDB |
|
MySQL or TiDB |
|
MariaDB |
|
DB2 |
|
SQL Server |
|
Oracle |
|
H2 |
|
HSQLDB |
|
In addition, we might add some of the following to the mix.
Optional dependency | Explanation |
---|---|
|
Hibernate Validator |
|
log4j |
|
Weld CDI |
You’ll need to configure the annotation processor to run when your project is compiled.
In Gradle, for example, you’ll need to use annotationProcessor
.
annotationProcessor 'org.hibernate.orm:hibernate-jpamodelgen:6.5.0'
2.2. Excluding classes from processing
There’s three ways to limit the annotation processor to certain classes:
-
A given repository may be excluded from processing simply by specifying
@Repository(provider="acme")
where"acme"
is any string other than the empty string or a string equal, ignoring case, to"Hibernate"
. This is the preferred solution when there are multiple Jakarta Data Providers available. -
A package or type may be excluded by annotating it with the
@Exclude
annotation fromorg.hibernate.annotations.processing
. -
The annotation processor may be limited to consider only certain types or certain packages using the
include
configuration option, for example,-Ainclude=*.entity.*,*Repository
. Alternatively, types or packages may be excluded using theexclude
option, for example,-Aexclude=*Impl
.
2.3. Configuring Hibernate ORM
How you configure Hibernate depends on the environment you’re running in, and on your preference:
-
in Java SE, we often just use
hibernate.properties
, but some people prefer to usepersistence.xml
, especially in case of multiple persistence units, -
in Quarkus, we must use
application.properties
, and -
in a Jakarta EE container, we usually use
persistence.xml
.
Here’s a simple hibernate.properties
file for h2 database, just to get you started.
# Database connection settings
jakarta.persistence.jdbc.url=jdbc:h2:~/h2temp;DB_CLOSE_DELAY=-1
jakarta.persistence.jdbc.user=sa
jakarta.persistence.jdbc.pass=
# Echo all executed SQL to console
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.highlight_sql=true
# Automatically export the schema
hibernate.hbm2ddl.auto=create
Please see the Introduction to Hibernate 6 for more information about configuring Hibernate.
2.4. Obtaining a StatelessSession
Each repository implementation must somehow obtain a StatelessSession
for its persistence unit.
This usually happens via dependency injection, so you’ll need to make sure that a StatelessSession
is available for injection:
-
in Quarkus, this problem is already taken care of for us—there’s always an injectable
StatelessSession
bean for each persistence unit, and -
in a Jakarta EE environment,
HibernateProcessor
generates special code which takes care of creating and destroying theStatelessSession
, but -
in other environments, this is something we need to take care of ourselves.
Depending on the libraries in your build path, |
If you have multiple persistence units, you’ll need to disambiguate the persistence unit for a repository interface using @Repository(dataStore="my-persistence-unit-name")
.
2.5. Injecting a repository
In principle, any implementation of jakarta.inject
may be used to inject a repository implementation.
@Inject Library library;
However, this code will fail if the repository implementation is not able to obtain a StatelessSession
from the bean container.
It’s always possible to instantiate a repository implementation directly.
Library library = new Library_(statelessSession);
This is useful for testing, or for executing in an environment with no support for jakarta.inject
.
2.6. Integration with Jakarta EE
Jakarta Data specifies that methods of a repository interface may be annotated with:
-
Jakarta Bean Validation constraint annotations, and
-
Jakarta Interceptors interceptor binding types, including,
-
in particular, the
@Transactional
interceptor binding defined by Jakarta Transactions.
Note that these annotations are usually applied to a CDI bean implementation class, not to an interface,[1] but a special exception is made for repository interfaces.
Therefore, when running in a Jakarta EE environment, or in Quarkus, and when an instance of a repository interface is obtained via CDI, the semantics of such annotations is respected.
@Transactional @Repository
public interface Library {
@Find
Book book(@NotNull String isbn);
@Find
Book book(@NotBlank String title, @NotNull LocalDate publicationDate);
}
As an aside, it’s rather satisfying to see all these things working so nicely together, since we members of the Hibernate team played pivotal roles in the creation of the Persistence, Bean Validation, CDI, Interceptors, and Data specifications.
3. Pagination and dynamic sorting
-
additional sorting criteria, and/or
-
a limit and offset restricting the results which are actually returned to the client.
Before we see this, let’s see how we can refer to a field of an entity in a completely typesafe way.
3.1. The static metamodel
You might already be familiar with the Jakarta Persistence static metamodel.
For an entity class Book
, the class Book_
exposes objects representing the persistent fields of Book
, for example, Book_.title
represents the field title
.
This class is generated by HibernateProcessor
at compilation time.
Jakarta Data has its own static metamodel, which is different to the Jakarta Persistence metamodel, but conceptually very similar. Instead of Book_
, the Jakarta Data static metamodel for Book
is exposed by the class _Book
.
The Jakarta Persistence static metamodel is most commonly used together with the Criteria Query API or the |
Let’s see how the static metamodel is useful, by considering a simple example.
It’s perfectly possible to obtain an instance of Sort
by passing the name of a field:
var sort = Sort.asc("title");
Unfortunately, since this is in regular code, and not in an annotation, the field name "title"
cannot be validated at compile time.
A much better solution is to use the static metamodel to obtain an instance of Sort
.
var sort = _Book.title.asc();
The static metamodel also declares constants containing the names of persistent fields.
For example, _Book.TITLE
evaluates to the string "title"
.
These constants are sometimes used as annotation values.
This example looks superficially more typesafe.
But since Hibernate Data Repositories already validates the content of the |
3.2. Dynamic sorting
Dynamic sorting criteria are expressed using the types Sort
and Order
:
-
an instance of
Sort
represents a single criterion for sorting query results, and -
an instance of
Order
packages multipleSort
s together.
A query method may accept an instance of Sort
.
@Find
List<Book> books(@Pattern String title, Year yearPublished,
Sort<Book> sort);
This method might be called as follows:
var books =
library.books(pattern, year,
_Book.title.ascIgnoreCase());
Alternatively the method may accept an instance of Order
.
@Find
List<Book> books(@Pattern String title, Year yearPublished,
Order<Book> order);
The method might now be called like this:
var books =
library.books(pattern, year,
Order.of(_Book.title.ascIgnoreCase(),
_Book.isbn.asc());
Dynamic sorting criteria may be combined with static criteria.
@Find
@OrderBy("title")
List<Book> books(@Pattern String title, Year yearPublished,
Sort<Book> sort);
We’re not convinced this is very useful in practice.
3.3. Limits
A Limit
is the simplest way to express a subrange of query results.
It specifies:
-
maxResults
, the maximum number of results to be returned from the database server to the client, and, -
optionally,
startAt
, an offset from the very first result.
These values map directly the familiar setMaxResults()
and setFirstResults()
of the Jakarta Persistence Query
interface.
@Find
@OrderBy(_Book.TITLE)
List<Book> books(@Pattern String title, Year yearPublished,
Limit limit);
var books =
library.books(pattern, year,
Limit.of(MAX_RESULTS));
A more sophisticated approach is provided by PageRequest
.
3.4. Offset-based pagination
A PageRequest
is superficially similar to a Limit
, except that it’s specified in terms of:
-
a page
size
, and -
a numbered
page
.
We can use a PageRequest
just like a Limit
.
@Find
@OrderBy("title")
@OrderBy("isbn")
List<Book> books(@Pattern String title, Year yearPublished,
PageRequest pageRequest);
var books =
library.books(pattern, year,
PageRequest.ofSize(PAGE_SIZE));
Query results should be totally ordered when a repository method is used for pagination.
The easiest way to be sure that you have a well-defined total order is to specify the identifier of the entity as the last element of the order.
For this reason, we specified |
However, a repository method which accepts a PageRequest
may return a Page
instead of a List
, making it easier to implement pagination.
@Find
@OrderBy("title")
@OrderBy("isbn")
Page<Book> books(@Pattern String title, Year yearPublished,
PageRequest pageRequest);
var page =
library.books(pattern, year,
PageRequest.ofSize(PAGE_SIZE));
var books = page.content();
long totalPages = page.totalPages();
// ...
while (page.hasNext()) {
page = library.books(pattern, year,
page.nextPageRequest().withoutTotal());
books = page.content();
// ...
}
Pagination may be combined with dynamic sorting.
@Find
Page<Book> books(@Pattern String title, Year yearPublished,
PageRequest pageRequest, Order<Book> order);
It’s important to pass the same arguments to query parameters, and the same sorting criteria, with each page request! The repository is stateless: it doesn’t remember the values passed on the previous page request. |
A repository method with return type Page
uses SQL offset and limit to implement pagination.
We’ll refer to this as offset-based pagination.
A problem with this approach is that it’s quite vulnerable to missed or duplicate results when the database is modified between page requests.
Therefore, Jakarta Data offers an alternative solution, which we’ll call key-based pagination.
3.5. Key-based pagination
In key-based pagination, the query results must be totally ordered by a unique key of the result set.
The SQL offset is replaced with a restriction on the unique key, appended to the where
clause of the query:
-
a request for the next page of query results uses the key value of the last result on the current page to restrict the results, or
-
a request for the previous page of query results uses the key value of the first result on the current page to restrict the results.
For key-based pagination, it’s essential that the query has a total order. |
From our point of view as users of Jakarta Data, key-based pagination works almost exactly like offset-based pagination.
The difference is that we must declare our repository method to return CursoredPage
.
@Find
@OrderBy("title")
@OrderBy("isbn")
CursoredPage<Book> books(@Pattern String title, Year yearPublished,
PageRequest pageRequest);
On the other hand, with key-based pagination, Hibernate must do some work under the covers rewriting our query.
Key-based pagination goes some way to protect us from skipped or duplicate results. The cost is that page numbers can lose synchronization with the query result set during navigation. This isn’t usually a problem, but it’s something to be aware of. |
3.6. Advanced control over querying
For more advanced usage, an automatic or annotated query method may be declared to return jakarta.persistence.Query
, jakarta.persistence.TypedQuery
, org.hibernate.query.Query
, or org.hibernate.query.SelectionQuery
.
@Find
SelectionQuery<Book> booksQuery(@Pattern String title, Year yearPublished);
default List<Book> booksQuery(String title, Year yearPublished) {
return books(title, yearPublished)
.enableFetchProfile(_Book.PROFILE_WITH_AUTHORS);
.setReadOnly(true)
.setTimeout(QUERY_TIMEOUT)
.getResultList();
}
This allows for direct control over query execution, without loss of typesafety.