Encodera

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

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

Оглавление

Транзакции

Транзакция — это группа действий с базой, которая выполняется целиком или не выполняется вовсе. Если на середине случилась ошибка, база откатывается к состоянию до начала группы. В учебнике вы управляли транзакциями через JDBC: setAutoCommit(false), commit, rollback. В Spring достаточно аннотации @Transactional над методом сервиса.

Аннотации в сервисе

// src/main/java/com/example/blog/PostService.java
@Service
@Transactional(readOnly = true)
public class PostService {
 
    // ...
 
    @Transactional
    public PostResponse create(NewPostRequest request) {
        User author = users.findById(request.authorId())
                .orElseThrow(() -> new IllegalArgumentException("Неизвестный автор " + request.authorId()));
        Post post = new Post(request.title(), request.body(), author, Instant.now(clock));
        return PostResponse.from(posts.save(post));
    }
}

Аннотация над классом действует на все его публичные методы: каждый работает в транзакции «только для чтения» — база и Hibernate могут её ускорить. Методы, которые меняют данные, помечены просто @Transactional: аннотация над методом важнее аннотации над классом. Обработчик IllegalArgumentException с кодом 400 добавьте в ApiExceptionHandler так же, как обработчик для PostNotFoundException.

Всё или ничего

Добавим создание нескольких статей одним запросом:

// src/main/java/com/example/blog/PostService.java
@Transactional
public List<PostResponse> createAll(List<NewPostRequest> requests) {
    return requests.stream().map(this::create).toList();
}
// src/main/java/com/example/blog/PostController.java
@PostMapping("/batch")
@ResponseStatus(HttpStatus.CREATED)
public List<PostResponse> createAll(@RequestBody List<@Valid NewPostRequest> requests) {
    return postService.createAll(requests);
}

Отправим две статьи, у второй — несуществующий автор:

curl -X POST http://localhost:8080/api/posts/batch -H "Content-Type: application/json" \
  -d '[{"title":"Один","body":"1","authorId":1},{"title":"Два","body":"2","authorId":42}]'
{"detail":"Неизвестный автор 42","instance":"/api/posts/batch","status":400,"title":"Bad Request"}

Первая статья успела записаться в базу, но исключение на второй откатило всю транзакцию. curl http://localhost:8080/api/posts покажет прежнее число статей — "totalElements":1. С правильными авторами придут обе статьи, причём с номерами 3 и 4: номер 2 база выдала откаченной вставке, и он не возвращается.

Изменения без save

Внутри транзакции Hibernate следит за загруженными сущностями. Если поменять поле, при завершении транзакции он сам выполнит update:

// src/main/java/com/example/blog/PostService.java
@Transactional
public PostResponse update(Long id, NewPostRequest request) {
    Post post = getPost(id);
    post.setTitle(request.title());
    post.setBody(request.body());
    return PostResponse.from(post);
}

Вызова save здесь нет, но в журнале видно:

Hibernate: select p1_0.id,p1_0.author_id,p1_0.body,p1_0.created_at,p1_0.title from posts p1_0 where p1_0.id=?
Hibernate: select u1_0.id,u1_0.username from users u1_0 where u1_0.id=?
Hibernate: update posts set author_id=?,body=?,created_at=?,title=? where id=?

Этот механизм называется отслеживанием изменений (dirty checking).

Правила @Transactional

ПравилоЧто значит
Ставьте на сервисТранзакция охватывает всю операцию, а не один запрос
Работает только на публичных методах бинаSpring оборачивает бин посредником (proxy)
Откат — при непроверяемых исключенияхRuntimeException и наследники; для проверяемых нужно rollbackFor
Вызов из того же класса идёт мимо посредникаАннотация на вызываемом методе в этом случае не действует

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

Транзакция откатывается только при непроверяемых исключениях. Если метод сервиса бросит проверяемое, например IOException при чтении файла, Spring зафиксирует всё, что успело записаться, и в базе останутся половинчатые данные. Либо бросайте наследников RuntimeException, как PostNotFoundException, либо укажите откат явно: @Transactional(rollbackFor = Exception.class).

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

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