323 lines
10 KiB
C++
323 lines
10 KiB
C++
#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;
|
||
} |