SQL и Qt

Подключение NoSQL (MongoDB) из Qt-приложения без QtSql

Введение

Qt SQL Module (QSqlDatabase, QSqlQuery) спроектирован специально для реляционных баз данных и не поддерживает NoSQL-хранилища типа MongoDB напрямую. Если приложению нужен доступ к MongoDB (например, для хранения документов с гибкой, заранее не фиксированной структурой), нужно использовать отдельный официальный MongoDB C++ Driver, интегрированный с остальным Qt-кодом вручную. В статье разберём базовое подключение и операции с MongoDB из Qt-приложения.

Концепция

MongoDB C++ Driver (mongocxx/bsoncxx) — отдельная, не связанная с Qt библиотека со своей моделью данных на основе BSON (бинарного JSON-подобного формата). Интеграция с Qt сводится к написанию собственного, тонкого repository-слоя (аналогично паттерну из статьи 231, но без QSqlQuery), который конвертирует между BSON-документами MongoDB и QJsonObject/собственными C++ структурами Qt-приложения — благодаря схожести JSON и BSON эта конвертация относительно прямолинейна.

Пример кода: Подключение NoSQL (MongoDB)

# CMakeLists.txt — подключение MongoDB C++ Driver
find_package(mongocxx REQUIRED)
find_package(bsoncxx REQUIRED)

target_link_libraries(MyApp PRIVATE
    Qt6::Core
    mongo::mongocxx_shared
    mongo::bsoncxx_shared
)
// mongoeventrepository.h — repository-слой поверх MongoDB для логов событий
#pragma once
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <bsoncxx/builder/stream/document.hpp>
#include <bsoncxx/json.hpp>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDateTime>

class MongoEventRepository
{
public:
    explicit MongoEventRepository(const std::string &uri, const std::string &dbName)
        : m_client(mongocxx::uri(uri))
    {
        m_database = m_client[dbName];
    }

    bool logEvent(const QString &eventType, const QJsonObject &payload)
    {
        using bsoncxx::builder::stream::document;
        using bsoncxx::builder::stream::finalize;

        // Конвертируем QJsonObject в JSON-строку, а затем в формат BSON MongoDB
        const QByteArray jsonBytes = QJsonDocument(payload).toJson(QJsonDocument::Compact);
        const auto payloadBson = bsoncxx::from_json(jsonBytes.toStdString());

        document builder{};
        builder << "eventType" << eventType.toStdString()
                << "timestamp" << bsoncxx::types::b_date(
                       std::chrono::system_clock::now())
                << "payload" << payloadBson.view();

        auto collection = m_database["events"];
        const auto result = collection.insert_one(builder.view());

        return result.has_value();
    }

    QVector<QJsonObject> findEventsByType(const QString &eventType, int limit = 50)
    {
        QVector<QJsonObject> results;
        using bsoncxx::builder::stream::document;
        using bsoncxx::builder::stream::finalize;

        document filter{};
        filter << "eventType" << eventType.toStdString();

        auto collection = m_database["events"];
        mongocxx::options::find options;
        options.limit(limit);

        auto cursor = collection.find(filter.view(), options);
        for (const auto &doc : cursor) {
            const std::string jsonString = bsoncxx::to_json(doc);
            const QJsonDocument parsedDoc = QJsonDocument::fromJson(
                QByteArray::fromStdString(jsonString));
            results.append(parsedDoc.object());
        }

        return results;
    }

private:
    mongocxx::client m_client;
    mongocxx::database m_database;
};
// main.cpp — обязательная единоразовая инициализация драйвера
#include <mongocxx/instance.hpp>

int main(int argc, char *argv[])
{
    mongocxx::instance instance{}; // должен существовать ровно один экземпляр на всё время работы программы

    QCoreApplication app(argc, argv);

    MongoEventRepository repository("mongodb://localhost:27017", "myapp_logs");
    repository.logEvent("user_login", QJsonObject{{"userId", 42}, {"ip", "192.168.1.5"}});

    return app.exec();
}

Пояснения к коду

MongoEventRepository::logEvent() показывает ключевую идею интеграции — QJsonObject (привычный из основного цикла статей про JSON) сначала сериализуется в QByteArray через QJsonDocument::toJson(), а затем конвертируется в формат BSON через bsoncxx::from_json() — это позволяет приложению продолжать работать с привычными Qt JSON-классами в остальном коде, выполняя конвертацию в формат MongoDB только в самом тонком, граничном слое repository.

findEventsByType() выполняет обратную конвертацию — каждый найденный BSON-документ из курсора результатов конвертируется обратно в JSON-строку через bsoncxx::to_json(), а затем разбирается уже знакомым QJsonDocument::fromJson(). mongocxx::instance instance{} в main() — обязательная деталь, легко забываемая новичками в работе с этим драйвером: согласно требованиям самой библиотеки MongoDB C++ Driver, должен существовать ровно один экземпляр mongocxx::instance на всё время жизни процесса, созданный до любого использования других классов драйвера.

Подводные камни

  • Забытое создание единственного mongocxx::instance или попытка создать несколько экземпляров — согласно документации драйвера, это либо приводит к неопределённому поведению, либо к явному исключению, зависящему от конкретной версии драйвера; этот объект должен жить дольше, чем любое использование mongocxx::client, и обычно создаётся первой строкой в main().
  • Потеря типов данных при двойной конвертации JSON → BSON → JSON. Хотя BSON и JSON концептуально близки, BSON поддерживает дополнительные типы, не имеющие прямого аналога в стандартном JSON (например, специфичные типы дат, как b_date в примере) — при обратной конвертации в QJsonObject такие специфичные BSON-типы могут быть представлены не так, как ожидается, и для критичных полей (особенно дат) стоит явно проверять, как именно конкретная используемая версия bsoncxx::to_json() сериализует эти типы.
  • Отсутствие схемы (schema-less природа MongoDB) перекладывает всю ответственность за консистентность структуры документов на код приложения. В отличие от реляционной базы данных с фиксированной схемой таблиц, MongoDB не запрещает вставить документ с произвольно отличающейся структурой — без дисциплинированного, явного контроля на уровне repository-слоя (например, через валидацию JSON Schema, разобранную в основном цикле статей про JSON, статья 147) база данных постепенно может накопить документы с несовместимой между собой структурой, что усложняет последующую обработку.
  • Линковка с mongocxx/bsoncxx требует, чтобы соответствующие библиотеки и заголовки были установлены в системе или подключены через vcpkg/Conan (основной цикл статей про CMake/Boost, статьи 33, 40) — это полноценная отдельная зависимость со своими требованиями к версии компилятора и системным библиотекам (включая сам MongoDB C Driver, от которого зависит C++ драйвер), и интеграция в существующий CMake-проект требует дополнительной настройки по сравнению с встроенным в Qt модулем QtSql.