Jakarta Persistence
Пока записи блога живут в памяти и пропадают при перезапуске. Jakarta Persistence (JPA) сохраняет Java-объекты в реляционную базу: класс-сущность соответствует таблице, а объект — строке. В приложении на сервере есть важное отличие от обычной Java-программы: подключение к базе настраивает сервер, а приложение лишь обращается к нему по имени.
Драйвер базы
Для учебного блога возьмём H2 — базу, которая работает прямо в памяти сервера. Драйвер нужен серверу, а не WAR-файлу, поэтому его копирует плагин Liberty. Добавьте в pom.xml настройку плагина:
<!-- pom.xml (фрагмент) -->
<plugin>
<groupId>io.openliberty.tools</groupId>
<artifactId>liberty-maven-plugin</artifactId>
<version>3.12.3</version>
<configuration>
<copyDependencies>
<location>jdbc</location>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.5.252</version>
</dependency>
</copyDependencies>
</configuration>
</plugin>При запуске файл h2-2.5.252.jar окажется в папке jdbc сервера.
Источник данных
В src/main/liberty/config/server.xml перед webApplication опишите библиотеку и источник данных (data source) — пул подключений с именем:
<!-- src/main/liberty/config/server.xml (фрагмент) -->
<library id="h2Library">
<fileset dir="${server.config.dir}/jdbc" includes="h2-*.jar" />
</library>
<dataSource id="blogDataSource" jndiName="jdbc/blog">
<jdbcDriver libraryRef="h2Library" />
<properties URL="jdbc:h2:mem:blog;DB_CLOSE_DELAY=-1;MODE=LEGACY" user="sa" password="changeme" />
</dataSource>jndiName="jdbc/blog" — имя, по которому приложение найдёт базу. DB_CLOSE_DELAY=-1 не даёт базе в памяти исчезнуть между запросами. Про MODE=LEGACY — в конце урока.
persistence.xml
Файл src/main/resources/META-INF/persistence.xml описывает единицу хранения (persistence unit):
<!-- src/main/resources/META-INF/persistence.xml -->
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.2">
<persistence-unit name="blog" transaction-type="JTA">
<jta-data-source>jdbc/blog</jta-data-source>
<properties>
<property name="jakarta.persistence.schema-generation.database.action"
value="drop-and-create"/>
<property name="jakarta.persistence.sql-load-script-source"
value="META-INF/data.sql"/>
</properties>
</persistence-unit>
</persistence>При старте таблицы создаются заново, а затем выполняется src/main/resources/META-INF/data.sql — по одной команде на строку:
INSERT INTO posts (title, body, created_at) VALUES ('Привет, Jakarta EE', 'Первая запись блога.', '2026-10-01');
INSERT INTO posts (title, body, created_at) VALUES ('Сервлеты и фильтры', 'Как запрос попадает в наш код.', '2026-10-02');Сущность
JPA не работает с записями (record): сущности нужны конструктор без параметров и изменяемые поля. Замените Post.java классом:
// src/main/java/com/example/blog/Post.java
package com.example.blog;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
@Column(length = 5000)
private String body;
@Column(name = "created_at")
private LocalDate createdAt;
protected Post() {
// нужен JPA
}
public Post(String title, String body, LocalDate createdAt) {
this.title = title;
this.body = body;
this.createdAt = createdAt;
}
public Long getId() { return id; }
public String getTitle() { return title; }
public String getBody() { return body; }
public LocalDate getCreatedAt() { return createdAt; }
public void setTitle(String title) { this.title = title; }
public void setBody(String body) { this.body = body; }
}JSON-B читает публичные геттеры, поэтому JSON останется прежним.
Репозиторий
Вместо PostStore создайте src/main/java/com/example/blog/PostRepository.java:
// src/main/java/com/example/blog/PostRepository.java
package com.example.blog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class PostRepository {
@PersistenceContext(unitName = "blog")
EntityManager em;
public List<Post> findAll() {
return em.createQuery("SELECT p FROM Post p ORDER BY p.id", Post.class)
.getResultList();
}
public Optional<Post> findById(long id) {
return Optional.ofNullable(em.find(Post.class, id));
}
public Post get(long id) {
return findById(id).orElseThrow(() -> new PostNotFoundException(id));
}
public List<Post> search(String text) {
return em.createQuery(
"SELECT p FROM Post p WHERE LOWER(p.title) LIKE :text ORDER BY p.id",
Post.class)
.setParameter("text", "%" + text.toLowerCase() + "%")
.getResultList();
}
public long count() {
return em.createQuery("SELECT COUNT(p) FROM Post p", Long.class)
.getSingleResult();
}
@Transactional
public Post save(Post post) {
em.persist(post);
return post;
}
@Transactional
public void delete(long id) {
em.remove(get(id));
}
}@PersistenceContext внедряет EntityManager — главный объект JPA. Запросы пишутся на JPQL: в нём фигурируют классы и поля (Post p, p.title), а не таблицы. Аннотация @Transactional нужна для записи в базу — ей посвящён следующий урок.
Остальной код
Удалите PostStore.java и замените его использование:
| Где | Было | Стало |
|---|---|---|
PostResource, StatsServlet | PostStore store | PostRepository repository |
PostService.publish | store.add(data) | repository.save(new Post(data.title(), data.body(), LocalDate.now(clock))) |
PostService | — | Поле @Inject Clock clock; |
| Везде | post.id(), post.title() | post.getId(), post.getTitle() |
StatsServlet | store.findAll().size() | repository.count() |
Метод списка в ресурсе научим искать:
// src/main/java/com/example/blog/PostResource.java (фрагмент)
@GET
public List<Post> list(@QueryParam("q") @DefaultValue("") String q) {
return q.isBlank() ? repository.findAll() : repository.search(q);
}curl "http://localhost:8080/blog/api/posts?q=серв"[{"body":"Как запрос попадает в наш код.","createdAt":"2026-10-02","id":2,"title":"Сервлеты и фильтры"}]
Частая ошибка
Подключить H2 2.x без MODE=LEGACY. EclipseLink — реализация JPA в Open Liberty — создаёт таблицу старым синтаксисом H2, и новая версия базы его не принимает:
Syntax error in SQL statement "CREATE TABLE posts (ID BIGINT [*]IDENTITY NOT NULL, ...
Таблица не создаётся, и первый же запрос падает с Table "POSTS" not found. Режим MODE=LEGACY в адресе базы включает совместимость. Предупреждения DROP TABLE posts при первом старте нормальны: при drop-and-create JPA пытается удалить таблицы, которых ещё нет.