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

Файл используется в проекте, будучи созданным по умолчанию.

  1. #include "mainwindow.h"
  2. #include <QApplication>
  3.  
  4. int main(int argc, char *argv[])
  5. {
  6. QApplication a(argc, argv);
  7. MainWindow w;
  8. w.show();
  9.  
  10. return a.exec();
  11. }

mainwindow.h

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

  1. #ifndef MAINWINDOW_H
  2. #define MAINWINDOW_H
  3.  
  4. #include <QMainWindow>
  5. #include <QSqlRelationalTableModel>
  6. #include <QSqlRelationalDelegate>
  7. #include <QSqlRelation>
  8.  
  9. /* Подлючаем заголовчный файл для работы с базой данных */
  10. #include "database.h"
  11.  
  12. namespace Ui {
  13. class MainWindow;
  14. }
  15.  
  16. class MainWindow : public QMainWindow
  17. {
  18. Q_OBJECT
  19.  
  20. public:
  21. explicit MainWindow(QWidget *parent = 0);
  22. ~MainWindow();
  23.  
  24. private:
  25. Ui::MainWindow *ui;
  26. /* В проекте используются объекты для работы с базой данных
  27. * и моделью представления таблицы базы данных
  28. * */
  29. DataBase *db;
  30. QSqlRelationalTableModel *modelMain;
  31. QSqlRelationalTableModel *modelDevice;
  32.  
  33. private:
  34. /* Также присутствуют два метода, которые формируют модель
  35. * и внешний вид TableView
  36. * */
  37. void setupMainModel(const QString &tableName, const QStringList &headers);
  38. void setupDeviceModel(const QString &tableName, const QStringList &headers);
  39. void createUI();
  40. };
  41.  
  42. #endif // MAINWINDOW_H

