Encodera

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

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

Оглавление

Транзакции

Транзакция — группа изменений в базе, которая выполняется целиком или не выполняется вовсе. Если на середине что-то пошло не так, база возвращается в состояние до начала — это называется откатом (rollback). Классический пример: импорт десяти постов, в котором пятый оказался ошибочным. Без транзакции четыре поста уже сохранены, а остальные нет — данные в полусломанном состоянии. С транзакцией не сохранится ни один.

@Transactional

В Quarkus транзакцию открывают аннотацией @Transactional на методе бина. Транзакция начинается при входе в метод и фиксируется (commit) при выходе. Если из метода вылетело непроверяемое исключение (RuntimeException), все изменения откатываются.

Создадим сервис импорта, который сохраняет список постов и отказывается от повторяющихся заголовков:

// src/main/java/com/example/blog/PostService.java
package com.example.blog;
 
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
 
import java.util.ArrayList;
import java.util.List;
 
@ApplicationScoped
public class PostService {
 
    @Transactional
    public List<Post> importAll(List<NewPost> newPosts) {
        List<Post> saved = new ArrayList<>();
        for (NewPost newPost : newPosts) {
            if (Post.titleExists(newPost.title())) {
                throw new DuplicateTitleException(newPost.title());
            }
            Post post = new Post();
            post.title = newPost.title();
            post.body = newPost.body();
            post.persist();
            saved.add(post);
        }
        return saved;
    }
}
// src/main/java/com/example/blog/DuplicateTitleException.java
package com.example.blog;
 
public class DuplicateTitleException extends RuntimeException {
 
    public DuplicateTitleException(String title) {
        super("Пост с заголовком '" + title + "' уже есть");
    }
}

Метод titleExists мы написали в уроке о запросах. Исключению нужен обработчик со статусом 409 Conflict — добавьте его в ExceptionMappers:

// src/main/java/com/example/blog/ExceptionMappers.java
@ServerExceptionMapper
public RestResponse<ErrorMessage> mapDuplicate(DuplicateTitleException e) {
    return RestResponse.status(Response.Status.CONFLICT, new ErrorMessage(e.getMessage()));
}

И эндпоинт в ресурсе:

// src/main/java/com/example/blog/PostResource.java
@Inject
PostService postService;
 
@POST
@Path("/import")
public List<Post> importPosts(List<@Valid NewPost> posts) {
    return postService.importAll(posts);
}

List<@Valid NewPost> проверяет каждый элемент списка. Сам эндпоинт без @Transactional: транзакцию открывает сервис.

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

Сначала импортируем два новых поста — получится четыре:

curl -X POST http://localhost:8080/posts/import -H "Content-Type: application/json" \
     -d '[{"title":"Part 1","body":"a"},{"title":"Part 2","body":"b"}]'
curl http://localhost:8080/posts/count

Ответ — список из двух сохранённых постов и число 4. Теперь импортируем новый пост и пост с уже существующим заголовком:

curl -i -X POST http://localhost:8080/posts/import -H "Content-Type: application/json" \
     -d '[{"title":"Part 3","body":"c"},{"title":"Part 1","body":"d"}]'
curl http://localhost:8080/posts/count
HTTP/1.1 409 Conflict
Content-Type: application/json;charset=UTF-8

{"error":"Пост с заголовком 'Part 1' уже есть"}
4

Пост Part 3 был сохранён через persist() до ошибки, но после исключения транзакция откатилась, и постов по-прежнему четыре.

Программные транзакции

Иногда транзакция нужна только для части метода. Для этого есть QuarkusTransaction из пакета io.quarkus.narayana.jta:

// src/main/java/com/example/blog/PostService.java
public void rename(long id, String title) {
    QuarkusTransaction.requiringNew().run(() -> {
        Post post = Post.findById(id);
        post.title = title;
    });
}

Изменение поля сущности внутри транзакции сохраняется само, вызывать persist() для уже сохранённого объекта не нужно.

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

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

Вторая ловушка — @Transactional на private-методе. Quarkus не может перехватить такой вызов и останавливает сборку с понятным сообщением:

DeploymentException: @Transactional will have no effect on method com.example.blog.PostService.importAll()
because the method is private. Either remove the annotation from the method, or turn this exception into
a simple warning by setting configuration property 'quarkus.arc.fail-on-intercepted-private-method' to 'false'.

Сделайте метод публичным или пакетным.

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

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