У 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
Небольшое дополнение, столкнулся вот. При вызове setFilter к модели созданной на основе QSqlRelationalTableModel нужно указывать полный фильтр
Ну да. Приходится уточнять конкретную таблицу, к которой применяется фильтр.
Хотя для QSqlTableModel можно и без такой конкретизации обойтись.
Вставки в базу выругались "" Parameter count mismatch""
Показывайте код, что там писали в коде вставки и т.д. Ну и какая структура таблиц базы данных.
https://www.dropbox.com/sh/vhxcx0iyq0j4578/AACwgWPnZwNqGBndKESiXfFqa?dl=0
В классе DataBase указывается путь к базе данных. В данном случае C:/example/ и т.д. Так вот, у вас есть каталог example ?
Есть, там была база. Я ее удалил, теперь DeviceTable отрабатывает. На остальное матерится error insert into MainTable " Parameter count mismatch"
Изменяли колонки? Названия колонок? Обычно такая ошибка возникает в том случае, если в запросе на добавление не хватает информации, или есть излишняя информация. Не совпадает количество колонок и т.д.
Есть таблица инредиенты с dishesId которая через setRelation получает название блюд из dishesTable, есть productId которая через setRelation получает название продукта из productTable, и столбец количества.
Вопрос: как добавлять в модель ещё один столбец с единицей измерения количества, хочется productId связать второй раз через setRelation с measureId в productTable? Надо в дальнейшем применять setFilter.
Из прешедшего в голову добавлять ещё один столбец в model и копировать в него productId и setRelation на него.
Добавление столбца и setData на него не сработала, хотя в уже имевшиеся стобцы работает.
Ещё вопрос как удалять строки в QSqlRelationalTableModel? Например работает QSqlTableModel, но не в QSqlRelationalTableModel:
У вас единицы измерения храняться где-то отдельно в базе данных? Если так то можете через setRelation также попытаться сделать.
Но если честно, я в итоге всегда приходил к QSqlQueryModel. Она конечно readOnly и потом приходилось реализовывать все редактирования и удаления. Но по крайней мере за счёт формирования запроса вручную можно было достаточно сложные выборки реализовывать.
Вот пример с QSqlQueryModel
Видите ли, думаю, что здесь ограничение из-за нескольких связей, и не удаётся правильно обработать удаление.
Вы можете сделать удаление вручную через SQL запрос, а потом просто обновить выборку в таблице.
Вот пример такого запроса в комментарии под одной из статей
Единицы измерения лежат там же где и названия продуктов. Просто в таблице ингредиенты нет ещё одного столбца, на который можно было бы установить setRelation. Я в итоге в базе создал ещё один пустой столбец и его редактировал c setData в который получал инфу через QSqlQueryModel.
Похоже всё проще делать через QSqlQueryModel.
Да вы правы. На самом деле проще через QSqlQueryModel, сколько не пытался использовать эти дженерики типо QSqlTableModel и QSqlRelationalTableModel, то всегда упирался в какие-то их ограничения. В итоге проще написать недостающий функционал для QSqlQueryModel для манипуляции с данными (поскольку она сама по себе readOnly), чем пытаться обойти какие-то ограничениях в дженерик-моделях. Они недостаточно гибкие.
Большое спасибо за разъяснения!
А можно ли сделать связную таблицу для QTableWidget?
Нет. Используйте QTableView
А как там тогда checkbox сделать? Через BLOB?
BLOB здесь вообще не при чём. Если вам нужен checkbox, то почитайте статью про делегаты
Для чего нужны в коде вот эти строки?
Хочу поделится, мы сделали свой вариант QSqlRelationalTableModel и заодно к нему новое развитие QTableView, и получилась готовая таблица PblTableDlg, у которой реализованы внешние связи, комбобоксы, чекбоксы, основные кнопки, поиск и чего там уже только нет... Настраивается все элементарно.
Открытый проект github