cards/src/database.cpp

323 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "database.hpp"
#include <chrono>
Database::Database(const DatabaseConfig& cfg)
: conn(std::make_unique<pqxx::connection>(
"host=" + cfg.host +
" port=" + std::to_string(cfg.port) +
" dbname=" + cfg.dbname +
" user=" + cfg.user +
" password=" + cfg.password)),
rng(std::random_device{}()) {
ensureTableExists();
}
void Database::ensureTableExists() {
pqxx::work txn(*conn);
// Создаем таблицу, если её нет
txn.exec(
"CREATE TABLE IF NOT EXISTS random_data ("
" id SERIAL PRIMARY KEY,"
" name VARCHAR(100) NOT NULL,"
" value INTEGER NOT NULL,"
" description TEXT,"
" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
")"
);
txn.commit();
}
json Database::getRandomRecord() {
std::lock_guard<std::mutex> lock(mtx);
try {
// Получаем общее количество записей
int totalCount = getTotalCount();
if (totalCount == 0) {
return json{
{"error", "No records found in database"}
};
}
// Генерируем случайный ID
std::uniform_int_distribution<int> idDist(1, totalCount);
int randomId = idDist(rng);
pqxx::work txn(*conn);
// Запрашиваем случайную запись
auto result = txn.exec_params(
"SELECT id, name, value, description, created_at "
"FROM random_data "
"WHERE id = $1",
randomId
);
if (result.empty()) {
// Если не нашли, берем первую (на всякий случай)
result = txn.exec("SELECT id, name, value, description, created_at FROM random_data LIMIT 1");
}
if (result.empty()) {
txn.abort();
return json{
{"error", "No records found in database"}
};
}
const auto& row = result[0];
json response = {
{"id", row[0].as<int>()},
{"name", row[1].as<std::string>()},
{"value", row[2].as<int>()},
{"description", row[3].as<std::string>()},
{"created_at", row[4].as<std::string>()},
{"timestamp", std::chrono::system_clock::now().time_since_epoch().count()}
};
txn.commit();
return response;
} catch (const std::exception& e) {
return json{
{"error", "Database error in getRandomRecord"},
{"message", e.what()}
};
}
}
// БЫСТРЫЙ ВАРИАНТ: Использует рекурсивный CTE
json Database::getRandomRecordsFast(const int count) {
pqxx::work txn(*conn);
try {
// Рекурсивный CTE запрос для получения случайных записей
std::string query =
"WITH RECURSIVE random_selection AS ("
" WITH bounds AS ("
" SELECT"
" MIN(t.id) AS min_id,"
" MAX(t.id) AS max_id"
" FROM random_data AS t"
" ),"
" first_row AS ("
" SELECT"
" t.id,"
" b.min_id,"
" b.max_id,"
" ARRAY[t.id] AS selected_ids,"
" 1 AS depth"
" FROM random_data AS t, bounds AS b"
" WHERE"
" t.id >= b.min_id + ((b.max_id - b.min_id) * random())::INT"
" ORDER BY t.id"
" LIMIT 1"
" ),"
" recursive_rows AS ("
" SELECT"
" t.id,"
" r.min_id,"
" r.max_id,"
" r.selected_ids || t.id AS selected_ids,"
" r.depth + 1 AS depth"
" FROM random_data AS t, first_row AS r"
" WHERE"
" t.id >= r.min_id + ((r.max_id - r.min_id) * random())::INT"
" AND NOT (t.id = ANY(r.selected_ids))"
" AND r.depth + 1 < " + std::to_string(count + 1) +
" ORDER BY t.id"
" LIMIT 1"
" )"
" SELECT * FROM first_row"
" UNION ALL"
" SELECT * FROM recursive_rows"
")"
"SELECT"
" rd.id,"
" rd.name,"
" rd.value,"
" rd.description,"
" rd.created_at"
"FROM random_selection AS rs"
"JOIN random_data AS rd ON rd.id = rs.id"
"ORDER BY rs.depth";
auto result = txn.exec(query);
if (result.empty()) {
return json{
{"error", "No records found in database"},
{"count", 0}
};
}
json response = json::array();
for (const auto& row : result) {
response.push_back({
{"id", row[0].as<int>()},
{"name", row[1].as<std::string>()},
{"value", row[2].as<int>()},
{"description", row[3].as<std::string>()},
{"created_at", row[4].as<std::string>()}
});
}
txn.commit();
return {
{"data", response},
{"count", static_cast<int>(response.size())},
{"strategy", "fast_recursive_cte"},
{"timestamp", std::chrono::system_clock::now().time_since_epoch().count()}
};
} catch (const std::exception& e) {
txn.abort();
return json{
{"error", "Database error in fast recursive CTE strategy"},
{"message", e.what()}
};
}
}
// МЕДЛЕННЫЙ ВАРИАНТ: Генерирует ID на клиенте, затем запрашивает по IN
json Database::getRandomRecordsSlow(const int count, const int totalCount) {
// Генерируем уникальные случайные ID
std::set<int> ids;
{
std::uniform_int_distribution<int> idDist(1, totalCount);
while (ids.size() < static_cast<size_t>(count)) {
ids.insert(idDist(rng));
}
}
// Запрашиваем записи по сгенерированным ID
pqxx::work txn(*conn);
try {
if (ids.empty()) {
return json{
{"data", json::array()},
{"count", 0},
{"strategy", "slow"}
};
}
// Формируем запрос с IN
std::string query = getQueryRandomIds(static_cast<int>(ids.size()));
// Подготавливаем параметры для exec_params
pqxx::params param_list;
for (int id : ids) {
param_list.append(id);
}
auto result = txn.exec_params(query, param_list);
json response = json::array();
for (const auto& row : result) {
response.push_back({
{"id", row[0].as<int>()},
{"name", row[1].as<std::string>()},
{"value", row[2].as<int>()},
{"description", row[3].as<std::string>()},
{"created_at", row[4].as<std::string>()}
});
}
txn.commit();
return {
{"data", response},
{"count", static_cast<int>(response.size())},
{"strategy", "slow"},
{"timestamp", std::chrono::system_clock::now().time_since_epoch().count()}
};
} catch (const std::exception& e) {
txn.abort();
return json{
{"error", "Database error in slow strategy"},
{"message", e.what()}
};
}
}
// ГЛАВНАЯ ФУНКЦИЯ: Выбирает стратегию в зависимости от count
json Database::getRandomRecords(int count, int med, int max) {
std::lock_guard<std::mutex> lock(mtx);
// Валидация входных параметров
if (count <= 0) {
return json{
{"error", "Count must be positive"},
{"count", 0}
};
}
// Получаем общее количество записей с блокировкой
int totalCount = getTotalCount();
if (totalCount == 0) {
return json{
{"error", "No records found in database"},
{"count", 0}
};
}
if (count > totalCount) {
count = totalCount;
}
// Защита от слишком больших запросов
if (count > max) {
count = max;
}
// Выбор стратегии:
// - Для малых выборок (<= med) используем медленный, но более точный способ
// - Для больших выборок (> med) используем быстрый способ с рекурсивным CTE запросом
json response;
if (count <= med) {
response = getRandomRecordsSlow(count, totalCount);
} else {
response = getRandomRecordsFast(count);
}
if(response.contains("error")){
return response;
}
// Добавляем общую мета-информацию
response["requested_count"] = count;
response["total_available"] = totalCount;
return response;
}
// Вспомогательный метод для получения общего количества записей
int Database::getTotalCount() {
pqxx::work txn(*conn);
auto result = txn.exec("SELECT COUNT(*) FROM random_data");
int count = result[0][0].as<int>();
txn.commit();
return count;
}
std::string Database::getQueryRandomIds(const int count){
std::string query =
"SELECT id, name, value, description, created_at "
"FROM random_data WHERE id IN (";
// Подготавливаем параметры
for (size_t i = 0; i < count; ++i) {
if (i > 0) query += ",";
query += "$" + std::to_string(i + 1);
}
query += ") ORDER BY id";
return query;
}