Статьи Утилиты Telegram YouTube RuTube Отзывы

Управление транзакциями в Spring

22 февраля 2020

Тэги: Java, Spring, Spring Data.

Транзакцией называется набор связанных операций, все из которых должны быть выполнены корректно без ошибок. Если при выполнении одной из операций возникла ошибка, все остальные должны быть отменены. Прежде всего такой механизм применяется при работе с БД.

Spring предлагает очень простой декларативный способ управления транзакциями. Вам достаточно добавить @org.springframework.transaction.annotation.Transactional к публичному сервисному методу, и все операции внутри этого метода будут выполняться в транзакции. При выходе из метода транзакция будет завершена (операция commit в терминах БД) автоматически. Если в процессе работы возникнет исключение и оно не будет перехвачено внутри метода, транзакция будет отменена (операция rollback) и все данные вернуться в то состояние, в котором они были до начала транзакции.

Предположим, у нас есть метод в спринговом сервисе, который выполняет несколько запросов в БД. Для простоты можно использовать Spring Data, чтобы оперировать записями в БД в ООП стиле.

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

@Entity
public class Record {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;

    // get- и set-методы...
}

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

public interface ExampleDao extends CrudRepository<Record, Integer> {
}

Теперь создадим метод в сервисном слое, который сначала создаёт сущность, сохраняет её в БД, затем обновляет её имя. То есть сначала в БД происходит запрос insert, затем update.

@Service
public class ExampleService {

    private final ExampleDao exampleDao;

    public ExampleService(ExampleDao exampleDao) {
        this.exampleDao = exampleDao;
    }

    public void doTransaction() {
        var record = new Record();
        record.setName("created");
        exampleDao.save(record); // insert
        record.setName("updated");
        exampleDao.save(record); // update
    }
}

Сервисный метод вызывается из контроллера при помощи POST-запроса:

@RestController
public class ExampleController {

    private final ExampleService exampleService;

    public ExampleController(ExampleService exampleService) {
        this.exampleService = exampleService;
    }

    @PostMapping
    public void doTransaction() {
        exampleService.doTransaction();
    }
}

Данный код всегда работает без ошибок. А теперь добавим между созданием и обновлением записи в БД ошибку.

public void doTransaction() {
    var record = new Record();
    record.setName("created");
    exampleDao.save(record);
    if (record.getId() > 0) {
        throw new RuntimeException();
    }
    record.setName("updated");
    exampleDao.save(record);
}

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

@Transactional
public void doTransaction() {
    var record = new Record();
    record.setName("created");
    exampleDao.save(record);
    if (record.getId() > 0) {
        throw new RuntimeException();
    }
    record.setName("updated");
    exampleDao.save(record);
}

В данном случае первый запрос будет выполнен, а потом отменён и в таблице в БД новых записей так и не появится.

Бывают такие ошибки, при которых откатывать транзакцию не требуется. Эти исключения вы можете перечислить при помощи параметра dontRollbackOn:

@Transactional(dontRollbackOn = {RuntimeException.class})

Если мы сделаем так для нашего примера, то запись всё-таки будет создана, затем возникнет исключение, обновления уже не произойдёт, но транзакция всё равно будет завершена.

Как Spring реализует механизм создания транзакций? Важно отметить, что @Transactional будет работать только для публичных методов, вызываемых из других компонентов. Это происходит потому что спринг делает вызов целевого метода через прокси-объект, имеющий такой же интерфейс, как и наш сервис. В этом прокси-объекте происходит открытие транзакции перед вызовом целевого метода. Затем, после завершения целевого метода, происходит закрытие транзакции. Если из метода вылетит исключение, транзакция будет отменена. Это поведение похоже на конструкцию try-catch. Можно представлять себе логику работы прокси так:

try {
    // открытие транзакции
    // вызов целевого метода
    // commit
} catch (Exception e) {
    // rollback
}

Если вы повесите @Transactional на какой-либо метод и вызовете его из того же сервиса, но из другого метода, механизм работать не будет, т.к. вызов не проходит через прокси. Это надо иметь в виду.

@Service
public class ExampleService {

    public void doWrongTransaction() {
        doTransaction(); // так транзакция работать не будет!
    }

    @Transactional
    public void doTransaction() {
        // работа с БД
    }
}

См. также


Комментарии

29.06.2021 21:28 Царский Евгений

Хотелось бы прояснить последний момент: "Если вы повесите @Transactional на какой-либо метод и вызовете его из того же сервиса, но из другого метода, механизм работать не будет, т.к. вызов не проходит через прокси". Что-то я тут не понял, ну так мы вызываем методы сервиса из контроллера, методы в контроллере не аннотированы @Transactional, а транзакция открывается. Или имеется в виду, что транзакция не будет открыта в том методе, который не аннотирован, даже если внутри него вызвать аннотированный метод (разумеется, сам аннотированный метод для себя откроет свою транзакцию)?

29.06.2021 22:17 devmark

Речь тут идёт о том, что аннотация @Transactional на методе будет проигнорирована, если этот метод вызвать из того же класса, где он находится. Для работы транзакции нужно обязательно делать вызов из другого бина. Что мы и делаем, вызывая сервисный метод из контроллера.

20.09.2021 11:08 Александр

Идеально подана информация. Кратко и по делу. Спасибо!

13.06.2022 13:04 Евгений

Действительно все понятно, без лишней воды, спасибо!

18.07.2022 19:45 Владислав

Даа, именно эта аналогия с Try - catch далма мне понимание того, что здесь происходит, спасибо!)

Добавить комментарий

×

devmark.ru