Транзакции и кеширование
Продвинутые техники: транзакции, кеширование запросов и оптимизация производительности
Транзакции
Транзакции обеспечивают целостность данных при выполнении нескольких связанных операций. В случае ошибки все изменения автоматически отменяются.
Основы транзакций
<!-- Базовая транзакция -->
{% 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 %}
Производительность: Используйте транзакции для связанных операций, кеширование для частых запросов, и всегда выбирайте только необходимые поля из базы данных.
Отлично! Вы изучили все основы работы с базой данных. Теперь переходите к изучению работы с формами.