mainwindow.cpp

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

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

  1. #include "mainwindow.h"
  2. #include "ui_mainwindow.h"
  3.  
  4. MainWindow::MainWindow(QWidget *parent) :
  5. QMainWindow(parent),
  6. ui(new Ui::MainWindow)
  7. {
  8. ui->setupUi(this);
  9.  
  10. /* Первым делом необходимо создать объект для работы с базой данных
  11. * и инициализировать подключение к базе данных
  12. * */
  13. db = new DataBase();
  14. db->connectToDataBase();
  15.  
  16. /* После чего производим наполнение таблицы базы данных
  17. * контентом, который будет отображаться в tableView и tableViewDevice
  18. * */
  19. for(int i = 1; i < 4; i++){
  20. QVariantList data;
  21. data.append("Device " + QString::number(i));
  22. data.append("192.168.0." + QString::number(i));
  23. db->inserIntoDeviceTable(data);
  24. }
  25.  
  26. for(int i = 0; i < 10; i++){
  27. QVariantList data;
  28. QString random = QString::number(qrand() % ((4 + 1) - 1) + 1);
  29. data.append(QDate::currentDate());
  30. data.append(QTime::currentTime());
  31. data.append(random);
  32. data.append(random);
  33. db->inserIntoMainTable(data);
  34. }
  35.  
  36. /* Инициализируем модели для представления данных
  37. * с заданием названий колонок
  38. * */
  39. this->setupMainModel(TABLE,
  40. QStringList() << trUtf8("id")
  41. << trUtf8("Дата")
  42. << trUtf8("Время")
  43. << trUtf8("Имя хоста")
  44. << trUtf8("IP адрес")
  45. );
  46.  
  47. this->setupDeviceModel(DEVICE,
  48. QStringList() << trUtf8("id")
  49. << trUtf8("Имя хоста")
  50. << trUtf8("IP адрес")
  51. );
  52. /* Инициализируем внешний вид таблицы с данными
  53. * */
  54. this->createUI();
  55. }
  56.  
  57. MainWindow::~MainWindow()
  58. {
  59. delete ui;
  60. }
  61.  
  62. /* Метод для инициализации модели представления данных
  63. * */
  64. void MainWindow::setupMainModel(const QString &tableName, const QStringList &headers)
  65. {
  66. /* Производим инициализацию модели представления данных
  67. * с установкой имени таблицы в базе данных, по которому
  68. * будет производится обращение в таблице
  69. * */
  70. modelMain = new QSqlRelationalTableModel(this);
  71. modelMain->setTable(tableName);
  72. /* Устанавливаем связи с таблицей устройств, по которым будет производится
  73. * подстановка данных
  74. * В метода setRelation указывается номер колонки, в которой будет
  75. * производится подстановка, а также с помощью класса
  76. * QSqlRelation указывается имя таблицы,
  77. * параметр, по которому будет произведена выборка строки
  78. * и колонка, из которой будут взяты данные
  79. * */
  80. modelMain->setRelation(3, QSqlRelation(DEVICE, "id", DEVICE_HOSTNAME));
  81. modelMain->setRelation(4, QSqlRelation(DEVICE, "id", DEVICE_IP));
  82.  
  83. /* Устанавливаем названия колонок в таблице с сортировкой данных
  84. * */
  85. for(int i = 0, j = 0; i < modelMain->columnCount(); i++, j++){
  86. modelMain->setHeaderData(i,Qt::Horizontal,headers[j]);
  87. }
  88. // Устанавливаем сортировку по возрастанию данных по нулевой колонке
  89. modelMain->setSort(0,Qt::AscendingOrder);
  90. modelMain->select(); // Делаем выборку данных из таблицы
  91. }
  92.  
  93. void MainWindow::setupDeviceModel(const QString &tableName, const QStringList &headers)
  94. {
  95. /* Производим инициализацию модели представления данных
  96. * с установкой имени таблицы в базе данных, по которому
  97. * будет производится обращение в таблице
  98. * */
  99. modelDevice = new QSqlRelationalTableModel(this);
  100. modelDevice->setTable(tableName);
  101.  
  102. /* Устанавливаем названия колонок в таблице с сортировкой данных
  103. * */
  104. for(int i = 0, j = 0; i < modelDevice->columnCount(); i++, j++){
  105. modelDevice->setHeaderData(i,Qt::Horizontal,headers[j]);
  106. }
  107. // Устанавливаем сортировку по возрастанию данных по нулевой колонке
  108. modelDevice->setSort(0,Qt::AscendingOrder);
  109. modelDevice->select(); // Делаем выборку данных из таблицы
  110. }
  111.  
  112. void MainWindow::createUI()
  113. {
  114. ui->tableView->setModel(modelMain); // Устанавливаем модель на TableView
  115. ui->tableView->setColumnHidden(0, true); // Скрываем колонку с id записей
  116. // Разрешаем выделение строк
  117. ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
  118. // Устанавливаем режим выделения лишь одно строки в таблице
  119. ui->tableView->setSelectionMode(QAbstractItemView::SingleSelection);
  120. // Устанавливаем размер колонок по содержимому
  121. ui->tableView->resizeColumnsToContents();
  122. ui->tableView->setItemDelegate(new QSqlRelationalDelegate(ui->tableView));
  123. ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
  124. ui->tableView->horizontalHeader()->setStretchLastSection(true);
  125.  
  126. modelMain->select(); // Делаем выборку данных из таблицы
  127.  
  128. ui->tableViewDevice->setModel(modelDevice); // Устанавливаем модель на TableView
  129. ui->tableViewDevice->setColumnHidden(0, true); // Скрываем колонку с id записей
  130. // Разрешаем выделение строк
  131. ui->tableViewDevice->setSelectionBehavior(QAbstractItemView::SelectRows);
  132. // Устанавливаем режим выделения лишь одно строки в таблице
  133. ui->tableViewDevice->setSelectionMode(QAbstractItemView::SingleSelection);
  134. // Устанавливаем размер колонок по содержимому
  135. ui->tableViewDevice->resizeColumnsToContents();
  136. ui->tableViewDevice->setItemDelegate(new QSqlRelationalDelegate(ui->tableViewDevice));
  137. ui->tableViewDevice->setEditTriggers(QAbstractItemView::NoEditTriggers);
  138. ui->tableViewDevice->horizontalHeader()->setStretchLastSection(true);
  139.  
  140. modelDevice->select();
  141. }

