Evgenii Legotckoi
15 серпня 2015 р. 22:31

Qt/C++ - Урок 005. QSqlRelationalTableModel - Робота з реляційними таблицями

У Qt для представлення таблиць поля, пов'язаних зовнішніми ключами з іншими таблицями бази даних, може застосовуватися QSqlRelationalTableModel , яка є більш просунутим варіантом класу QSqlTableModel , який був розглянутий у попередній статті .

Даний клас дозволяє встановлювати зв'язки між таблицями і підміняти у поданні значення таблиці, що формується значеннями з зв'язкових таблиць.

У статті розглядається варіант із двох таблиць. В одній з таблиць міститься інформація про пристрої (Ім'я хоста та IP адреса), а в другій таблиці ID цих пристроїв, за якими в другій таблиці будуть підставлятися Ім'я хоста та IP адреса відповідно до пристроїв.

Структура проекту для QSqlRelationalTableModel

Структура проекту Проект для цього уроку є доопрацьованою версією попередньої статті та залишається незмінною.


mainwindow.ui

При створенні форми додається додаткова таблиця, в якій будуть вказані пристрої. Назви таблиць такі:

  • tableView
  • tableViewDevice

DataBase.pro

Цей файл залишається незмінним, як і попередній статті.

main.cpp

Файл використовується у проекті, створений за замовчуванням.

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

mainwindow.h

Замість бібліотеки QSqlTableModel підключається бібліотека QSqlRelationalTableModel. А також додається додатковий метод, який відповідає за ініціалізацію моделі представлення таблиці пристроїв.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QSqlRelationalTableModel>
#include <QSqlRelationalDelegate>
#include <QSqlRelation>

/* Подлючаем заголовчный файл для работы с базой данных */
#include "database.h"

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow  *ui;
    /* В проекте используются объекты для работы с базой данных
     * и моделью представления таблицы базы данных
     * */
    DataBase                    *db;
    QSqlRelationalTableModel    *modelMain;
    QSqlRelationalTableModel    *modelDevice;

private:
    /* Также присутствуют два метода, которые формируют модель
     * и внешний вид TableView
     * */
    void setupMainModel(const QString &tableName, const QStringList &headers);
    void setupDeviceModel(const QString &tableName, const QStringList &headers);
    void createUI();
};

#endif // MAINWINDOW_H

mainwindow.cpp

В основному вихідному файлі ініціалізуємо моделі представлення даних та зовнішній вигляд головного вікна програми. У методі з ініціалізації основної таблиці будуть встановлені зв'язки, якими вибиратимуться дані з таблиці пристроїв.

При запуску програми в базу даних вноситься три пристрої, а потім 9 записів з випадковим призначенням ID пристроїв від 1 до 3. При першому запуску програми створюється база даних і в порожню таблицю пристроїв вносяться пристрої з ID від одного до трьох. При другому запуску ID вже буде відмінним від 1, 2 або 3. але оскільки це навчальний приклад, то обмежимося одним першим запуском програми.

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    /* Первым делом необходимо создать объект для работы с базой данных
     * и инициализировать подключение к базе данных
     * */
    db = new DataBase();
    db->connectToDataBase();

    /* После чего производим наполнение таблицы базы данных
     * контентом, который будет отображаться в tableView и tableViewDevice
     * */
    for(int i = 1; i < 4; i++){
        QVariantList data;
        data.append("Device " + QString::number(i));
        data.append("192.168.0." + QString::number(i));
        db->inserIntoDeviceTable(data);
    }

    for(int i = 0; i < 10; i++){
        QVariantList data;
        QString random = QString::number(qrand() % ((4 + 1) - 1) + 1);
        data.append(QDate::currentDate());
        data.append(QTime::currentTime());
        data.append(random);
        data.append(random);
        db->inserIntoMainTable(data);
    }

    /* Инициализируем модели для представления данных
     * с заданием названий колонок
     * */
    this->setupMainModel(TABLE,
                     QStringList() << trUtf8("id")
                                   << trUtf8("Дата")
                                   << trUtf8("Время")
                                   << trUtf8("Имя хоста")
                                   << trUtf8("IP адрес")
               );

    this->setupDeviceModel(DEVICE,
                     QStringList() << trUtf8("id")
                                   << trUtf8("Имя хоста")
                                   << trUtf8("IP адрес")
               );
    /* Инициализируем внешний вид таблицы с данными
     * */
    this->createUI();
}

