Транзакції — це ключовий механізм для безпечної роботи з базами даних. Давай розберемося на прикладі інтернет‑магазину, чому вони такі важливі.
Уяви: покупець оформлює замовлення. Здається проста операція, але насправді треба:
- Списати гроші з рахунку покупця
- Зарезервувати товар на складі
- Створити запис про замовлення в базі
Що може піти не так? Майже все! Наприклад, гроші списались, а сервер «впав» до створення замовлення. Без транзакцій це перетвориться на справжній кошмар для служби підтримки. На щастя, транзакції вирішують цю проблему: або всі операції виконуються успішно, або система автоматично все відкатує назад.
Налаштування 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;
}
}
Підсумки
Тепер у нас є транзакційний сервіс, який:
- Гарантує цілісність даних.
- Автоматично відкатує зміни при помилках.
- Легко тестується.
На практиці такі підходи роблять додатки надійними й передбачуваними. І так, не забудь поставити собі зірку за сьогоднішній прогрес!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