SQL и Qt

QSqlQuery и QSqlTableModel в Qt: практическая работа с БД

Введение

Основной цикл статей (статья 24, БД/ORM) уже касался работы с БД из Qt — в этой статье систематизируем практический инструментарий Qt для работы с реляционными БД: QSqlQuery для прямого выполнения SQL (включая параметризованные запросы из статьи 433), и QSqlTableModel/QSqlRelationalTableModel как Qt-специфичный, более высокоуровневый подход, концептуально близкий к лёгкому ORM (статья 436), но интегрированный непосредственно с моделями Qt для отображения в табличных виджетах.

Концепция

QSqlQuery — прямой, низкоуровневый интерфейс выполнения произвольного SQL, дающий полный контроль над запросом (аналогично прямому SQL из всего предыдущего цикла), но требующий явного написания каждого запроса. QSqlTableModel оборачивает обращение к конкретной таблице БД в интерфейс модели Qt (QAbstractTableModel), что позволяет напрямую связать таблицу БД с табличным виджетом (QTableView) без написания отдельного кода для загрузки/отображения/сохранения данных — изменения, сделанные пользователем непосредственно в виджете, автоматически транслируются в соответствующие SQL-команды модели. QSqlRelationalTableModel расширяет это дополнительной поддержкой внешних ключей (статья 397), автоматически отображая связанные значения (например, имя клиента вместо его числового customer_id) непосредственно в табличном представлении.

Пример кода: QSqlQuery и QSqlTableModel в Qt

// QSqlQuery — прямое выполнение запроса с обработкой результата построчно
QSqlQuery query;
query.prepare("SELECT id, name, total_amount FROM orders WHERE customer_id = :id");
query.bindValue(":id", 5);

if (query.exec()) {
    while (query.next()) {
        int orderId = query.value("id").toInt();
        QString name = query.value("name").toString();
        double amount = query.value("total_amount").toDouble();
        qDebug() << orderId << name << amount;
    }
} else {
    qWarning() << "Ошибка запроса:" << query.lastError().text();
}
// QSqlTableModel — связывание таблицы БД с QTableView без написания SQL вручную
#include <QSqlTableModel>
#include <QTableView>

void setupTableModel(QTableView *view)
{
    auto *model = new QSqlTableModel();
    model->setTable("orders");
    model->setEditStrategy(QSqlTableModel::OnFieldChange); // изменения сохраняются автоматически
    model->select(); // выполняет SELECT * FROM orders под капотом

    view->setModel(model);
    // Пользователь может редактировать данные прямо в таблице интерфейса —
    // модель автоматически генерирует соответствующие UPDATE-команды
}
// QSqlRelationalTableModel — автоматическое отображение связанных данных через внешний ключ
#include <QSqlRelationalTableModel>
#include <QSqlRelationalDelegate>

void setupRelationalModel(QTableView *view)
{
    auto *model = new QSqlRelationalTableModel();
    model->setTable("orders");
    model->setRelation(1, QSqlRelation("customers", "id", "name")); // столбец 1 = customer_id
    model->select();

    view->setModel(model);
    view->setItemDelegate(new QSqlRelationalDelegate(view)); // показывает имя клиента, не его id,
                                                                // и даёт выбор из выпадающего списка при редактировании
}

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

QSqlQuery::prepare()/bindValue() показывает уже знакомый из статьи 433 паттерн параметризованного запроса — QSqlQuery остаётся базовым строительным блоком даже при использовании более высокоуровневых моделей, и понимание его API необходимо для любых случаев, не покрываемых удобствами QSqlTableModel. QSqlTableModel::setEditStrategy(OnFieldChange) показывает декларативную настройку поведения сохранения изменений — вместо явного написания кода обработки сигналов изменения данных виджета и соответствующего вызова UPDATE, разработчик просто настраивает желаемую стратегию, и остальное берёт на себя сама модель. QSqlRelationalTableModel::setRelation() явно демонстрирует Qt-специфичный механизм работы со связанными данными — концептуально похожий на JOIN (статья 398), но выраженный через объектно-ориентированный API модели, а не явный SQL-запрос, и сопровождаемый QSqlRelationalDelegate, автоматически предоставляющим удобный выпадающий список для выбора связанного значения непосредственно в табличном виджете.

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

  • Использование QSqlTableModel для сложных, многотабличных операций, требующих явного JOIN или агрегации, для которых эта модель не предназначена — QSqlTableModel ориентирован на простой, прямой доступ к одной конкретной таблице (с опциональным расширением через QSqlRelationalTableModel для отображения, но не для сложной агрегации), и для запросов, требующих более сложной логики (статьи 398-405), стоит либо использовать QSqlQueryModel (read-only модель, принимающая произвольный, написанный вручную SQL-запрос), либо явный QSqlQuery с собственной логикой представления данных.
  • Установка EditStrategy::OnFieldChange для таблицы с большим количеством одновременных, быстрых изменений (например, программная массовая правка многих ячеек подряд), что приводит к отдельной SQL-команде UPDATE для каждого отдельного изменённого поля — для случаев, требующих массовых изменений, стоит рассмотреть OnManualSubmit (накопление изменений локально, с последующим явным, единократным вызовом submitAll() для отправки всех накопленных изменений за один проход), что значительно эффективнее множества отдельных команд для каждого отдельного изменения.
  • Игнорирование обработки ошибок при работе с QSqlTableModel, предполагая, что операции (select(), изменение данных через интерфейс) всегда успешны — аналогично явной проверке query.exec() в примере с QSqlQuery, операции модели также могут завершиться неудачей (нарушение ограничения уникальности, проблема соединения с БД), и приложение должно явно проверять model->lastError() после потенциально неудачных операций, а не предполагать их безусловный успех.
  • Смешивание прямых SQL-запросов через отдельный QSqlQuery и операций через QSqlTableModel для одной и той же таблицы без понимания возможной несогласованности их представлений о текущем состоянии данных — если данные изменены напрямую через отдельный QSqlQuery (минуя модель), уже существующий QSqlTableModel, отображающий ту же таблицу, не узнает об этом изменении автоматически и продолжит показывать устаревшие, закешированные внутри модели данные до явного повторного вызова select() для обновления её внутреннего состояния.