MainWindow::~MainWindow()
{
    delete ui;
}

/* Метод для инициализации модели представления данных
 * */
void MainWindow::setupMainModel(const QString &tableName, const QStringList &headers)
{
    /* Производим инициализацию модели представления данных
     * с установкой имени таблицы в базе данных, по которому
     * будет производится обращение в таблице
     * */
    modelMain = new QSqlRelationalTableModel(this);
    modelMain->setTable(tableName);
    /* Устанавливаем связи с таблицей устройств, по которым будет производится
     * подстановка данных
     * В метода setRelation указывается номер колонки, в которой будет
     * производится подстановка, а также с помощью класса
     * QSqlRelation указывается имя таблицы,
     * параметр, по которому будет произведена выборка строки
     * и колонка, из которой будут взяты данные
     * */
    modelMain->setRelation(3, QSqlRelation(DEVICE, "id", DEVICE_HOSTNAME));
    modelMain->setRelation(4, QSqlRelation(DEVICE, "id", DEVICE_IP));

    /* Устанавливаем названия колонок в таблице с сортировкой данных
     * */
    for(int i = 0, j = 0; i < modelMain->columnCount(); i++, j++){
        modelMain->setHeaderData(i,Qt::Horizontal,headers[j]);
    }
    // Устанавливаем сортировку по возрастанию данных по нулевой колонке
    modelMain->setSort(0,Qt::AscendingOrder);
    modelMain->select(); // Делаем выборку данных из таблицы
}

void MainWindow::setupDeviceModel(const QString &tableName, const QStringList &headers)
{
    /* Производим инициализацию модели представления данных
     * с установкой имени таблицы в базе данных, по которому
     * будет производится обращение в таблице
     * */
    modelDevice = new QSqlRelationalTableModel(this);
    modelDevice->setTable(tableName);

    /* Устанавливаем названия колонок в таблице с сортировкой данных
     * */
    for(int i = 0, j = 0; i < modelDevice->columnCount(); i++, j++){
        modelDevice->setHeaderData(i,Qt::Horizontal,headers[j]);
    }
    // Устанавливаем сортировку по возрастанию данных по нулевой колонке
    modelDevice->setSort(0,Qt::AscendingOrder);
    modelDevice->select(); // Делаем выборку данных из таблицы
}

void MainWindow::createUI()
{
    ui->tableView->setModel(modelMain);     // Устанавливаем модель на TableView
    ui->tableView->setColumnHidden(0, true);    // Скрываем колонку с id записей
    // Разрешаем выделение строк
    ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    // Устанавливаем режим выделения лишь одно строки в таблице
    ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection);
    // Устанавливаем размер колонок по содержимому
    ui->tableView->resizeColumnsToContents();
    ui->tableView->setItemDelegate(new QSqlRelationalDelegate(ui->tableView));
    ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
    ui->tableView->horizontalHeader()->setStretchLastSection(true);

    modelMain->select(); // Делаем выборку данных из таблицы

    ui->tableViewDevice->setModel(modelDevice);     // Устанавливаем модель на TableView
    ui->tableViewDevice->setColumnHidden(0, true);    // Скрываем колонку с id записей
    // Разрешаем выделение строк
    ui->tableViewDevice->setSelectionBehavior(QAbstractItemView::SelectRows);
    // Устанавливаем режим выделения лишь одно строки в таблице
    ui->tableViewDevice->setSelectionMode(QAbstractItemView::SingleSelection);
    // Устанавливаем размер колонок по содержимому
    ui->tableViewDevice->resizeColumnsToContents();
    ui->tableViewDevice->setItemDelegate(new QSqlRelationalDelegate(ui->tableViewDevice));
    ui->tableViewDevice->setEditTriggers(QAbstractItemView::NoEditTriggers);
    ui->tableViewDevice->horizontalHeader()->setStretchLastSection(true);

    modelDevice->select();
}

база даних.h

