SQL и Qt

QSqlTableModel и QSqlRelationalTableModel: связка БД с UI без ручного SQL

Введение

Для типичной задачи отображения и редактирования таблицы базы данных в табличном виджете Qt предоставляет QSqlTableModel, который автоматически генерирует SQL-запросы для чтения, обновления, вставки и удаления строк, избавляя от необходимости писать соответствующий SQL вручную для каждой операции. QSqlRelationalTableModel расширяет эту идею для таблиц со связями (foreign keys), автоматически подставляя человекочитаемые значения вместо идентификаторов связанных таблиц. В статье разберём оба класса на практическом примере.

Концепция

QSqlTableModel наследует QAbstractTableModel и подключается к таблице базы данных через setTable(). После select() модель загружает данные, которые можно отображать в QTableView без какого-либо дополнительного кода. Режим редактирования (setEditStrategy) определяет, когда изменения, сделанные пользователем в представлении, фактически отправляются в базу данных: немедленно при каждом изменении ячейки (OnFieldChange), при переходе к другой строке (OnRowChange), или только при явном вызове submitAll() (OnManualSubmit).

QSqlRelationalTableModel добавляет метод setRelation(), который указывает, что значение конкретного столбца на самом деле — внешний ключ на другую таблицу, и в представлении должно показываться не само число ID, а соответствующее ему отображаемое значение (например, не category_id = 3, а "Электроника").

Пример кода: QSqlTableModel и QSqlRelationalTableModel

#include <QSqlTableModel>
#include <QSqlRelationalTableModel>
#include <QSqlRelation>
#include <QTableView>
#include <QApplication>

void setupSimpleTableModel(QTableView *view)
{
    auto *model = new QSqlTableModel(view);
    model->setTable("products");
    model->setEditStrategy(QSqlTableModel::OnFieldChange); // сохранение сразу при изменении ячейки
    model->select();

    model->setHeaderData(0, Qt::Horizontal, "ID");
    model->setHeaderData(1, Qt::Horizontal, "Название");
    model->setHeaderData(2, Qt::Horizontal, "Цена");

    view->setModel(model);
}

void setupRelationalTableModel(QTableView *view)
{
    auto *model = new QSqlRelationalTableModel(view);
    model->setTable("products");

    // Столбец 3 (category_id) на самом деле ссылается на таблицу categories,
    // и в представлении должно отображаться поле "name" связанной строки
    model->setRelation(3, QSqlRelation("categories", "id", "name"));

    model->setEditStrategy(QSqlTableModel::OnManualSubmit);
    model->select();

    view->setModel(model);
    // QSqlRelationalDelegate автоматически рисует ComboBox для связанных колонок
    view->setItemDelegate(new QSqlRelationalDelegate(view));
}

bool saveChanges(QSqlTableModel *model)
{
    if (!model->submitAll()) {
        qWarning() << "Ошибка сохранения изменений:" << model->lastError().text();
        model->revertAll(); // откат несохранённых изменений в модели
        return false;
    }
    return true;
}

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

setupSimpleTableModel настраивает базовый QSqlTableModel на таблицу products с режимом OnFieldChange — изменение значения любой ячейки в QTableView немедленно отправляет соответствующий UPDATE-запрос в базу данных, без явного вызова сохранения со стороны кода. setHeaderData задаёт человекочитаемые заголовки столбцов взамен автоматически сгенерированных имён полей таблицы.

setupRelationalTableModel демонстрирует более сложный сценарий: столбец category_id (индекс 3) связывается с таблицей categories через QSqlRelation, и в QTableView для этого столбца автоматически отображается название категории, а не её числовой идентификатор — для интерактивного редактирования этого столбца через выпадающий список используется специальный QSqlRelationalDelegate. Режим OnManualSubmit в этом случае требует явного вызова submitAll() (как показано в saveChanges), что даёт возможность пользователю отменить несохранённые изменения через revertAll() перед фактической записью в базу.

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

  • Отсутствие первичного ключа в таблице или его неверное определение моделью. QSqlTableModel опирается на первичный ключ для формирования корректных UPDATE/DELETE-запросов (чтобы однозначно определить, какую строку обновлять) — для таблиц без явного первичного ключа поведение может быть некорректным, обновляя не ту строку или сразу несколько.
  • OnFieldChange для таблиц с интенсивным редактированием создаёт избыточную нагрузку на базу данных — каждое отдельное изменение ячейки превращается в отдельный SQL-запрос; для форм с массовым редактированием нескольких полей лучше подходит OnManualSubmit с одним пакетным сохранением.
  • Несинхронизация модели с изменениями данных, сделанными другим клиентом/процессом напрямую в базе данных. QSqlTableModel не отслеживает изменения, сделанные извне (другим приложением, другим экземпляром того же приложения, прямым SQL-клиентом) — для отображения актуальных данных модель нужно периодически или по явному действию пользователя обновлять через повторный вызов select().
  • Производительность QSqlRelationalTableModel на таблицах с большим количеством связанных строк. Каждая связь требует дополнительного JOIN или отдельного запроса для получения отображаемых значений — для таблиц с множеством связей и большим объёмом строк стоит проверять реальную производительность запросов, генерируемых моделью, через анализ плана выполнения СУБД.