Queries in Jakarta Data
The built-in CrudRepository methods are enough for working with one post. For search, filters and pages you add your own methods to the repository interface — again without an implementation. Jakarta Data works out the query in one of three ways: from the method name, from the @Find annotation with parameters, or from the query text in @Query. All examples in this lesson are additions to src/main/java/com/example/blog/Posts.java from the previous lesson.
Queries by method name
The method name is built from a prefix, the word By, an entity field and a condition:
// src/main/java/com/example/blog/Posts.java (fragment)
List<Post> findByTitleIgnoreCaseContains(String text);
long countByCreatedAtGreaterThan(LocalDate date);The first method finds posts whose title contains text, ignoring case; the second counts posts newer than a date. A mistake in a field name shows up the first time the method is called.
| Name part | Examples | Meaning |
|---|---|---|
| Prefix | find, count, exists, delete | Find, count, check, delete |
| Condition | Contains, StartsWith, Like, In, Null | Substring, prefix, pattern, list, empty |
| Comparison | GreaterThan, LessThanEqual, Between | Greater, not greater, range |
| Connectors | And, Or, Not, IgnoreCase | Several conditions, negation, case-insensitive |
| Ordering | OrderByTitleAsc, OrderByIdDesc | Sorting the result |
@Find and @By
Long names are hard to read. A method with @Find can have any name, and its parameters set the conditions:
// src/main/java/com/example/blog/Posts.java (fragment)
@Find
@OrderBy("title")
List<Post> publishedOn(@By("createdAt") LocalDate date);The method returns the posts of the given day, sorted by title. @By binds the parameter to a field: by default Maven does not keep parameter names in the class file, so the field is named explicitly.
@Query and JDQL
For complex conditions there is @Query. The query is written in JDQL (Jakarta Data Query Language) — a simplified JPQL where you may leave out SELECT and FROM when the query targets the repository's entity:
// src/main/java/com/example/blog/Posts.java (fragment)
@Query("where lower(title) like :pattern or lower(body) like :pattern order by id")
List<Post> search(@Param("pattern") String pattern);The method looks for the text in both the title and the body. @Param names the :pattern parameter.
Sorting and pages
Pagination does not need a new method: BasicRepository has findAll(PageRequest, Order). PageRequest is a page number (starting at 1) and its size, Order is a sort order made of one or more Sort. The result is a Page: the posts of the page plus information about all pages. Create a record for the response:
// src/main/java/com/example/blog/PostPage.java
package com.example.blog;
import java.util.List;
public record PostPage(List<Post> items, long page, long totalPages, long totalPosts) {
}The resource
Add these methods to PostV2Resource, plus the imports jakarta.data.Order, jakarta.data.Sort, jakarta.data.page.Page, jakarta.data.page.PageRequest, jakarta.ws.rs.QueryParam, jakarta.ws.rs.DefaultValue:
// src/main/java/com/example/blog/PostV2Resource.java (fragment)
@GET
@Path("/search")
public List<Post> search(@QueryParam("q") @DefaultValue("") String q) {
return posts.findByTitleIgnoreCaseContains(q);
}
@GET
@Path("/text")
public List<Post> text(@QueryParam("q") @DefaultValue("") String q) {
return posts.search("%" + q.toLowerCase() + "%");
}
@GET
@Path("/day/{date}")
public List<Post> day(@PathParam("date") String date) {
return posts.publishedOn(LocalDate.parse(date));
}
@GET
@Path("/count")
public long countAfter(@QueryParam("after") String date) {
return posts.countByCreatedAtGreaterThan(LocalDate.parse(date));
}
@GET
@Path("/page")
public PostPage page(@QueryParam("page") @DefaultValue("1") long page,
@QueryParam("size") @DefaultValue("2") int size) {
Page<Post> result = posts.findAll(
PageRequest.ofPage(page, size, true),
Order.by(Sort.desc("createdAt"), Sort.asc("id")));
return new PostPage(result.content(), page, result.totalPages(), result.totalElements());
}In PageRequest.ofPage(page, size, true) the third argument asks for the total number of posts; without it totalElements() is not available. Sort.desc("createdAt") puts new posts first, and Sort.asc("id") orders posts of the same day.
Trying it out
Add two more posts, "Query methods" and "Pagination", through POST /api/v2/posts so there are five. Replace 2026-10-04 with today's date:
curl "http://localhost:8080/blog/api/v2/posts/search?q=DATA"
curl "http://localhost:8080/blog/api/v2/posts/text?q=request"
curl http://localhost:8080/blog/api/v2/posts/day/2026-10-04
curl "http://localhost:8080/blog/api/v2/posts/count?after=2026-10-01"
curl "http://localhost:8080/blog/api/v2/posts/page?page=3&size=2"[{"body":"Repositories without boilerplate.","createdAt":"2026-10-04","id":3,"title":"Jakarta Data"}]
[{"body":"How a request reaches our code.","createdAt":"2026-10-02","id":2,"title":"Servlets and filters"},{"body":"Pages of posts with PageRequest.","createdAt":"2026-10-04","id":5,"title":"Pagination"}]
[{"body":"Repositories without boilerplate.","createdAt":"2026-10-04","id":3,"title":"Jakarta Data"},{"body":"Pages of posts with PageRequest.","createdAt":"2026-10-04","id":5,"title":"Pagination"},{"body":"findBy, Find and Query.","createdAt":"2026-10-04","id":4,"title":"Query methods"}]
4
{"items":[{"body":"The first post of the blog.","createdAt":"2026-10-01","id":1,"title":"Hello, Jakarta EE"}],"page":3,"totalPages":3,"totalPosts":5}
The search for DATA found "Jakarta Data" regardless of case, and text found the word in the bodies of two posts. The third page holds only one post, the oldest.
Common mistake
Taking a method name from Spring Data, for example countByCreatedAtAfter. Jakarta Data has no After keyword, and calling the method fails:
CWWKD1091E: An entity attribute named CreatedAtAfter is not found on the com.example.blog.Post entity class for the countByCreatedAtAfter method of the com.example.blog.Posts repository interface.
The server took CreatedAtAfter for a field name. Use GreaterThan and LessThan. Word order matters too: IgnoreCase comes right after the field, so findByTitleContainsIgnoreCase does not work, while findByTitleIgnoreCaseContains does.