Порівняно з прикладом про QSqlTableModel у цьому прикладі додається два нових методи, пов'язані з таблицею пристроїв, а саме створення таблиці createDeviceTable() та вставкою запису в таблицю пристроїв insertIntoDeviceTable(). А також додаються нові директиви define для таблиці пристроїв та переробляються директиви для основної таблиці.

#ifndef DATABASE_H
#define DATABASE_H

#include <QObject>
#include <QSql>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlDatabase>
#include <QFile>
#include <QDate>
#include <QDebug>

/* Директивы имен таблицы, полей таблицы и базы данных */
#define DATABASE_HOSTNAME   "ExampleDataBase"
#define DATABASE_NAME       "DataBase.db"

#define TABLE                   "MainTable"
#define TABLE_DATE              "Date"
#define TABLE_TIME              "Time"
#define TABLE_IP                "IP"
#define TABLE_HOSTNAME          "Hostname"

#define DEVICE                  "DeviceTable"
#define DEVICE_IP               "IP"
#define DEVICE_HOSTNAME         "Hostname"

class DataBase : public QObject
{
    Q_OBJECT
public:
    explicit DataBase(QObject *parent = 0);
    ~DataBase();
    /* Методы для непосредственной работы с классом
     * Подключение к базе данных и вставка записей в таблицу
     * */
    void connectToDataBase();
    bool inserIntoMainTable(const QVariantList &data);
    bool inserIntoDeviceTable(const QVariantList &data);

private:
    // Сам объект базы данных, с которым будет производиться работа
    QSqlDatabase    db;

private:
    /* Внутренние методы для работы с базой данных
     * */
    bool openDataBase();
    bool restoreDataBase();
    void closeDataBase();
    bool createMainTable();
    bool createDeviceTable();
};

#endif // DATABASE_H

database.cpp

У цьому файлі зазнали змін лише частина методів, а також додалися два нових методи порівняно з попередньою статтею .

#include "database.h"

DataBase::DataBase(QObject *parent) : QObject(parent)
{

}

DataBase::~DataBase()
{

}

/* Методы для подключения к базе данных
 * */
void DataBase::connectToDataBase()
{
    /* см. статью про QSqlTableModel */
}

/* Методы восстановления базы данных
 * */
bool DataBase::restoreDataBase()
{
    if(this->openDataBase()){
        if((!this->createMainTable()) || (!this->createDeviceTable())){
            return false;
        } else {
            return true;
        }
    } else {
        qDebug() << "Не удалось восстановить базу данных";
        return false;
    }
    return false;
}

/* Метод для открытия базы данных
 * */
bool DataBase::openDataBase()
{
    /* см. статью про QSqlTableModel */
}

/* Методы закрытия базы данных
 * */
void DataBase::closeDataBase()
{
    db.close();
}

/* Метод для создания основной таблицы в базе данных
 * */
bool DataBase::createMainTable()
{
    /* В данном случае используется формирование сырого SQL-запроса
     * с последующим его выполнением.
     * */
    QSqlQuery query;
    if(!query.exec( "CREATE TABLE " TABLE " ("
                            "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                            TABLE_DATE      " DATE            NOT NULL,"
                            TABLE_TIME      " TIME            NOT NULL,"
                            TABLE_HOSTNAME  " INTEGER         NOT NULL,"
                            TABLE_IP        " INTEGER         NOT NULL"
                        " )"
                    )){
        qDebug() << "DataBase: error of create " << TABLE;
        qDebug() << query.lastError().text();
        return false;
    } else {
        return true;
    }
    return false;
}

/* Метод для создания таблицы устройств в базе данных
 * */
bool DataBase::createDeviceTable()
{
    /* В данном случае используется формирование сырого SQL-запроса
     * с последующим его выполнением.
     * */
    QSqlQuery query;
    if(!query.exec( "CREATE TABLE " DEVICE " ("
                            "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                            DEVICE_HOSTNAME  " VARCHAR(255)    NOT NULL,"
                            DEVICE_IP        " VARCHAR(16)     NOT NULL"
                        " )"
                    )){
        qDebug() << "DataBase: error of create " << DEVICE;
        qDebug() << query.lastError().text();
        return false;
    } else {
        return true;
    }
    return false;
}

