Let's get started with a Microservice Architecture with Spring Cloud:
The @Find Annotation in Hibernate
Last updated: September 26, 2026
1. Introduction
In this article, we’re going to look at how to use the @Find annotation in Hibernate. We’ll see what it is, what it’s used for, and how to use it.
2. Setting up Hibernate
Before we can use the @Find annotation, we need to set up Hibernate and our database.
2.1. Dependencies
To use @Find in Hibernate, we need to use version 6.3 or newer. The newest release at the time of writing is 7.4.6.Final:
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>7.4.6.Final</version>
</dependency>
We also need the Jakarta Data Core API. The newest release at the time of writing is 1.0.2:
<dependency>
<groupId>jakarta.data</groupId>
<artifactId>jakarta.data-api</artifactId>
<version>1.0.2</version>
</dependency>
This gives us everything we need to write our repository code.
2.2. Hibernate Processor
In addition to the dependencies for writing our repositories, we need to configure the Hibernate Processor. This will generate concrete classes for our repository interfaces at compile time.
When using Hibernate 7, this is configured by adding an annotation processor to the maven-compiler-plugin:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-processor</artifactId>
<version>7.4.6.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
This needs to match the version of the hibernate-core dependency we specified earlier.
Once the plugin is added, the Maven build will automatically generate additional classes for us, including Hibernate Metamodel classes for our entities, and concrete classes that implement our repository interfaces, as we’ll see later.
2.3. Database
For this article, we need to set up a database. Our tables will look as follows:
CREATE TABLE authors (
author_id BIGINT PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE books (
book_id BIGINT PRIMARY KEY,
title TEXT NOT NULL,
author_id BIGINT NOT NULL,
FOREIGN KEY (author_id) REFERENCES authors(author_id)
);
This gives us two tables – books and authors – such that there’s a foreign key between them.
We then need some data in our database:
INSERT INTO authors (author_id, name) VALUES
(1, 'George Orwell'),
(2, 'Haruki Murakami'),
(3, 'Agatha Christie'),
(4, 'Ursula K. Le Guin');
INSERT INTO books (book_id, title, author_id) VALUES
(101, '1984', 1),
(102, 'Animal Farm', 1),
(103, 'Norwegian Wood', 2),
(104, 'Kafka on the Shore', 2),
(105, 'Murder on the Orient Express', 3),
(106, 'And Then There Were None', 3),
(107, 'The Left Hand of Darkness', 4);
2.4. JPA Entity
Finally, since we’ll be working with Hibernate, we also need Hibernate entities to represent our data.
First, our Author entity:
@Entity
@Table(name = "authors")
public class Author {
@Id
@Column(name = "author_id")
private Long authorId;
private String name;
public Long getAuthorId() {
return authorId;
}
public String getName() {
return name;
}
}
And then the Book entity:
@Entity
@Table(name = "books")
public class Book {
@Id
@Column(name = "book_id")
private Long bookId;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", nullable = false)
private Author author;
public Long getBookId() {
return bookId;
}
public String getTitle() {
return title;
}
public Author getAuthor() {
return author;
}
}
This references our Author entity so we can follow those links in our queries.
3. Repository Interfaces
Once everything is set up, we’re ready to start writing our repositories. These are written as Java interfaces and annotated with the @jakarta.data.repository.Repository annotation:
@Repository
public interface BookRepository {
}
On its own, this is enough for the Hibernate Processor to discover and generate a concrete implementation – in this case, called BookRepository_.
// Generated code
public class BookRepository_ implements BookRepository {
protected StatelessSession session;
public BookRepository_(StatelessSession session) {
this.session = session;
}
public StatelessSession session() {
return session;
}
}
This generates an implementation constructed with a StatelessSession instance. We can then create instances of this as needed when we want to query our data.
3.1. Injecting an EntityManager
We often don’t want to create repository instances on demand. Instead, we’d like to create them once at the start of our application and then pass them around. For example, we might want to create them in our Spring context.
Fortunately, Hibernate supports this too. All we need to do is add a special method to our interface that returns an EntityManager:
@Repository
public interface AuthorRepository {
EntityManager entityManager();
}
If we do this, Hibernate will construct an alternative form of our repository that is constructed using our EntityManager instead:
// Generated code
public class AuthorRepository_ implements AuthorRepository {
protected EntityManager entityManager;
public AuthorRepository_(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public EntityManager entityManager() {
return entityManager;
}
}
We can then safely construct this once and reuse it as much as needed.
4. Finding By Fields
Once we’ve got our repository, we need to be able to do something with it. We can add methods to find entities using the @Find annotation, with special conventions for parameter names and return types.
@Repository
public interface BookRepository {
@Find
List<Book> getAllBooks();
}
Here we have a method called getAllBooks. The method name is unimportant. However, the fact that it returns a List<Book> means that the generated code will understand that we’re working with Book entities, and that we’re returning all of the ones that match our query.
This will equate to running the following query:
SELECT b1_0.book_id, b1_0.author_id, b1_0.title
FROM books b1_0
We can go a step further with this by actually filtering our results:
@Find
Book getBookWithTitle(String title);
Again, the method title can be anything. However, the parameter name must exactly match a field in our Book entity. As such, Hibernate generates code that will filter by that field:
SELECT b1_0.book_id, b1_0.author_id, b1_0.title
FROM books b1_0
WHERE b1_0.title = ?
This time, we return only a single Book entity. As such, Hibernate knows to return a single matching entity. If there isn’t one, we’ll get an EmptyResultException thrown instead. Alternatively, if there are multiple records that match, we’ll get a NonUniqueResultException thrown.
5. Optional Results
Sometimes we want to search for a single record that may not exist. We can handle the EmptyResultException that gets thrown, but this isn’t ideal.
Hibernate supports a few ways to handle this automatically. The most obvious is to update our method to return Optional<Book> instead:
@Find
Optional<Book> getOptionalBookWithTitle(String title);
In this case, an unknown record returns Optional.empty() instead.
Alternatively, if we don’t want to deal with this, we can annotate our method with jakarta.annotation.Nullable.
@Find
@Nullable
Book getNullableBookWithTitle(String title);
This tells the generated code to return null instead of throwing an exception.
In both cases, the database queries are identical. The only difference is what the generated code does with the results after executing the query. Both cases also still throw a NonUniqueResultException if the query returns more than one result.
6. Finding By Nested Fields
So far we’ve seen how to find records based on data directly in that record. However, often we want to search based on related data too. Hibernate can do this based on how we name our method parameters. If we use a “$” symbol, this is interpreted as linking fields through related entities:
@Find
List<Book> getAllBooksByAuthorName(String author$name);
Here, our query will join from Book through the Book.author field, and then query based on Author.name:
SELECT b1_0.book_id, b1_0.author_id, b1_0.title
FROM books b1_0
JOIN authors a1_0 ON a1_0.author_id = b1_0.author_id
WHERE a1_0.name = ?
We can use this technique for as many steps as we want. However, we can only follow relationships that use @ManyToOne or @OneToOne. Relationships using @OneToMany or @ManyToMany won’t work since the generated query would need to link to multiple records.
7. Multiple Results
So far we’ve seen how to return either a single result or all matching results. However, sometimes we need more control. Hibernate can manage all of this for us.
7.1. Sorting
By default, queries that return lists of records return them in the order the database returns them. This can vary based on many factors, so we can’t rely on it for consistent ordering.
If we add the @OrderBy annotation to our method, Hibernate will sort our results by the specified fields:
@Find
@OrderBy("title")
List<Book> getAllBooks();
This equates to executing the following query:
SELECT b1_0.book_id, b1_0.author_id, b1_0.title
FROM books b1_0
ORDER BY b1_0.title
With the addition of the ORDER BY clause for consistent sorting.
If necessary, we can repeat the annotation to allow sorting by multiple fields:
@Find
@OrderBy("title")
@OrderBy("author$name")
List<Book> getAllBooks();
Note that we can also sort by fields on joined entities using exactly the same syntax as we saw earlier. Doing this generates a query like this:
SELECT b1_0.book_id, b1_0.author_id, b1_0.title
FROM books b1_0
JOIN authors a1_0 ON a1_0.author_id = b1_0.author_id
ORDER BY b1_0.title desc, a1_0.name
We can also specify the sort direction using the descending parameter. This defaults to false, but we can change this to indicate descending sorts:
@Find
@OrderBy(value = "title", descending = true)
List<Book> getAllBooks();
Unsurprisingly, we can mix all of this as needed, allowing for sorts on multiple fields in different directions.
We can also allow dynamic sorting by accepting a parameter of type Order<Book>.
@Find
List<Book> getAllBooks(Order<Book> sort);
This is generic over the entity that we’re working on. If we use the wrong generic type, Hibernate will throw an error at compile time. We can now call this and define the sorts at runtime:
List<Book> books = repository.getAllBooks(Order.by(
Sort.asc("title"),
Sort.desc("author.name")
));
Note that here we need to specify nested fields using dotted syntax instead of the “$” symbol, but otherwise it all works the same.
7.2. Pagination
In addition to sorting our results, we can also request specific pages of results. We can do this by adding a parameter of type PageRequest to our method:
@Find
@OrderBy("title")
List<Book> getBooksPage(PageRequest pageRequest);
We don’t technically need to enforce ordering on these queries, but it’s usually a good idea so results stay consistent across pages.
We can then call this specifying the desired page and page size:
List<Book> books = repository.getBooksPage(PageRequest.ofPage(1, 3, true));
Note that the page is 1-indexed, so this will return the first page of 3 records.
If we want to know more about the details of the page, we need to change our return type to Page<Book> instead of List<Book>:
@Find
@OrderBy("title")
Page<Book> getBooksPage(PageRequest pageRequest);
We now have access to not only the page contents, but also page details like the total number of elements and whether there are next and previous pages:
long total = books.totalElements();
if (books.hasNext()) {
// Do something
}
if (books.hasPrevious()) {
// Do something
}
Note that the third parameter to our PageRequest.ofPage() call tells Hibernate whether we want a total count of the elements across all pages. If we pass true, Hibernate executes an additional query to count matching elements. If we pass in false, Hibernate doesn’t execute this additional query, and the total number of elements won’t be available.
8. Summary
In this article, we’ve had a very brief look at using the @Find annotation in Hibernate. We’ve seen what it is, how it works, and how we can use it. Next time you need to generate repositories like this, why not give it a go?
As always, all the code from this article is available over on GitHub.
















