Micronaut Data JDBC и H2
Статьи в памяти пропадают при каждом перезапуске. Пора сохранять их в базу данных. Micronaut Data — модуль доступа к данным: вы описываете интерфейс репозитория, а реализацию с готовыми SQL-запросами обработчик аннотаций генерирует при компиляции. В курсе используется вариант Micronaut Data JDBC — он работает поверх обычного JDBC, без Hibernate. База — H2: она живёт в памяти и не требует установки.
Зависимости
Те же изменения покажет mn feature-diff --features=data-jdbc,h2. Добавьте в <dependencies>:
<!-- pom.xml -->
<dependency>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-jdbc</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>В <annotationProcessorPaths> добавьте <path> с io.micronaut.data:micronaut-data-processor и версией ${micronaut.data.version} — точно так же, как обработчик валидации в прошлой главе. micronaut-jdbc-hikari даёт пул соединений HikariCP.
Настройки источника данных
# src/main/resources/application.properties
datasources.default.url=jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
datasources.default.driver-class-name=org.h2.Driver
datasources.default.username=sa
datasources.default.password=
datasources.default.dialect=H2
datasources.default.schema-generate=CREATE_DROPschema-generate=CREATE_DROP создаёт таблицы по сущностям при запуске и удаляет при остановке — удобно для учёбы. В настоящем проекте схему ведут миграциями (возможности flyway или liquibase).
Сущность
Запись Article становится строкой таблицы article:
// src/main/java/com/example/Article.java
package com.example;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;
@Serdeable
@MappedEntity
public record Article(
@Id @GeneratedValue @Nullable Long id,
String title,
String body) {
}@MappedEntity связывает запись с таблицей, @Id отмечает первичный ключ, @GeneratedValue — что его назначает база. @Nullable нужен, потому что у новой статьи id ещё нет.
Репозиторий
// src/main/java/com/example/ArticleRepository.java
package com.example;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
@JdbcRepository(dialect = Dialect.H2)
public interface ArticleRepository extends CrudRepository<Article, Long> {
}Метод CrudRepository | Что делает |
|---|---|
save(entity) | INSERT, возвращает запись с новым id |
update(entity) | UPDATE по id |
findById(id) | Optional с записью |
findAll() | Список всех записей |
existsById(id), count() | Проверка и подсчёт |
deleteById(id) | DELETE |
Сервис на репозитории
Сервис больше не хранит Map — он получает репозиторий через конструктор:
// src/main/java/com/example/ArticleService.java
@Singleton
public class ArticleService {
private final ArticleRepository articleRepository;
public ArticleService(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
public List<Article> findAll() {
return articleRepository.findAll();
}
public Optional<Article> findById(Long id) {
return articleRepository.findById(id);
}
public Article create(ArticleForm form) {
return articleRepository.save(new Article(null, form.title(), form.body()));
}
}Методы update и delete устроены так же: они вызывают articleRepository.update(...) и articleRepository.deleteById(id), а в контроллере появляются маршруты @Put("/{id}") и @Delete("/{id}").
Запросы к базе блокируют поток, а Micronaut по умолчанию обрабатывает запросы в потоках цикла событий Netty. Поэтому над ArticleController добавьте @ExecuteOn(TaskExecutors.BLOCKING) (пакет io.micronaut.scheduling): методы контроллера будут выполняться в отдельном пуле — на Java 25 это виртуальные потоки.
Проверка
Чтобы видеть SQL, добавьте в src/main/resources/logback.xml строку <logger name="io.micronaut.data.query" level="debug"/>. При запуске в журнале появится:
DEBUG io.micronaut.data.query - Executing CREATE statement:
CREATE TABLE `article` (`id` BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,`title` VARCHAR(255) NOT NULL,`body` VARCHAR(255) NOT NULL);POST /articles из прошлых уроков теперь выполняет INSERT INTO article, а ответ {"id":1,"title":"Привет, Micronaut","body":"Первая статья"} приходит с номером из базы.
Частая ошибка
Если обработчик micronaut-data-processor не подключён, приложение запустится, но первый запрос упадёт:
ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Micronaut Data method is missing compilation time query information.
Ensure that the Micronaut Data annotation processors are declared in your build and try again with a clean re-build.Добавьте обработчик и выполните ./mvnw clean package, как и советует сообщение.