Статьи Генератор паролей UUID MD5 Unix-время URL-encode Base64 Форматирование XML Ваш внешний IP Число прописью


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

Вернуться назад

22 февраля 2020

Тэги: Spring Spring Data Java 10

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

Spring предлагает очень простой декларативный способ управления транзакциями. Вам достаточно добавить @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() {
        // работа с БД
    }
}