Jakarta Data
В прошлых уроках мы написали PostRepository вручную: каждый метод — запрос JPQL и вызов EntityManager. Jakarta Data 1.0 — новая спецификация Jakarta EE 11, которая убирает этот однообразный код. Вы объявляете только интерфейс репозитория (объект, через который приложение читает и сохраняет сущности), а реализацию сервер создаёт сам при запуске. Похожие репозитории есть в Spring Data, теперь они стали стандартом.
Возможность сервера
В Open Liberty спецификацию включает возможность data-1.0. Она уже входит в webProfile-11.0 из нашего server.xml, поэтому ничего добавлять не нужно. При старте она видна в логе:
[AUDIT ] CWWKF0012I: The server installed the following features: [appAuthentication-3.1, appSecurity-6.0, cdi-4.1, concurrent-3.1, data-1.0, ...]
Если в проекте перечислены отдельные возможности вместо профиля, добавьте строку <feature>data-1.0</feature> в featureManager. API тоже уже есть: пакет jakarta.data входит в зависимость jakarta.jakartaee-api версии 11.
Интерфейс репозитория
Создайте файл src/main/java/com/example/blog/Posts.java:
// src/main/java/com/example/blog/Posts.java
package com.example.blog;
import jakarta.data.repository.CrudRepository;
import jakarta.data.repository.Repository;
@Repository(dataStore = "java:app/env/blogUnit")
public interface Posts extends CrudRepository<Post, Long> {
}@Repository говорит серверу: создай реализацию и сделай её бином CDI. В CrudRepository<Post, Long> первый тип — сущность из урока про JPA, второй — тип её @Id. Тела у интерфейса нет, а методы уже есть: их дают родительские интерфейсы.
Связь с базой
Параметр dataStore указывает, через что репозиторий работает с базой. Возьмём нашу единицу хранения blog из persistence.xml — тогда репозиторий и PostRepository видят одну и ту же таблицу. Для этого единице хранения дают имя в JNDI. Добавьте аннотацию на RestApplication:
// src/main/java/com/example/blog/RestApplication.java
package com.example.blog;
import jakarta.persistence.PersistenceUnit;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/api")
@PersistenceUnit(name = "java:app/env/blogUnit", unitName = "blog")
public class RestApplication extends Application {
}@PersistenceUnit на классе объявляет ссылку java:app/env/blogUnit на единицу blog. Пространство java:app общее для всего приложения, поэтому объявить ссылку достаточно один раз.
Встроенные методы
| Метод | Откуда | Что делает |
|---|---|---|
findById(id) | BasicRepository | Ищет по ключу, возвращает Optional |
findAll() | BasicRepository | Все записи как Stream |
save(post) | BasicRepository | Вставляет новую или обновляет существующую |
deleteById(id), delete(post) | BasicRepository | Удаляет |
insert(post) | CrudRepository | Только вставка, для существующей — ошибка |
update(post) | CrudRepository | Только обновление |
CrudRepository расширяет BasicRepository. Если отдельные insert и update не нужны, можно наследовать BasicRepository.
Внедрение в ресурс
Чтобы не ломать существующий API, сделаем рядом вторую версию — /api/v2/posts. Создайте src/main/java/com/example/blog/PostV2Resource.java:
// src/main/java/com/example/blog/PostV2Resource.java
package com.example.blog;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.util.List;
@Path("/v2/posts")
@Produces(MediaType.APPLICATION_JSON)
public class PostV2Resource {
@Inject
Posts posts;
@GET
public List<Post> list() {
return posts.findAll().toList();
}
@GET
@Path("/{id}")
public Post show(@PathParam("id") long id) {
return posts.findById(id).orElseThrow(() -> new PostNotFoundException(id));
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Post create(@Valid NewPost data) {
return posts.insert(new Post(data.title(), data.body(), LocalDate.now()));
}
}Репозиторий внедряется через @Inject, как любой бин. Записывающие методы сами выполняются в транзакции, а внутри метода с @Transactional присоединяются к уже открытой.
Проверка
curl http://localhost:8080/blog/api/v2/posts
curl -i -H "Content-Type: application/json" -d '{"title":"Jakarta Data","body":"Репозитории без лишнего кода."}' http://localhost:8080/blog/api/v2/posts
curl http://localhost:8080/blog/api/posts/3[{"body":"Первая запись блога.","createdAt":"2026-10-01","id":1,"title":"Привет, Jakarta EE"},{"body":"Как запрос попадает в наш код.","createdAt":"2026-10-02","id":2,"title":"Сервлеты и фильтры"}]
HTTP/1.1 200 OK
X-Powered-By: Blog on Jakarta EE
Content-Type: application/json
Content-Length: 120
{"body":"Репозитории без лишнего кода.","createdAt":"2026-10-04","id":3,"title":"Jakarta Data"}
{"body":"Репозитории без лишнего кода.","createdAt":"2026-10-04","id":3,"title":"Jakarta Data"}
Запись, созданная через репозиторий, видна и в старом API /api/posts: оба работают с одной таблицей. Запрос /api/v2/posts/99 вернёт 404 с {"error":"Post 99 not found","status":404} — сработал обработчик PostNotFoundException из главы про REST.
Частая ошибка
Написать @Repository без dataStore. Тогда репозиторий ищет источник данных по умолчанию java:comp/DefaultDataSource, которого в нашем server.xml нет, и первый же запрос падает с ошибкой 500:
CWWKD1077E: The blog#blog.war application artifact has one or more repository interfaces (com.example.blog.Posts) that use or default to the java:comp/DefaultDataSource data store. This data store must be configured in the server configuration by defining a dataSource element that has an identifier of DefaultDataSource.
Укажите dataStore со ссылкой на единицу хранения, как выше, или опишите в server.xml источник данных с id="DefaultDataSource".