Створення моделі простого транзакційного сценарію
Мета: Ознайомитися з патерном Transaction Script та його використанням у програмуванні. Закріпити навички роботи з базами даних, обробки транзакцій та забезпечення узгодженості даних у програмі.
Завдання¶
Реалізувати приклад проєкту на тему «Інтернет-магазин», що імплементує патерн Transaction Script, де кожна операція має виконуватися в транзакції.
Опис: Користувач формує кошик, оформлює замовлення, а система перевіряє наявність товару та проводить оплату.
Проєкт має включати:
- Створити
Productізid,name,price,stockQuantity. - Реалізувати
AddToCart,Checkout,CancelOrder. - Використати Transaction Script для обробки операцій.
- Реалізувати зменшення залишку товару після покупки.
- Вивести історію замовлень.
Теоретичні відомості¶
Транзакційний сценарій (Transaction script) — це патерн, який організовує бізнес-логіку у вигляді процедур, де кожна процедура обробляє один конкретний запит від презентаційного шару (користувача).
Фактично, це простий процедурний скрипт, який виконує дії послідовно: отримує дані, проводить обчислення та зберігає результат у базу даних.
Ключові характеристики:
- Транзакційна поведінка: Це головна вимога. Сценарій має виконуватися повністю успішно або повністю скасувати зміни (rollback). Він ніколи не повинен залишати систему в частково зміненому (неузгодженому) стані.
-
Сфера застосування: ідеально підходить для допоміжних піддоменів (Supporting Subdomains), простих CRUD-операцій або ETL-процесів (вилучення-перетворення-завантаження), де логіка є лінійною і простою.
-
Недоліки: У складних доменах (Core subdomains) цей підхід призводить до дублювання коду та перетворення системи на "велику купу бруду" (Big Ballof Mud), оскільки бізнес-правила розмиваються між різними скриптами.
Це повна протилежність Доменної моделі (де логіка живе всередині об'єктів), яку ми обговорювали раніше.
Хід роботи¶
Як випливає з теоретичної частини, транзакційний сценарій є протилежністю доменної моделі щодо розташування бізнес-логіки. Тому сутності Product та Order виступають лише як проєкції (або контейнери даних) таблиць у базі даних.
@Data
@Entity
@Table(name = "shopping_carts")
public class Cart {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@ManyToMany
@JoinTable(
name = "shopping_carts_products",
joinColumns = @JoinColumn(name = "cart_id"),
inverseJoinColumns = @JoinColumn(name = "product_id"))
private Set<Product> products;
}
@Data
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@ManyToMany
@JoinTable(
name = "orders_products",
joinColumns = @JoinColumn(name = "order_id"),
inverseJoinColumns = @JoinColumn(name = "product_id"))
private Set<Product> products;
@Enumerated(EnumType.STRING)
private OrderStatus status;
}
В даному випадку проєкт керується трьома таблицями.
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
private final CartRepository cartRepository;
@Transactional
@Override
public Product addToCart(UUID productId) {
// 1. Find product by ID
Product product = productRepository
.findById(productId)
.orElseThrow(
() -> new IllegalArgumentException("Product not found with id: " + productId));
// 2. Check stock availability
if (product.getStock() <= 0) {
throw new IllegalStateException("Product out of stock: " + product.getName());
}
// 3. Decrease stock
product.setStock(product.getStock() - 1);
productRepository.save(product);
// 4. Get or create cart (singleton pattern)
Cart cart = cartRepository
.findFirstByOrderByIdAsc()
.orElseGet(
() -> {
Cart newCart = new Cart();
newCart.setProducts(new HashSet<>());
return cartRepository.save(newCart);
});
// 5. Add product to cart
cart.getProducts().add(product);
cartRepository.save(cart);
return product;
}
}
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final CartRepository cartRepository;
@Transactional
@Override
public BigDecimal checkout() {
// 1. Get current cart
Cart cart = cartRepository
.findFirstByOrderByIdAsc()
.orElseThrow(() -> new IllegalStateException("Cart not found"));
// 2. Check cart is not empty
if (cart.getProducts() == null || cart.getProducts().isEmpty()) {
throw new IllegalStateException("Cannot checkout with empty cart");
}
// 3. Create new order
Order order = new Order();
order.setProducts(new HashSet<>(cart.getProducts()));
order.setStatus(OrderStatus.OPEN);
// 4. Calculate total price
BigDecimal totalPrice = cart.getProducts().stream()
.map(Product::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 5. Clear cart
cart.getProducts().clear();
cartRepository.save(cart);
// 6. Save order
orderRepository.save(order);
return totalPrice;
}
@Transactional
@Override
public void cancel() {
// 1. Find the last open order
Order order = ((Iterable<Order>) orderRepository.findAll())
.iterator().hasNext()
? ((Iterable<Order>) orderRepository.findAll()).iterator().next()
: null;
if (order == null) {
throw new IllegalStateException("No order found to cancel");
}
// 2. Check order status is OPEN
if (order.getStatus() != OrderStatus.OPEN) {
throw new IllegalStateException("Cannot cancel order with status: " + order.getStatus());
}
// 3. Restore stock for each product
for (Product product : order.getProducts()) {
Product dbProduct = productRepository
.findById(product.getId())
.orElseThrow(
() -> new IllegalStateException(
"Product not found during cancel: " + product.getId()));
dbProduct.setStock(dbProduct.getStock() + 1);
productRepository.save(dbProduct);
}
// 4. Set order status to CLOSED
order.setStatus(OrderStatus.CLOSED);
orderRepository.save(order);
}
}
Cервіси виконують бізнес-логіку, а не в агрегаті, що є протилежним підходу DDD. Кожен метод містить анотацію @Transactional, що дозволяє виконати операцію успішно або відкотити зміни у разі помилки.
Висновки¶
Ознайомлення з патерном Transaction Script закріпило навички роботи з базами даних, обробки транзакцій та забезпечення атомарності й узгодженості операцій і даних у програмі. Результатом став приклад проєкту, що демонструє патерн, реалізуючи операції в рамках транзакцій і розміщуючи бізнес-логіку у сервісах.