/* Метод для вставки записи в основную таблицу
 * */
bool DataBase::inserIntoMainTable(const QVariantList &data)
{
    /* Запрос SQL формируется из QVariantList,
     * в который передаются данные для вставки в таблицу.
     * */
    QSqlQuery query;
    /* В начале SQL запрос формируется с ключами,
     * которые потом связываются методом bindValue
     * для подстановки данных из QVariantList
     * */
    query.prepare("INSERT INTO " TABLE " ( " TABLE_DATE ", "
                                             TABLE_TIME ", "
                                             TABLE_HOSTNAME ", "
                                             TABLE_IP " ) "
                  "VALUES (:Date, :Time, :Hostname, :IP )");
    query.bindValue(":Date",        data[0].toDate());
    query.bindValue(":Time",        data[1].toTime());
    query.bindValue(":Hostname",    data[2].toInt());
    query.bindValue(":IP",          data[3].toInt());
    // После чего выполняется запросом методом exec()
    if(!query.exec()){
        qDebug() << "error insert into " << TABLE;
        qDebug() << query.lastError().text();
        return false;
    } else {
        return true;
    }
    return false;
}

/* Метод для вставки записи в таблицу устройств
 * */
bool DataBase::inserIntoDeviceTable(const QVariantList &data)
{
    /* Запрос SQL формируется из QVariantList,
     * в который передаются данные для вставки в таблицу.
     * */
    QSqlQuery query;
    /* В начале SQL запрос формируется с ключами,
     * которые потом связываются методом bindValue
     * для подстановки данных из QVariantList
     * */
    query.prepare("INSERT INTO " DEVICE " ( " DEVICE_HOSTNAME ", "
                                              DEVICE_IP " ) "
                  "VALUES (:Hostname, :IP )");
    query.bindValue(":Hostname",    data[0].toString());
    query.bindValue(":IP",          data[1].toString());
    // После чего выполняется запросом методом exec()
    if(!query.exec()){
        qDebug() << "error insert into " << DEVICE;
        qDebug() << query.lastError().text();
        return false;
    } else {
        return true;
    }
    return false;
}

Підсумок

В результаті виконаної роботи Програма міститиме дві таблиці. Основну таблицю, з якої в якій будуть міститися записи, що посилаються на таблицю пристроїв, і другу таблицю, яка буде містити записи про пристрої відповідно.

Програма QSqlRelationTableModel

Вам це подобається? Поділіться в соціальних мережах!

Ruslan Polupan
  • 09 лютого 2017 р. 19:32

Небольшое дополнение, столкнулся вот. При вызове setFilter к модели созданной на основе QSqlRelationalTableModel нужно указывать полный фильтр

myModel->setFilter("tableName.fielsName>100");
Evgenii Legotckoi
  • 10 лютого 2017 р. 12:23

Ну да. Приходится уточнять конкретную таблицу, к которой применяется фильтр.

Хотя для QSqlTableModel можно и без такой конкретизации обойтись.

z
  • 21 травня 2017 р. 23:11

Вставки в базу выругались "" Parameter count mismatch""

Evgenii Legotckoi
  • 21 травня 2017 р. 23:26

Показывайте код, что там писали в коде вставки и т.д. Ну и какая структура таблиц базы данных.

z
  • 22 травня 2017 р. 01:55

https://www.dropbox.com/sh/vhxcx0iyq0j4578/AACwgWPnZwNqGBndKESiXfFqa?dl=0

Evgenii Legotckoi
  • 23 травня 2017 р. 12:26

В классе DataBase указывается путь к базе данных. В данном случае C:/example/ и т.д. Так вот, у вас есть каталог example ?

z
  • 27 травня 2017 р. 14:14

Есть, там была база. Я ее удалил, теперь DeviceTable отрабатывает. На остальное матерится error insert into MainTable " Parameter count mismatch"

Evgenii Legotckoi
  • 28 травня 2017 р. 19:23

Изменяли колонки? Названия колонок? Обычно такая ошибка возникает в том случае, если в запросе на добавление не хватает информации, или есть излишняя информация. Не совпадает количество колонок и т.д.

JS
  • 11 березня 2019 р. 15:46
  • (відредаговано)

