Транзакции — это ключевой механизм для безопасной работы с базами данных. Давайте разберем на примере интернет-магазина, почему они так важны.
Представьте: покупатель оформляет заказ. Казалось бы, простая операция, но на самом деле нам нужно:
- Списать деньги со счета покупателя
- Зарезервировать товар на складе
- Создать запись о заказе в базе
Что может пойти не так? Да почти всё! Например, деньги списались, а сервер "упал" до создания заказа. Без транзакций это превратится в настоящий кошмар для службы поддержки. К счастью, транзакции решают эту проблему: либо все операции проходят успешно, либо система автоматически всё откатывает назад.
Настройка 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. Это значит, что мы уже готовы работать с транзакциями. Ура!
Реализация транзакции: создание примера приложения
Теперь создадим небольшое приложение, моделирующее регистрацию заказа в интернет-магазине. Наши задачи:
- Сохранить информацию о заказе.
- Обновить количество доступных товаров.
- Если что-то идёт не так, откатить все изменения.
Создаём сущности
Начнём с базы данных и сущностей. У нас будут две таблицы: 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
Теперь самое интересное! Создадим сервис, который:
- Зарегистрирует заказ.
- Откатит изменения, если товара недостаточно на складе.
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;
}
}
Итоги
Теперь у нас есть транзакционный сервис, который:
- Гарантирует целостность данных.
- Автоматически откатывает изменения при ошибках.
- Легко тестируется.
На практике такие подходы делают приложения надёжными и предсказуемыми. И да, не забудьте поставить себе звезду за сегодняшний прогресс!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