database.h

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

  1. #ifndef DATABASE_H
  2. #define DATABASE_H
  3.  
  4. #include <QObject>
  5. #include <QSql>
  6. #include <QSqlQuery>
  7. #include <QSqlError>
  8. #include <QSqlDatabase>
  9. #include <QFile>
  10. #include <QDate>
  11. #include <QDebug>
  12.  
  13. /* Директивы имен таблицы, полей таблицы и базы данных */
  14. #define DATABASE_HOSTNAME "ExampleDataBase"
  15. #define DATABASE_NAME "DataBase.db"
  16.  
  17. #define TABLE "MainTable"
  18. #define TABLE_DATE "Date"
  19. #define TABLE_TIME "Time"
  20. #define TABLE_IP "IP"
  21. #define TABLE_HOSTNAME "Hostname"
  22.  
  23. #define DEVICE "DeviceTable"
  24. #define DEVICE_IP "IP"
  25. #define DEVICE_HOSTNAME "Hostname"
  26.  
  27. class DataBase : public QObject
  28. {
  29. Q_OBJECT
  30. public:
  31. explicit DataBase(QObject *parent = 0);
  32. ~DataBase();
  33. /* Методы для непосредственной работы с классом
  34. * Подключение к базе данных и вставка записей в таблицу
  35. * */
  36. void connectToDataBase();
  37. bool inserIntoMainTable(const QVariantList &data);
  38. bool inserIntoDeviceTable(const QVariantList &data);
  39.  
  40. private:
  41. // Сам объект базы данных, с которым будет производиться работа
  42. QSqlDatabase db;
  43.  
  44. private:
  45. /* Внутренние методы для работы с базой данных
  46. * */
  47. bool openDataBase();
  48. bool restoreDataBase();
  49. void closeDataBase();
  50. bool createMainTable();
  51. bool createDeviceTable();
  52. };
  53.  
  54. #endif // DATABASE_H

database.cpp