Есть таблица инредиенты с dishesId которая через setRelation получает название блюд из dishesTable, есть productId которая через setRelation получает название продукта из productTable, и столбец количества.

Вопрос: как добавлять в модель ещё один столбец с единицей измерения количества, хочется productId связать второй раз через setRelation с measureId в productTable? Надо в дальнейшем применять setFilter.
Из прешедшего в голову добавлять ещё один столбец в model и копировать в него productId и setRelation на него.

Добавление столбца и setData на него не сработала, хотя в уже имевшиеся стобцы работает.

JS
  • 12 березня 2019 р. 15:29
  • (відредаговано)

Ещё вопрос как удалять строки в QSqlRelationalTableModel? Например работает QSqlTableModel, но не в QSqlRelationalTableModel:

    QItemSelectionModel *ingredientSelectionModel = ui->ingredientsView->selectionModel();
    if (!ingredientSelectionModel->currentIndex().isValid())
    {
        qDebug() << "ingredientSelectionModel->currentIndex().isValid() error";
        return;
    }
    ingredientsModel->removeRows(ingredientSelectionModel->currentIndex().row(), 1);
    ingredientsModel->select();
Evgenii Legotckoi
  • 12 березня 2019 р. 15:41

У вас единицы измерения храняться где-то отдельно в базе данных? Если так то можете через setRelation также попытаться сделать.

Но если честно, я в итоге всегда приходил к QSqlQueryModel. Она конечно readOnly и потом приходилось реализовывать все редактирования и удаления. Но по крайней мере за счёт формирования запроса вручную можно было достаточно сложные выборки реализовывать.

Вот пример с QSqlQueryModel

Evgenii Legotckoi
  • 12 березня 2019 р. 15:42

Видите ли, думаю, что здесь ограничение из-за нескольких связей, и не удаётся правильно обработать удаление.

Вы можете сделать удаление вручную через SQL запрос, а потом просто обновить выборку в таблице.

Вот пример такого запроса в комментарии под одной из статей

JS
  • 12 березня 2019 р. 15:47

Единицы измерения лежат там же где и названия продуктов. Просто в таблице ингредиенты нет ещё одного столбца, на который можно было бы установить setRelation. Я в итоге в базе создал ещё один пустой столбец и его редактировал c setData в который получал инфу через QSqlQueryModel.
Похоже всё проще делать через QSqlQueryModel.

Evgenii Legotckoi
  • 12 березня 2019 р. 15:51

Да вы правы. На самом деле проще через QSqlQueryModel, сколько не пытался использовать эти дженерики типо QSqlTableModel и QSqlRelationalTableModel, то всегда упирался в какие-то их ограничения. В итоге проще написать недостающий функционал для QSqlQueryModel для манипуляции с данными (поскольку она сама по себе readOnly), чем пытаться обойти какие-то ограничениях в дженерик-моделях. Они недостаточно гибкие.

JS
  • 12 березня 2019 р. 16:19

Большое спасибо за разъяснения!

s
  • 03 серпня 2019 р. 17:33

А можно ли сделать связную таблицу для QTableWidget?

Evgenii Legotckoi
  • 03 серпня 2019 р. 17:51

Нет. Используйте QTableView

s
  • 03 серпня 2019 р. 19:44

А как там тогда checkbox сделать? Через BLOB?

Evgenii Legotckoi
  • 05 серпня 2019 р. 14:54

BLOB здесь вообще не при чём. Если вам нужен checkbox, то почитайте статью про делегаты

IscanderChe
  • 06 грудня 2019 р. 15:29

Для чего нужны в коде вот эти строки?

    ui->tableView->setItemDelegate(new QSqlRelationalDelegate(ui->tableView));
...
    ui->tableViewDevice->setItemDelegate(new QSqlRelationalDelegate(ui->tableViewDevice));
Павел Дорофеев
  • 31 січня 2023 р. 14:46

Хочу поделится, мы сделали свой вариант QSqlRelationalTableModel и заодно к нему новое развитие QTableView, и получилась готовая таблица PblTableDlg, у которой реализованы внешние связи, комбобоксы, чекбоксы, основные кнопки, поиск и чего там уже только нет... Настраивается все элементарно.

Открытый проект github

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up