Encodera

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

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

Оглавление

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_DROP

schema-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, как и советует сообщение.

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

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