Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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".

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.