Transaction Script (Сценнарий транзакции)
Паттерн проектирования Transaction Script
Описание Transaction Script
Организует бизнес-логику в процедуры, которые управляют каждая своим запросом.
Большинство бизнес-приложений можно представить в виде набора транзакций. Какие-то из них выбират данные, какие-то ‐ меняют. Каждое взаимодействие пользователя и системы содержит определённый набор действий. В некоторых случаях это может быть просто вывод данных из БД. В других случаях эти дествия могут содержать в себе множество вычислений и проверок.
Паттерн Transaction Script организует всю эту логику в одну процедуру, работая в БД напрямую или через тонкую обёртку. Каждая транзакция имеет свой Transaction Script, хотя общие подзадачи могут быть разбиты на процедуры.
Примеры реализации
// Transaction Script Pattern in JavaScript
class OrderTransactionScript {
constructor(database) {
this.db = database;
}
async createOrder(customerId, items) {
console.log('Starting createOrder transaction');
try {
await this.db.beginTransaction();
const customer = await this.validateCustomer(customerId);
if (!customer) {
throw new Error('Invalid customer');
}
const total = this.calculateOrderTotal(items);
const orderId = await this.createOrderRecord(customerId, total);
for (const item of items) {
await this.addOrderItem(orderId, item);
}
await this.updateInventory(items);
await this.sendOrderConfirmation(customer.email, orderId);
await this.db.commit();
console.log(`Order ${orderId} created successfully`);
return { orderId, total, status: 'success' };
} catch (error) {
await this.db.rollback();
console.error('Order creation failed:', error.message);
throw error;
}
}
async validateCustomer(customerId) {
return await this.db.query('SELECT * FROM customers WHERE id = ?', [customerId]);
}
calculateOrderTotal(items) {
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
}
async createOrderRecord(customerId, total) {
const result = await this.db.query(
'INSERT INTO orders (customer_id, total, status) VALUES (?, ?, ?)',
[customerId, total, 'pending']
);
return result.insertId;
}
async addOrderItem(orderId, item) {
await this.db.query(
'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)',
[orderId, item.productId, item.quantity, item.price]
);
}
async updateInventory(items) {
for (const item of items) {
await this.db.query(
'UPDATE products SET stock = stock - ? WHERE id = ?',
[item.quantity, item.productId]
);
}
}
async sendOrderConfirmation(email, orderId) {
return { success: true, messageId: `msg_${Date.now()}` };
}
}
// Usage
const orderScript = new OrderTransactionScript(database);
orderScript.createOrder(1, [
{ productId: 101, quantity: 2, price: 25.99 },
{ productId: 102, quantity: 1, price: 15.50 }
]).then(result => {
console.log('Transaction completed:', result);
}).catch(error => {
console.error('Transaction failed:', error);
});
<?php
// Transaction Script Pattern in PHP
class OrderTransactionScript {
private $db;
public function __construct($database) {
$this->db = $database;
}
public function createOrder($customerId, $items) {
echo "Starting createOrder transaction\n";
try {
$this->db->beginTransaction();
$customer = $this->validateCustomer($customerId);
if (!$customer) {
throw new Exception('Invalid customer');
}
$total = $this->calculateOrderTotal($items);
$orderId = $this->createOrderRecord($customerId, $total);
foreach ($items as $item) {
$this->addOrderItem($orderId, $item);
}
$this->updateInventory($items);
$this->sendOrderConfirmation($customer['email'], $orderId);
$this->db->commit();
echo "Order $orderId created successfully\n";
return ['orderId' => $orderId, 'total' => $total, 'status' => 'success'];
} catch (Exception $e) {
$this->db->rollback();
echo "Order creation failed: " . $e->getMessage() . "\n";
throw $e;
}
}
private function validateCustomer($customerId) {
$stmt = $this->db->prepare('SELECT * FROM customers WHERE id = ?');
$stmt->execute([$customerId]);
return $stmt->fetch();
}
private function calculateOrderTotal($items) {
$total = 0;
foreach ($items as $item) {
$total += $item['price'] * $item['quantity'];
}
return $total;
}
private function createOrderRecord($customerId, $total) {
$stmt = $this->db->prepare('INSERT INTO orders (customer_id, total, status) VALUES (?, ?, ?)');
$stmt->execute([$customerId, $total, 'pending']);
return $this->db->lastInsertId();
}
private function addOrderItem($orderId, $item) {
$stmt = $this->db->prepare('INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)');
$stmt->execute([$orderId, $item['productId'], $item['quantity'], $item['price']]);
}
private function updateInventory($items) {
foreach ($items as $item) {
$stmt = $this->db->prepare('UPDATE products SET stock = stock - ? WHERE id = ?');
$stmt->execute([$item['quantity'], $item['productId']]);
}
}
private function sendOrderConfirmation($email, $orderId) {
return ['success' => true, 'messageId' => 'msg_' . time()];
}
}
// Usage
$orderScript = new OrderTransactionScript($database);
try {
$result = $orderScript->createOrder(1, [
['productId' => 101, 'quantity' => 2, 'price' => 25.99],
['productId' => 102, 'quantity' => 1, 'price' => 15.50]
]);
echo "Transaction completed: " . json_encode($result) . "\n";
} catch (Exception $e) {
echo "Transaction failed: " . $e->getMessage() . "\n";
}
?>
// Transaction Script Pattern in Go
package main
import (
"database/sql"
"fmt"
)
type OrderItem struct {
ProductID int `json:"productId"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
}
type OrderTransactionScript struct {
db *sql.DB
}
func NewOrderTransactionScript(db *sql.DB) *OrderTransactionScript {
return &OrderTransactionScript{db: db}
}
func (ots *OrderTransactionScript) CreateOrder(customerID int, items []OrderItem) (map[string]interface{}, error) {
fmt.Println("Starting createOrder transaction")
tx, err := ots.db.Begin()
if err != nil {
return nil, err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
customer, err := ots.validateCustomer(tx, customerID)
if err != nil {
return nil, err
}
if customer == nil {
return nil, fmt.Errorf("invalid customer")
}
total := ots.calculateOrderTotal(items)
orderID, err := ots.createOrderRecord(tx, customerID, total)
if err != nil {
return nil, err
}
for _, item := range items {
if err := ots.addOrderItem(tx, orderID, item); err != nil {
return nil, err
}
}
if err := ots.updateInventory(tx, items); err != nil {
return nil, err
}
if err := ots.sendOrderConfirmation(customer["email"], orderID); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
fmt.Printf("Order %d created successfully\n", orderID)
return map[string]interface{}{
"orderId": orderID,
"total": total,
"status": "success",
}, nil
}
func (ots *OrderTransactionScript) validateCustomer(tx *sql.Tx, customerID int) (map[string]string, error) {
var customer map[string]string
err := tx.QueryRow("SELECT * FROM customers WHERE id = ?", customerID).Scan(&customer)
if err == sql.ErrNoRows {
return nil, nil
}
return customer, err
}
func (ots *OrderTransactionScript) calculateOrderTotal(items []OrderItem) float64 {
total := 0.0
for _, item := range items {
total += item.Price * float64(item.Quantity)
}
return total
}
func (ots *OrderTransactionScript) createOrderRecord(tx *sql.Tx, customerID int, total float64) (int64, error) {
result, err := tx.Exec("INSERT INTO orders (customer_id, total, status) VALUES (?, ?, ?)",
customerID, total, "pending")
if err != nil {
return 0, err
}
return result.LastInsertId()
}
func (ots *OrderTransactionScript) addOrderItem(tx *sql.Tx, orderID int64, item OrderItem) error {
_, err := tx.Exec("INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)",
orderID, item.ProductID, item.Quantity, item.Price)
return err
}
func (ots *OrderTransactionScript) updateInventory(tx *sql.Tx, items []OrderItem) error {
for _, item := range items {
_, err := tx.Exec("UPDATE products SET stock = stock - ? WHERE id = ?",
item.Quantity, item.ProductID)
if err != nil {
return err
}
}
return nil
}
func (ots *OrderTransactionScript) sendOrderConfirmation(email string, orderID int64) error {
return nil
}
// Usage
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err)
}
defer db.Close()
orderScript := NewOrderTransactionScript(db)
result, err := orderScript.CreateOrder(1, []OrderItem{
{ProductID: 101, Quantity: 2, Price: 25.99},
{ProductID: 102, Quantity: 1, Price: 15.50},
})
if err != nil {
fmt.Printf("Transaction failed: %v\n", err)
} else {
fmt.Printf("Transaction completed: %+v\n", result)
}
}
Использована иллюстрация с сайта Мартина Фаулера.