Encodera

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

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

Оглавление

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, StatsServletPostStore storePostRepository repository
PostService.publishstore.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()
StatsServletstore.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 пытается удалить таблицы, которых ещё нет.

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

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