Транзакции и кеширование

Продвинутые техники: транзакции, кеширование запросов и оптимизация производительности

Транзакции

Транзакции обеспечивают целостность данных при выполнении нескольких связанных операций. В случае ошибки все изменения автоматически отменяются.

Основы транзакций

<!-- Базовая транзакция -->
{% php %}
    $db = db('1752665380840');
    
    $db->beginTransaction();
    
    try {
        // Операции с базой данных
        $db->editItem('users', ['id' => $userId], ['balance' => $newBalance]);
        $db->addItem('transactions', [
            'user_id' => $userId,
            'amount' => $amount,
            'type' => 'deposit',
            'created_at' => time()
        ]);
        
        $db->commit();
        $success = true;
        
    } catch (Exception $e) {
        $db->rollback();
        $error = $e->getMessage();
        $success = false;
    }
{% endphp %}

{% if success %}
    <div class="alert alert-success">Операция выполнена успешно!</div>
{% else %}
    <div class="alert alert-danger">Ошибка: {{ error }}</div>
{% endif %}

Перевод средств между пользователями

{% php %}
    $db = db('1752665380840');
    $transferResult = false;
    $transferError = '';
    
    $db->beginTransaction();
    
    try {
        // Получаем данные отправителя
        $sender = $db->getItem('users', ['id' => $senderId]);
        if (!$sender) {
            throw new Exception('Отправитель не найден');
        }
        
        if ($sender['ban'] == 1) {
            throw new Exception('Отправитель заблокирован');
        }
        
        if ($sender['balance'] < $amount) {
            throw new Exception('Недостаточно средств на балансе');
        }
        
        // Получаем данные получателя
        $receiver = $db->getItem('users', ['id' => $receiverId]);
        if (!$receiver) {
            throw new Exception('Получатель не найден');
        }
        
        if ($receiver['ban'] == 1) {
            throw new Exception('Получатель заблокирован');
        }
        
        // Проверяем дневной лимит переводов
        $todayTransferred = $db->query('
            SELECT COALESCE(SUM(amount), 0) as total
            FROM transfers 
            WHERE from_user_id = ? AND created_at >= ? AND status = "completed"
        ', [$senderId, strtotime('today')])[0]['total'];
        
        $dailyLimit = $sender['daily_limit'] ?? 50000;
        
        if ($todayTransferred + $amount > $dailyLimit) {
            throw new Exception("Превышен дневной лимит переводов: " . number_format($dailyLimit, 0, '.', ' ') . " ₽");
        }
        
        // Рассчитываем комиссию
        $commission = 0;
        if ($amount > 1000) {
            $commission = min($amount * 0.01, 500);  // 1%, но не больше 500₽
        }
        
        $totalDebit = $amount + $commission;
        
        if ($sender['balance'] < $totalDebit) {
            throw new Exception('Недостаточно средств с учетом комиссии (' . number_format($commission, 2) . ' ₽)');
        }
        
        // Выполняем перевод
        
        // 1. Списываем с отправителя
        $db->editItem('users', ['id' => $senderId], [
            'balance' => $sender['balance'] - $totalDebit,
            'updated_at' => time()
        ]);
        
        // 2. Зачисляем получателю
        $db->editItem('users', ['id' => $receiverId], [
            'balance' => $receiver['balance'] + $amount,
            'updated_at' => time()
        ]);
        
        // 3. Записываем перевод
        $transferId = $db->addItem('transfers', [
            'from_user_id' => $senderId,
            'to_user_id' => $receiverId,
            'amount' => $amount,
            'commission' => $commission,
            'comment' => $comment ?? '',
            'status' => 'completed',
            'created_at' => time()
        ]);
        
        // 4. Записываем транзакции для истории
        $db->addItem('user_transactions', [
            'user_id' => $senderId,
            'type' => 'transfer_out',
            'amount' => -$amount,
            'balance_before' => $sender['balance'],
            'balance_after' => $sender['balance'] - $totalDebit,
            'description' => "Перевод пользователю {$receiver['login']}",
            'reference_id' => $transferId,
            'created_at' => time()
        ]);
        
        $db->addItem('user_transactions', [
            'user_id' => $receiverId,
            'type' => 'transfer_in', 
            'amount' => $amount,
            'balance_before' => $receiver['balance'],
            'balance_after' => $receiver['balance'] + $amount,
            'description' => "Перевод от {$sender['login']}",
            'reference_id' => $transferId,
            'created_at' => time()
        ]);
        
        // 5. Комиссия (если есть)
        if ($commission > 0) {
            $db->addItem('user_transactions', [
                'user_id' => $senderId,
                'type' => 'commission',
                'amount' => -$commission,
                'balance_before' => $sender['balance'] - $amount,
                'balance_after' => $sender['balance'] - $totalDebit,
                'description' => "Комиссия за перевод #{$transferId}",
                'reference_id' => $transferId,
                'created_at' => time()
            ]);
        }
        
        // 6. Уведомляем получателя
        if ($receiver['notifications'] ?? 1) {
            $db->addItem('notifications', [
                'user_id' => $receiverId,
                'title' => 'Поступление средств',
                'message' => "Вы получили {$amount} ₽ от {$sender['login']}",
                'type' => 'money_received',
                'data' => json_encode([
                    'transfer_id' => $transferId,
                    'amount' => $amount,
                    'from_user' => $sender['login']
                ]),
                'created_at' => time()
            ]);
        }
        
        $db->commit();
        $transferResult = true;
        
    } catch (Exception $e) {
        $db->rollback();
        $transferError = $e->getMessage();
        
        // Логируем ошибку
        error_log("Transfer error: " . $e->getMessage() . " From: {$senderId} To: {$receiverId} Amount: {$amount}");
    }
{% endphp %}

Создание заказа с резервированием товаров

{% php %}
    $db = db('1752665380840');
    $orderResult = false;
    $orderId = null;
    $orderError = '';
    
    $db->beginTransaction();
    
    try {
        // Проверяем пользователя
        $user = $db->getItem('users', ['id' => $userId]);
        if (!$user || $user['ban'] == 1) {
            throw new Exception('Пользователь не найден или заблокирован');
        }
        
        $totalAmount = 0;
        $orderItems = [];
        
        // Проверяем доступность всех товаров
        foreach ($cartItems as $item) {
            $product = $db->getItem('products', ['id' => $item['product_id']]);
            
            if (!$product || $product['active'] == 0) {
                throw new Exception("Товар с ID {$item['product_id']} не найден или неактивен");
            }
            
            if ($product['stock'] < $item['quantity']) {
                throw new Exception("Недостаточно товара '{$product['name']}' на складе. Доступно: {$product['stock']}, запрошено: {$item['quantity']}");
            }
            
            $itemTotal = $product['price'] * $item['quantity'];
            $totalAmount += $itemTotal;
            
            $orderItems[] = [
                'product_id' => $product['id'],
                'name' => $product['name'],
                'price' => $product['price'],
                'quantity' => $item['quantity'],
                'total' => $itemTotal
            ];
        }
        
        // Проверяем баланс пользователя
        if ($user['balance'] < $totalAmount) {
            throw new Exception('Недостаточно средств на балансе');
        }
        
        // Создаем заказ
        $orderId = $db->addItem('orders', [
            'user_id' => $userId,
            'status' => 'pending',
            'total' => $totalAmount,
            'items_count' => count($orderItems),
            'created_at' => time(),
            'updated_at' => time()
        ]);
        
        // Добавляем товары в заказ и резервируем на складе
        foreach ($orderItems as $item) {
            // Добавляем товар в заказ
            $db->addItem('order_items', [
                'order_id' => $orderId,
                'product_id' => $item['product_id'],
                'name' => $item['name'],
                'price' => $item['price'],
                'quantity' => $item['quantity'],
                'total' => $item['total']
            ]);
            
            // Резервируем товар на складе
            $db->query('
                UPDATE products 
                SET stock = stock - ?, reserved = reserved + ?, updated_at = ?
                WHERE id = ?
            ', [$item['quantity'], $item['quantity'], time(), $item['product_id']]);
        }
        
        // Списываем деньги с баланса
        $db->editItem('users', ['id' => $userId], [
            'balance' => $user['balance'] - $totalAmount,
            'updated_at' => time()
        ]);
        
        // Записываем транзакцию
        $db->addItem('user_transactions', [
            'user_id' => $userId,
            'type' => 'order_payment',
            'amount' => -$totalAmount,
            'balance_before' => $user['balance'],
            'balance_after' => $user['balance'] - $totalAmount,
            'description' => "Оплата заказа #{$orderId}",
            'reference_id' => $orderId,
            'created_at' => time()
        ]);
        
        // Очищаем корзину
        $db->deleteItem('cart_items', ['user_id' => $userId]);
        
        // Добавляем уведомление
        $db->addItem('notifications', [
            'user_id' => $userId,
            'title' => 'Заказ оформлен',
            'message' => "Заказ #{$orderId} на сумму {$totalAmount} ₽ успешно создан",
            'type' => 'order_created',
            'data' => json_encode(['order_id' => $orderId]),
            'created_at' => time()
        ]);
        
        $db->commit();
        $orderResult = true;
        
    } catch (Exception $e) {
        $db->rollback();
        $orderError = $e->getMessage();
        error_log("Order creation error: " . $e->getMessage() . " User: {$userId}");
    }
{% endphp %}

Кеширование запросов

Базовое кеширование

<!-- Простое кеширование на время -->
{% set popularProducts = db('1752665380840')
    ->cache(600)  # 10 минут
    ->getItems('products', {'featured': 1}, ['*'], 'views', 'DESC', null, 10) %}

<!-- Именованный кеш -->
{% set topUsers = db('1752665380840')
    ->cache('top_users_daily', 3600)  # Кеш на час с именем
    ->getItems('users', {'ban': 0}, ['*'], 'balance', 'DESC', null, 20) %}

<!-- Кеширование сложного запроса -->
{% set monthlyStats = db('1752665380840')
    ->cache('monthly_stats', 1800)  # 30 минут
    ->query('
        SELECT 
            DATE_FORMAT(FROM_UNIXTIME(created_at), "%Y-%m") as month,
            COUNT(*) as orders,
            SUM(total) as revenue,
            AVG(total) as avg_order
        FROM orders 
        WHERE status = "completed" AND created_at >= ?
        GROUP BY month 
        ORDER BY month DESC 
        LIMIT 12
    ', [strtotime('-12 months')]) %}

Кеширование пагинации

<!-- Кеш для пагинированных результатов -->
{% set page = request.input('page', 1) %}
{% set cacheKey = 'products_page_' ~ page ~ '_' ~ (request.input('category') ?? 'all') %}

{% set products = db('1752665380840')
    ->cache(cacheKey, 900)  # 15 минут
    ->paginate('products', page, 24, {'active': 1}, 'created_at', 'DESC') %}

Условное кеширование

<!-- Кеширование только для гостей (авторизованные видят актуальные данные) -->
{% if not auth() %}
    {% set products = db('1752665380840')
        ->cache('guest_products', 1800)
        ->getItems('products', {'active': 1}) %}
{% else %}
    {% set products = db('1752665380840')
        ->getItems('products', {'active': 1}) %}
{% endif %}

<!-- Кеширование в зависимости от роли -->
{% if auth() and auth().role == 'admin' %}
    {% set stats = db('1752665380840')
        ->cache('admin_stats', 300)  # 5 минут для админов
        ->query('SELECT COUNT(*) as total FROM users') %}
{% else %}
    {% set stats = db('1752665380840')
        ->cache('public_stats', 3600)  # 1 час для остальных
        ->query('SELECT COUNT(*) as total FROM users WHERE ban = 0') %}
{% endif %}

Пулы подключений

Управление подключениями

<!-- Использование разных подключений -->
{% set mainUsers = db('1752665380840')->getItems('users') %}
{% set analyticsData = db('analytics_db')->getItems('events') %}
{% set logsData = db('logs_db')->getItems('error_logs') %}

<!-- Проверка доступности подключения -->
{% try %}
    {% set backupData = db('backup_connection')->getItems('backups') %}
{% catch %}
    <div class="alert alert-warning">Резервное подключение недоступно</div>
{% endtry %}

Оптимизация производительности

Выбор полей

<!-- Вместо SELECT * -->
{% set heavyQuery = db('1752665380840')->getItems('users') %}  # Плохо

<!-- Выбираем только нужные поля -->
{% set optimizedQuery = db('1752665380840')
    ->getItems('users', {}, ['id', 'login', 'balance']) %}  # Хорошо

<!-- Для больших таблиц -->
{% set usersList = db('1752665380840')
    ->getItems('users', 
        {'active': 1}, 
        ['id', 'login', 'last_seen'],  # Только нужные поля
        'last_seen', 
        'DESC', 
        null, 
        100  # Лимит
    ) %}

Пакетные операции

{% php %}
    $db = db('1752665380840');
    
    // Вместо множественных UPDATE
    $userIds = [1, 2, 3, 4, 5];
    
    // Плохо - N запросов
    // foreach ($userIds as $userId) {
    //     $db->editItem('users', ['id' => $userId], ['last_ping' => time()]);
    // }
    
    // Хорошо - один запрос
    $db->query('
        UPDATE users 
        SET last_ping = ? 
        WHERE id IN (' . implode(',', array_fill(0, count($userIds), '?')) . ')
    ', array_merge([time()], $userIds));
    
    // Или для простых случаев
    $db->query('
        UPDATE users 
        SET last_ping = ? 
        WHERE id IN (' . implode(',', $userIds) . ')
    ', [time()]);
{% endphp %}

Предварительная загрузка связанных данных

<!-- Плохо: N+1 запросов -->
{% set orders = db('1752665380840')->getItems('orders', {'status': 'completed'}, ['*'], 'id', 'DESC', null, 50) %}

{% foreach orders as order %}
    <div class="order">
        {% set customer = db('1752665380840')->getItem('users', {'id': order.user_id}) %}  <!-- Плохо -->
        <h3>Заказ #{{ order.id }} - {{ customer.login }}</h3>
    </div>
{% endforeach %}

<!-- Хорошо: JOIN запрос -->
{% set ordersWithCustomers = db('1752665380840')->query('
    SELECT 
        o.id,
        o.total,
        o.status,
        o.created_at,
        u.login as customer_login,
        u.email as customer_email
    FROM orders o
    JOIN users u ON o.user_id = u.id
    WHERE o.status = "completed"
    ORDER BY o.id DESC
    LIMIT 50
') %}

{% foreach ordersWithCustomers as order %}
    <div class="order">
        <h3>Заказ #{{ order.id }} - {{ order.customer_login }}</h3>
        <p>Сумма: {{ order.total | money('₽') }}</p>
    </div>
{% endforeach %}

Мониторинг и диагностика

Отладка запросов

<!-- В режиме отладки -->
{% if config('app.debug') %}
    {% set startTime = microtime(true) %}
    
    {% set users = db('1752665380840')->getItems('users', {'active': 1}) %}
    
    {% set queryTime = (microtime(true) - startTime) * 1000 %}
    
    <div class="debug-info">
        <p>Запрос выполнен за: {{ queryTime | round(2) }}ms</p>
        <p>Записей получено: {{ users | length }}</p>
        <p>Память: {{ (memory_get_usage() / 1024 / 1024) | round(2) }}MB</p>
    </div>
{% endif %}

Логирование медленных запросов

{% php %}
    // Настройка в config/database.json
    /*
    {
        "connections": {
            "1752665380840": {
                ...
                "slow_query_log": true,
                "slow_query_time": 1000,  // 1 секунда в миллисекундах
                "log_path": "/mnrfy-temp/logs/slow_queries.log"
            }
        }
    }
    */
    
    // В коде
    $startTime = microtime(true);
    
    $result = db('1752665380840')->query('
        SELECT u.*, COUNT(o.id) as orders_count
        FROM users u
        LEFT JOIN orders o ON u.id = o.user_id
        GROUP BY u.id
        HAVING orders_count > 10
        ORDER BY orders_count DESC
    ');
    
    $queryTime = (microtime(true) - $startTime) * 1000;
    
    if ($queryTime > 1000) {  // Больше 1 секунды
        error_log("Slow query ({$queryTime}ms): " . $query, 3, MNRFY_TEMP . '/logs/slow_queries.log');
    }
{% endphp %}

Примеры производственного использования

Массовая обработка пользователей

{% php %}
    $db = db('1752665380840');
    $batchSize = 1000;
    $processed = 0;
    $errors = 0;
    
    // Получаем общее количество
    $total = $db->countItems('users', ['processed' => 0]);
    
    echo "<div class='batch-progress'>";
    echo "<p>Обработка {$total} пользователей...</p>";
    
    while (true) {
        // Получаем пакет пользователей
        $users = $db->getItems('users', 
            ['processed' => 0], 
            ['id', 'login', 'email', 'balance'], 
            'id', 
            'ASC', 
            null, 
            $batchSize
        );
        
        if (empty($users)) break;
        
        $db->beginTransaction();
        
        try {
            foreach ($users as $user) {
                // Какая-то обработка
                $newData = processUser($user);  // Ваша функция обработки
                
                // Обновляем пользователя
                $db->editItem('users', ['id' => $user['id']], [
                    'processed' => 1,
                    'processed_at' => time(),
                    'new_field' => $newData
                ]);
                
                $processed++;
            }
            
            $db->commit();
            echo "<p>Обработано: {$processed} из {$total}</p>";
            
        } catch (Exception $e) {
            $db->rollback();
            $errors++;
            error_log("Batch processing error: " . $e->getMessage());
            
            // Пропускаем проблемных пользователей
            foreach ($users as $user) {
                $db->editItem('users', ['id' => $user['id']], ['processed' => -1]);
            }
        }
        
        // Пауза между пакетами
        usleep(100000); // 0.1 секунды
    }
    
    echo "<p class='success'>Обработка завершена! Успешно: {$processed}, ошибок: {$errors}</p>";
    echo "</div>";
{% endphp %}

Система ротации логов

{% php %}
    $db = db('1752665380840');
    
    // Архивируем старые логи (старше 30 дней)
    $cutoffDate = time() - (30 * 24 * 60 * 60);
    
    $db->beginTransaction();
    
    try {
        // Копируем в архив
        $db->query('
            INSERT INTO logs_archive (user_id, action, ip_address, user_agent, created_at)
            SELECT user_id, action, ip_address, user_agent, created_at
            FROM logs 
            WHERE created_at < ?
        ', [$cutoffDate]);
        
        // Удаляем из основной таблицы
        $deletedCount = $db->query('DELETE FROM logs WHERE created_at < ?', [$cutoffDate]);
        
        $db->commit();
        
        echo "<p>Архивировано {$deletedCount} записей логов</p>";
        
        // Оптимизируем таблицы
        $db->query('OPTIMIZE TABLE logs');
        $db->query('OPTIMIZE TABLE logs_archive');
        
    } catch (Exception $e) {
        $db->rollback();
        error_log("Log rotation error: " . $e->getMessage());
    }
{% endphp %}
Производительность: Используйте транзакции для связанных операций, кеширование для частых запросов, и всегда выбирайте только необходимые поля из базы данных.
Отлично! Вы изучили все основы работы с базой данных. Теперь переходите к изучению работы с формами.