Encodera

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

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

Оглавление

Транзакции и @Transactional

Транзакция — группа изменений в базе, которая выполняется целиком или не выполняется вовсе. Если импорт десяти записей упал на седьмой, в базе не должно остаться шести «половинчатых». В Jakarta EE транзакциями управляет сервер (JTA — Jakarta Transactions): вы ставите на метод аннотацию @Transactional, а сервер открывает транзакцию перед вызовом и фиксирует или откатывает её после.

Как это работает

Что произошло в методеЧто сделает сервер
Метод завершился нормальноФиксирует транзакцию (commit)
Выброшено непроверяемое исключение (RuntimeException)Откатывает транзакцию (rollback)
Выброшено проверяемое исключение (Exception)Фиксирует, если не указано иное

@Transactional работает только на бинах CDI и только при вызове снаружи: вызов метода из того же объекта (this.save(...)) мимо прокси транзакцию не откроет.

Импорт записей

Добавим в сервис импорт сразу нескольких записей. Полный src/main/java/com/example/blog/PostService.java:

// src/main/java/com/example/blog/PostService.java
package com.example.blog;
 
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.Clock;
import java.time.LocalDate;
import java.util.List;
 
@ApplicationScoped
public class PostService {
 
    @Inject
    PostRepository repository;
 
    @Inject
    Event<PostCreated> postCreated;
 
    @Inject
    Clock clock;
 
    public Post publish(NewPost data) {
        Post post = repository.save(new Post(data.title(), data.body(), LocalDate.now(clock)));
        PostCreated event = new PostCreated(post.getId(), post.getTitle());
        postCreated.fire(event);
        postCreated.fireAsync(event);
        return post;
    }
 
    @Transactional
    public int importPosts(List<NewPost> items) {
        for (NewPost item : items) {
            if (item.title() == null || item.title().isBlank()) {
                throw new IllegalArgumentException("Запись без заголовка в импорте");
            }
            repository.save(new Post(item.title(), item.body(), LocalDate.now(clock)));
        }
        return items.size();
    }
}

importPosts открывает транзакцию. Метод repository.save тоже помечен @Transactional, но по умолчанию он присоединяется к уже открытой транзакции, а не создаёт свою. Поэтому все записи импорта сохраняются вместе.

Ресурс

// src/main/java/com/example/blog/ImportResult.java
package com.example.blog;
 
public record ImportResult(int imported) {
}
// src/main/java/com/example/blog/PostResource.java (фрагмент)
@POST
@Path("/import")
@Consumes(MediaType.APPLICATION_JSON)
public ImportResult importPosts(List<NewPost> items) {
    return new ImportResult(service.importPosts(items));
}

Проверка отката

Сначала успешный импорт двух записей, затем импорт, где у второй записи пустой заголовок:

curl http://localhost:8080/blog/stats
curl -H "Content-Type: application/json" -d '[{"title":"Импорт один","body":"Текст первой записи"},{"title":"Импорт два","body":"Текст второй записи"}]' http://localhost:8080/blog/api/posts/import
curl http://localhost:8080/blog/stats
curl -i -H "Content-Type: application/json" -d '[{"title":"Импорт три","body":"Текст третьей записи"},{"title":"","body":"Без заголовка"}]' http://localhost:8080/blog/api/posts/import
curl http://localhost:8080/blog/stats
Записей в блоге: 2
{"imported":2}
Записей в блоге: 4
HTTP/1.1 500 Internal Server Error

Запись без заголовка в импорте
Записей в блоге: 4

Запись «Импорт три» успела пройти через save, но после исключения транзакция откатилась, и в базе её нет — счётчик остался 4. В настоящем API это исключение стоит превратить в ответ 400 обработчиком, как в главе про REST.

Настройки аннотации

ПараметрЧто меняет
value = TxType.REQUIREDПо умолчанию: присоединиться к транзакции или открыть новую
TxType.REQUIRES_NEWВсегда новая транзакция, внешняя приостанавливается
TxType.MANDATORYТранзакция обязана быть, иначе исключение
TxType.NEVERТранзакции быть не должно
rollbackOn = IOException.classОткатывать и при этом проверяемом исключении
dontRollbackOn = ...Не откатывать при этом исключении

Например, @Transactional(TxType.REQUIRES_NEW) подходит для журнала аудита: запись в журнал должна сохраниться, даже если основная операция откатится.

Частая ошибка

Вызвать persist без транзакции. Если убрать @Transactional у PostRepository.save, создание записи падает:

jakarta.persistence.TransactionRequiredException: No active transaction for PuId=blog#blog.war#blog

Читать данные можно без транзакции, а изменять — нет: persist, merge и remove требуют, чтобы метод выше по цепочке вызовов был помечен @Transactional.

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

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