JavaRush /Курсы /Модуль 5. Spring /Практика: Реализация транзакций в Spring-приложении

Практика: Реализация транзакций в Spring-приложении

Модуль 5. Spring
6 уровень , 5 лекция
Открыта

Транзакции — это ключевой механизм для безопасной работы с базами данных. Давайте разберем на примере интернет-магазина, почему они так важны.

Представьте: покупатель оформляет заказ. Казалось бы, простая операция, но на самом деле нам нужно:

  1. Списать деньги со счета покупателя
  2. Зарезервировать товар на складе
  3. Создать запись о заказе в базе

Что может пойти не так? Да почти всё! Например, деньги списались, а сервер "упал" до создания заказа. Без транзакций это превратится в настоящий кошмар для службы поддержки. К счастью, транзакции решают эту проблему: либо все операции проходят успешно, либо система автоматически всё откатывает назад.


Настройка TransactionManager

Прежде чем писать код, убедимся, что наше приложение готово к управлению транзакциями. TransactionManager — это сердце транзакционного механизма в Spring. Он координирует выполнение операций.

Если вы используете JPA/Hibernate (что мы и сделаем), то настройка TransactionManager выглядит просто.

Добавляем зависимости

Первое, что сделаем — включим зависимости для работы с JPA и базы данных (например, H2 Database для тестов). Убедитесь, что в pom.xml указан следующий код:


<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

Конфигурация базы данных

В файле application.properties пропишем настройки для подключения к H2:


spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Конфигурация TransactionManager

Spring Boot настроит TransactionManager автоматически, если вы используете spring-boot-starter-data-jpa. Это значит, что мы уже готовы работать с транзакциями. Ура!


Реализация транзакции: создание примера приложения

Теперь создадим небольшое приложение, моделирующее регистрацию заказа в интернет-магазине. Наши задачи:

  1. Сохранить информацию о заказе.
  2. Обновить количество доступных товаров.
  3. Если что-то идёт не так, откатить все изменения.

Создаём сущности

Начнём с базы данных и сущностей. У нас будут две таблицы: Order и Product.

Сущность Product


import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Product {

    @Id
    private Long id;
    private String name;
    private int stock; // Количество на складе

    // Геттеры и сеттеры (можно использовать Lombok)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }
}

Сущность Order


import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long productId;
    private int quantity;

    // Геттеры и сеттеры
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getProductId() {
        return productId;
    }

    public void setProductId(Long productId) {
        this.productId = productId;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

Создаём репозитории

Теперь создадим репозитории для работы с нашими сущностями. Мы будем использовать JpaRepository.

ProductRepository


import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
    // Никаких дополнительных методов пока не нужно
}

OrderRepository


import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
    // Никаких дополнительных методов пока не нужно
}

Управление транзакцией через @Transactional

Теперь самое интересное! Создадим сервис, который:

  1. Зарегистрирует заказ.
  2. Откатит изменения, если товара недостаточно на складе.

OrderService


import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final ProductRepository productRepository;
    private final OrderRepository orderRepository;

    public OrderService(ProductRepository productRepository, OrderRepository orderRepository) {
        this.productRepository = productRepository;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void placeOrder(Long productId, int quantity) {
        // 1. Получаем товар
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("Product not found!"));

        // 2. Проверяем наличие товара на складе
        if (product.getStock() < quantity) {
            throw new RuntimeException("Not enough stock for product!");
        }

        // 3. Уменьшаем количество на складе
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);

        // 4. Создаём и сохраняем заказ
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        // Сохраняем изменения только если всё прошло успешно
        System.out.println("Order placed successfully!");
    }
}

Что здесь делает аннотация @Transactional? Она гарантирует, что все изменения либо зафиксируются, либо откатятся, если произойдёт ошибка. Например, если товаров на складе недостаточно, метод выбрасывает исключение, и изменения откатываются.


Тестирование транзакций

Давайте протестируем, как это работает! Напишем простой тестовый класс.


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;

@SpringBootTest
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    public void testPlaceOrder_Success() {
        // Создаём тестовый продукт
        Product product = new Product();
        product.setId(1L);
        product.setName("Test Product");
        product.setStock(10);
        productRepository.save(product);

        // Создаём заказ
        orderService.placeOrder(1L, 2);

        // Проверяем количество товара на складе
        Product updatedProduct = productRepository.findById(1L).get();
        assert updatedProduct.getStock() == 8;
    }

    @Test
    public void testPlaceOrder_NotEnoughStock() {
        // Создаём тестовый продукт
        Product product = new Product();
        product.setId(2L);
        product.setName("Another Product");
        product.setStock(5);
        productRepository.save(product);

        try {
            // Пытаемся заказать больше, чем есть на складе
            orderService.placeOrder(2L, 10);
        } catch (Exception e) {
            System.out.println(e.getMessage()); // Expect "Not enough stock for product!"
        }

        // Проверяем, что товар не изменился
        Product updatedProduct = productRepository.findById(2L).get();
        assert updatedProduct.getStock() == 5;
    }
}

Итоги

Теперь у нас есть транзакционный сервис, который:

  • Гарантирует целостность данных.
  • Автоматически откатывает изменения при ошибках.
  • Легко тестируется.

На практике такие подходы делают приложения надёжными и предсказуемыми. И да, не забудьте поставить себе звезду за сегодняшний прогресс!

Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Александр Уровень 112
10 ноября 2025
Возможно, меня закидают тухлыми яйцами и гнилыми помидорами... Но в предложенном варианте у меня ни приложение, ни тесты не запускались с ошибкой: Syntax error in SQL statement "create table [*]order (id bigint generated by default as identity, product_id bigint, quantity integer not null, primary key (id))"; expected "identifier";] Удалось победить добавлением @Table(name = "tbl_order") в классе Order (то есть чтобы у таблицы в БД было любое другое имя, отличное от Order).
12 ноября 2025
у меня то же самое, все дело в том что ORDER — это зарезервированное слово во всех SQL-диалектах (в том числе в H2, PostgreSQL, MySQL и др.), так как используется в конструкции ORDER BY. тут два варианта: переименовать таблицу, например как выше указано, или экранировать имя таблицы: @Table(name = "`order`") (вариант для H2, MySQL)