Encodera

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

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

Оглавление

Транзакции

Транзакция — группа запросов к базе, которая выполняется целиком или не выполняется вовсе. Пример из блога: публикация статьи вместе с первым комментарием. Если статья сохранилась, а комментарий — нет, в базе останется половина операции. В уроке по 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. Транзакционный метод делайте открытым и вызывайте из другого бина.

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

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