Encodera

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

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

Оглавление

Транзакции и сброс изменений

Транзакция объединяет несколько изменений, чтобы они сохранились все вместе или не сохранились вовсе. Сброс (flush) — момент, когда Hibernate превращает накопленные в контексте персистентности изменения в команды insert, update и delete. Это разные вещи: сброс отправляет SQL, но постоянными и видимыми для других изменения делает только фиксация (commit).

Полный шаблон

// src/main/java/com/example/blog/Main.java
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
    tx.begin();
    em.persist(new Post("Транзакции", null));
    em.persist(new Post("Сброс изменений", null));
    IO.println("persist() вызван дважды");
 
    Long count = em.createQuery("select count(p) from Post p", Long.class).getSingleResult();
    IO.println("Количество внутри транзакции: " + count);
 
    tx.commit();
    IO.println("Зафиксировано");
} catch (RuntimeException e) {
    if (tx.isActive()) {
        tx.rollback();
    }
    throw e;
} finally {
    em.close();
}

EntityTransaction импортируется из jakarta.persistence. Вывод:

Hibernate: select next value for Post_SEQ
Hibernate: select next value for Post_SEQ
persist() вызван дважды
Hibernate: insert into Post (author_id,title,views,id) values (?,?,?,?)
Hibernate: insert into Post (author_id,title,views,id) values (?,?,?,?)
Hibernate: select count(p1_0.id) from Post p1_0
Количество внутри транзакции: 2
Зафиксировано

Когда происходит сброс

Команды insert отправил не persist, а момент прямо перед запросом count. В режиме по умолчанию FlushModeType.AUTO Hibernate сбрасывает изменения:

  • перед запросом, результат которого может зависеть от несохранённых изменений;
  • при commit();
  • когда вы сами вызываете em.flush().

Благодаря этому запрос посчитал обе новые статьи, хотя ничего ещё не зафиксировано. Другие соединения этих строк до фиксации не видят.

Откат

runInTransaction работает по тому же шаблону: если лямбда бросает исключение, транзакция откатывается, а исключение пробрасывается дальше.

// src/main/java/com/example/blog/Rollback.java
try {
    emf.runInTransaction(em -> {
        em.persist(new Post("Недоделанная", null));
        em.flush();
        throw new IllegalStateException("Что-то пошло не так");
    });
} catch (IllegalStateException e) {
    IO.println("Ошибка: " + e.getMessage());
}
 
Long count = emf.callInTransaction(em ->
        em.createQuery("select count(p) from Post p", Long.class).getSingleResult());
IO.println("Статей в базе: " + count);
Hibernate: select next value for Post_SEQ
Hibernate: insert into Post (author_id,title,views,id) values (?,?,?,?)
Ошибка: Что-то пошло не так
Hibernate: select count(p1_0.id) from Post p1_0
Статей в базе: 0

Из-за em.flush() команда insert действительно дошла до базы, но откат её отменил. Сброс — это ещё не фиксация.

Держите транзакции короткими

Транзакция занимает соединение с базой и, возможно, блокировки строк. Не вызывайте внутри неё медленные внешние сервисы и не ждите ввода пользователя. В Spring Boot метод сервиса помечают @Transactional, и Spring выполняет вокруг него ровно этот шаблон «начать — зафиксировать — откатить».

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

Если исключение поймано, а транзакция не откачена, она остаётся открытой. Следующий begin() на том же EntityManager падает:

// src/main/java/com/example/blog/Main.java
try {
    em.getTransaction().begin();
    em.persist(new Post("Черновик", null));
    throw new IllegalStateException("Что-то пошло не так");
} catch (IllegalStateException e) {
    IO.println("Ошибка: " + e.getMessage());
}
em.getTransaction().begin();
Ошибка: Что-то пошло не так
Exception in thread "main" java.lang.IllegalStateException: Resource-local transaction already active

Всегда откатывайте транзакцию в catch, как в полном шаблоне выше, или используйте runInTransaction, который делает это за вас.

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

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