Транзакции
Транзакция — группа запросов к базе, которая выполняется целиком или не выполняется вовсе. Пример из блога: публикация статьи вместе с первым комментарием. Если статья сохранилась, а комментарий — нет, в базе останется половина операции. В уроке по JDBC вы управляли этим вручную через commit() и rollback(). В Micronaut достаточно аннотации @Transactional на методе бина.
Метод в транзакции
Добавьте в ArticleService репозиторий комментариев (вторым параметром конструктора) и новый метод:
// src/main/java/com/example/ArticleService.java
import jakarta.transaction.Transactional;
@Transactional
public Article publish(ArticleForm form, String firstComment) {
Article article = articleRepository.save(new Article(null, form.title(), form.body()));
if (firstComment.isBlank()) {
throw new IllegalArgumentException("Первый комментарий пустой");
}
commentRepository.save(new Comment(null, article.id(), "Anna", firstComment));
return article;
}Перед вызовом метода Micronaut открывает транзакцию. Если метод завершился нормально — фиксирует её. Если вылетело непроверяемое исключение (наследник RuntimeException) — откатывает: статья, сохранённая в первой строке, исчезнет. Аннотация jakarta.transaction.Transactional уже доступна вместе с micronaut-data-jdbc.
Контроллер публикации
// src/main/java/com/example/PublishController.java
package com.example;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.serde.annotation.Serdeable;
@ExecuteOn(TaskExecutors.BLOCKING)
@Controller("/publish")
public class PublishController {
@Serdeable
public record PublishForm(String title, String body, String firstComment) {
}
private final ArticleService articleService;
public PublishController(ArticleService articleService) {
this.articleService = articleService;
}
@Post
public Article publish(@Body PublishForm form) {
return articleService.publish(new ArticleForm(form.title(), form.body()), form.firstComment());
}
}Проверка
Сначала успешная публикация:
curl -X POST http://localhost:8080/publish -H "Content-Type: application/json" \
-d '{"title":"Транзакции","body":"Четвёртая статья","firstComment":"Добро пожаловать"}'{"id":4,"title":"Транзакции","body":"Четвёртая статья"}Теперь с пустым комментарием:
curl -X POST http://localhost:8080/publish -H "Content-Type: application/json" \
-d '{"title":"Потерянная статья","body":"Будет отменена","firstComment":" "}'Ответ — 500, а журнал SQL показывает, что INSERT в таблицу article был выполнен:
DEBUG io.micronaut.data.query - Executing SQL query: INSERT INTO `article` (`title`,`body`) VALUES (?,?)
ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Первый комментарий пустойНо curl http://localhost:8080/articles «Потерянную статью» не покажет: транзакция откатилась. В настоящем API для такой ошибки стоит написать ExceptionHandler и вернуть 400.
Полезные варианты
| Запись | Что значит |
|---|---|
@Transactional | Открыть транзакцию или присоединиться к уже открытой |
@Transactional(rollbackOn = IOException.class) | Откатывать и при этом проверяемом исключении |
@Transactional(Transactional.TxType.REQUIRES_NEW) | Всегда новая, отдельная транзакция |
@ReadOnly | Только чтение (пакет io.micronaut.transaction.annotation) |
Каждый метод репозитория сам по себе уже выполняется в транзакции. @Transactional нужен, когда несколько вызовов должны стать одним целым.
Частая ошибка
Аннотация работает через перехватчик, который Micronaut генерирует при компиляции в подклассе бина. Закрытый метод переопределить нельзя, поэтому @Transactional на private-методе — ошибка сборки:
[ERROR] ArticleService.java:[50,18] Method annotated as executable but is declared private.
Change the method to be non-private in order for AOP advice to be applied.По той же причине перехватчик не срабатывает, если метод вызван изнутри того же класса через this. Транзакционный метод делайте открытым и вызывайте из другого бина.