В данном файле подверглись изменениям лишь часть методов, а также добавились два новых метода по сравнению с предыдущей статьей .

  1. #include "database.h"
  2.  
  3. DataBase::DataBase(QObject *parent) : QObject(parent)
  4. {
  5.  
  6. }
  7.  
  8. DataBase::~DataBase()
  9. {
  10.  
  11. }
  12.  
  13. /* Методы для подключения к базе данных
  14. * */
  15. void DataBase::connectToDataBase()
  16. {
  17. /* см. статью про QSqlTableModel */
  18. }
  19.  
  20. /* Методы восстановления базы данных
  21. * */
  22. bool DataBase::restoreDataBase()
  23. {
  24. if(this->openDataBase()){
  25. if((!this->createMainTable()) || (!this->createDeviceTable())){
  26. return false;
  27. } else {
  28. return true;
  29. }
  30. } else {
  31. qDebug() << "Не удалось восстановить базу данных";
  32. return false;
  33. }
  34. return false;
  35. }
  36.  
  37. /* Метод для открытия базы данных
  38. * */
  39. bool DataBase::openDataBase()
  40. {
  41. /* см. статью про QSqlTableModel */
  42. }
  43.  
  44. /* Методы закрытия базы данных
  45. * */
  46. void DataBase::closeDataBase()
  47. {
  48. db.close();
  49. }
  50.  
  51. /* Метод для создания основной таблицы в базе данных
  52. * */
  53. bool DataBase::createMainTable()
  54. {
  55. /* В данном случае используется формирование сырого SQL-запроса
  56. * с последующим его выполнением.
  57. * */
  58. QSqlQuery query;
  59. if(!query.exec( "CREATE TABLE " TABLE " ("
  60. "id INTEGER PRIMARY KEY AUTOINCREMENT, "
  61. TABLE_DATE " DATE NOT NULL,"
  62. TABLE_TIME " TIME NOT NULL,"
  63. TABLE_HOSTNAME " INTEGER NOT NULL,"
  64. TABLE_IP " INTEGER NOT NULL"
  65. " )"
  66. )){
  67. qDebug() << "DataBase: error of create " << TABLE;
  68. qDebug() << query.lastError().text();
  69. return false;
  70. } else {
  71. return true;
  72. }
  73. return false;
  74. }
  75.  
  76. /* Метод для создания таблицы устройств в базе данных
  77. * */
  78. bool DataBase::createDeviceTable()
  79. {
  80. /* В данном случае используется формирование сырого SQL-запроса
  81. * с последующим его выполнением.
  82. * */
  83. QSqlQuery query;
  84. if(!query.exec( "CREATE TABLE " DEVICE " ("
  85. "id INTEGER PRIMARY KEY AUTOINCREMENT, "
  86. DEVICE_HOSTNAME " VARCHAR(255) NOT NULL,"
  87. DEVICE_IP " VARCHAR(16) NOT NULL"
  88. " )"
  89. )){
  90. qDebug() << "DataBase: error of create " << DEVICE;
  91. qDebug() << query.lastError().text();
  92. return false;
  93. } else {
  94. return true;
  95. }
  96. return false;
  97. }
  98.  
  99. /* Метод для вставки записи в основную таблицу
  100. * */
  101. bool DataBase::inserIntoMainTable(const QVariantList &data)
  102. {
  103. /* Запрос SQL формируется из QVariantList,
  104. * в который передаются данные для вставки в таблицу.
  105. * */
  106. QSqlQuery query;
  107. /* В начале SQL запрос формируется с ключами,
  108. * которые потом связываются методом bindValue
  109. * для подстановки данных из QVariantList
  110. * */
  111. query.prepare("INSERT INTO " TABLE " ( " TABLE_DATE ", "
  112. TABLE_TIME ", "
  113. TABLE_HOSTNAME ", "
  114. TABLE_IP " ) "
  115. "VALUES (:Date, :Time, :Hostname, :IP )");
  116. query.bindValue(":Date", data[0].toDate());
  117. query.bindValue(":Time", data[1].toTime());
  118. query.bindValue(":Hostname", data[2].toInt());
  119. query.bindValue(":IP", data[3].toInt());
  120. // После чего выполняется запросом методом exec()
  121. if(!query.exec()){
  122. qDebug() << "error insert into " << TABLE;
  123. qDebug() << query.lastError().text();
  124. return false;
  125. } else {
  126. return true;
  127. }
  128. return false;
  129. }
  130.  
  131. /* Метод для вставки записи в таблицу устройств
  132. * */
  133. bool DataBase::inserIntoDeviceTable(const QVariantList &data)
  134. {
  135. /* Запрос SQL формируется из QVariantList,
  136. * в который передаются данные для вставки в таблицу.
  137. * */
  138. QSqlQuery query;
  139. /* В начале SQL запрос формируется с ключами,
  140. * которые потом связываются методом bindValue
  141. * для подстановки данных из QVariantList
  142. * */
  143. query.prepare("INSERT INTO " DEVICE " ( " DEVICE_HOSTNAME ", "
  144. DEVICE_IP " ) "
  145. "VALUES (:Hostname, :IP )");
  146. query.bindValue(":Hostname", data[0].toString());
  147. query.bindValue(":IP", data[1].toString());
  148. // После чего выполняется запросом методом exec()
  149. if(!query.exec()){
  150. qDebug() << "error insert into " << DEVICE;
  151. qDebug() << query.lastError().text();
  152. return false;
  153. } else {
  154. return true;
  155. }
  156. return false;
  157. }

Итог

В результате проделанной работы Приложение будет содержать две таблицы. Основную таблицу, из которой в которой будут содержаться записи ссылающиеся на таблицу устройств, и вторую таблицу, которая будет содержать записи об устройствах соответственно.

Приложение QSqlRelationTableModel

Вам это нравится? Поделитесь в социальных сетях!

Ruslan Polupan
  • 9 февраля 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 г. 1: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:

  1. QItemSelectionModel *ingredientSelectionModel = ui->ingredientsView->selectionModel();
  2. if (!ingredientSelectionModel->currentIndex().isValid())
  3. {
  4. qDebug() << "ingredientSelectionModel->currentIndex().isValid() error";
  5. return;
  6. }
  7. ingredientsModel->removeRows(ingredientSelectionModel->currentIndex().row(), 1);
  8. 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
  • 3 августа 2019 г. 17:33

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

Evgenii Legotckoi
  • 3 августа 2019 г. 17:51

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

s
  • 3 августа 2019 г. 19:44

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

Evgenii Legotckoi
  • 5 августа 2019 г. 14:54

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

IscanderChe
  • 6 декабря 2019 г. 15:29

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

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

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

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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь