ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Qt数据库开发:QSqlTableModel实战,从缓存提交到性能优化

Qt数据库开发:QSqlTableModel实战,从缓存提交到性能优化 直接开始。做 Qt 客户端开发这几年数据库操作这块我踩过不少坑。从最早用 QSqlQuery 一条条拼 SQL到后来用 QSqlQueryModel 做只读展示再到最终稳定用 QSqlTableModel 配合 QTableView 做数据维护这个演进过程让我明显感觉到选对模型开发效率差出来不是一星半点。这篇博文我把 QSqlTableModel 从原理到实战一次讲透不绕弯子直接上能用的代码和方案。QSqlTableModel 是 Qt SQL 模块里专门为单张表设计的可编辑数据模型它把数据库表和 QTableView 这类视图组件之间的数据同步问题封装得很好。你不需要手写大量 INSERT、UPDATE、DELETE 语句改单元格、加行删行都是对模型操作提交时再统一写回数据库。这个特性尤其适合后台管理界面、数据录入工具、本地配置管理器这类型项目。如果你正在纠结用什么方案做数据列表展示和维护或者已经被 QSqlQuery 拼 SQL 搞到崩溃这篇内容能帮你把思路彻底理清。1. 为什么是 QSqlTableModel先搞懂它解决的三个核心痛点1.1 从 QSqlQuery 到 QSqlTableModel代码量差距有多大最早我写数据库维护界面逻辑基本是用户点保存我拿界面上所有控件的值手动拼 UPDATE 语句再用 QSqlQuery 执行。听起来不复杂但一旦字段数量超过十个界面和数据库字段的映射关系就会变得难以维护。每次增删字段都得同时改界面、改映射代码、改初始化逻辑。而且表格类的数据展示往往需要支持多行编辑、排序、筛选这些用 QSqlQuery 做的话每一件事都要重写。后来我换成了 QSqlQueryModel确实能直接显示查询结果到 QTableView但它默认是只读的用户想在表格里直接改数据还得自己子类化重写 setData 和 flags 方法工作量并没有减少太多。QSqlTableModel 的定位就在这两者之间。它面向“单表操作”这个最常见的场景把 增删改查 全部封装成对模型的行和列操作。打个比方QSqlQuery 像是你手写每一封邮件而 QSqlTableModel 是给你一个填好收件人、正文模板的表单工具你只关心内容投递逻辑由它负责。虽然它不能处理复杂的多表 JOIN 查询但单表维护这个范围内的需求它的效率提升是实打实的。1.2 编辑策略与缓存机制理解 setEditStrategy 的三个选项QSqlTableModel 最核心的设计就是缓存提交机制。你在表格里改了数据默认情况下不会立刻写进数据库而是先留在模型的缓存区里等合适的时机再统一提交。这个“合适的时机”由编辑策略决定OnFieldChange只要某个字段编辑完成立刻提交到数据库。优点是实时性高缺点是每改一个格就触发一次 SQL 操作高频率修改时性能较差。OnRowChange当前行切换的时候提交。适合按行录入的场景比如配置项管理改完一行切到下一行自动保存。OnManualSubmit完全手动控制调用 submitAll() 才真正提交。这个模式我最常用因为它给了你批量处理的空间配合事务机制性能优势非常明显。我常用的做法是需要用户逐行快速录入时用 OnRowChange需要用户批量修改再统一确认时用 OnManualSubmit。两种策略配合不同的场景比一刀切要稳妥得多。1.3 信号槽机制数据变化如何自动通知视图刷新我刚开始用这个类的时候总觉得它是不是用了什么黑魔法为什么改了数据库界面能自动更新。其实就是模型视图框架的信号槽机制在起作用。模型里的数据发生变化时会发射 dataChanged、rowsInserted、rowsRemoved 等信号QTableView 监听到这些信号后会重新请求对应区域的数据来重绘界面。这个机制我自己去翻过源码确认过view 在 setModel 的时候会建立连接模型调用 beginInsertRows 和 endInsertRows 等保护方法后视图就能感知到变化。理解这个机制有什么好处当你需要在代码里批量修改数据时不会担心界面卡住不动。批量操作时最终只触发一次大规模通知效率往往比逐条操作还高。另外如果未来需要自定义模型这个信号机制也是你必须掌握的基础知识。2. 从零搭建一个可编辑的数据库表格完整实操步骤2.1 环境准备包括数据库驱动连接检查动手写代码之前先确认你的 Qt 环境能不能正常连接数据库。Qt 自带的 SQL 驱动包括 QSQLITE、QMYSQL、QPSQL、QODBC 等其中 SQLite 几乎不需要额外配置非常适合做功能验证和本地数据存储。这里有一个常见坑如果你用的是 MySQL 或 PostgreSQL除了 Qt 安装时要勾选对应模块还要把驱动库文件放到正确位置。否则程序运行时会报 “Driver not loaded”。检查驱动是否可用可以在代码里加上这段快速诊断qDebug() QSqlDatabase::drivers();如果输出列表里没有你需要的驱动类型先不要往下写任何业务代码优先解决驱动问题。编译器位数不匹配比如 32 位程序连 64 位驱动、缺少依赖 DLL是最常见的两个原因。2.2 创建模型与绑定视图核心代码逐行拆解准备一张测试表我用 SQLite 来演示表结构就定为最简单的用户信息表CREATE TABLE user_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER, city TEXT );接下来是 QSqlTableModel 的核心使用流程我按步骤拆出来// 1. 创建数据库连接 QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); db.setDatabaseName(test.db); if (!db.open()) { qDebug() 数据库打开失败 db.lastError().text(); return; } // 2. 创建模型并指定要操作的表 QSqlTableModel *model new QSqlTableModel(this, db); model-setTable(user_info); model-setEditStrategy(QSqlTableModel::OnManualSubmit); model-select(); // 执行 SELECT * FROM user_info // 3. 设置列标题默认显示字段名这一步改成中文可读性更好 model-setHeaderData(0, Qt::Horizontal, tr(ID)); model-setHeaderData(1, Qt::Horizontal, tr(姓名)); model-setHeaderData(2, Qt::Horizontal, tr(年龄)); model-setHeaderData(3, Qt::Horizontal, tr(城市)); // 4. 创建视图并绑定模型 QTableView *view new QTableView(this); view-setModel(model); // 下面两个设置推荐打开体验会好很多 view-setSelectionBehavior(QAbstractItemView::SelectRows); view-setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);到这步一个能显示数据库表内容且支持单元格编辑的界面就完成了。你双击表格里的“年龄”可以直接改数字改完后数据暂时缓存在模型里还没进数据库。这个“没进数据库”非常关键下一节重点讲提交机制。2.3 新增行、删除行与保存提交动作的正确触发方式界面上只有展示还不够增删行和保存是刚需。QSqlTableModel 提供的接口让我觉得它确实是做过实际需求考量的// 新增一行返回新行的行号 int row model-rowCount(); model-insertRow(row); // 删除当前选中行 QModelIndex index view-currentIndex(); model-removeRow(index.row()); // 保存所有未提交的修改 if (model-submitAll()) { qDebug() 保存成功; } else { qDebug() 保存失败 model-lastError().text(); model-revertAll(); // 回滚所有未提交的修改 }但是这里有几个细节你一定要注意。第一新增行后数据库表的自增主键 id 字段在提交前是无效值如果这时候读取 id 字段拿到的是一个无效的数据。第二OnManualSubmit 模式下用户改了内容不点保存直接关程序修改就会丢。第三也是最容易出问题的如果提交失败记得调用 revertAll() 回滚否则模型里的缓存数据和数据库里的实际数据是不一致的。实测下来我用 OnManualSubmit 配合简单的事务包裹一层是稳定性和用户可控性最好的一种方案。事务的问题后面专门讲。2.4 提交失败也必须回滚一条最容易忽略的要则上面代码里 revertAll() 那行注释很多参考代码都不会写但实际项目里它就是救命稻草。举个例子用户改了 20 行数据点保存的时候第 15 行违反唯一约束数据库报错。这时候如果没有 revertAll()模型里还存着所有修改用户再次点击保存依然会失败而且界面显示的数据和数据库已经不一致了排查起来非常恼火。我现在的习惯是保存操作永远按这样的模式写bool ok model-submitAll(); if (!ok) { QMessageBox::warning(this, 保存失败, model-lastError().text()); model-revertAll(); }先把失败摆到用户面前再把模型状态恢复到和数据库一致避免出现“界面显示已修改、数据库没改”的脱节状态。这一点无论你用的是哪一种编辑策略都建议遵照执行。3. 数据提交策略、事务控制与性能优化实操3.1 为什么 OnManualSubmit 事务是批量操作的性能利器我用 QSqlTableModel 做过一个导入功能一次性往表里插入三千条记录。最初的做法是逐行 insertRow setData然后马上 submitAll耗时大概在十几秒级别用户体验很差。后来我改成所有行数据先塞进模型最后统一 submitAll配合事务同样的数据量压缩到了两秒以内。QSqlTableModel 性能差距的核心不是模型本身而是提交方式。当你调用 submitAll() 的时候模型会为每一行缓存的数据生成 UPDATE 或 INSERT 语句并执行。如果不使用事务SQLite 默认每个 INSERT 语句都在独立事务中执行写入磁盘的代价非常高。包装一层事务后所有写操作在一个事务中提交磁盘写入次数大幅下降性能自然就上来了。3.2 在 submitAll 外层包装事务手写示例与参数分析事务操作放在哪一层是个需要注意的问题。QSqlTableModel 内部有自己的提交逻辑但事务需要我们手动控制推荐的做法是这样// 在提交前开启事务 QSqlDatabase::database().transaction(); bool ok model-submitAll(); if (ok) { QSqlDatabase::database().commit(); qDebug() 事务提交完成; } else { QSqlDatabase::database().rollback(); QMessageBox::warning(this, 错误, model-lastError().text()); model-revertAll(); qDebug() 事务已回滚; }这里有一个很多人忽略的细节不是所有数据库驱动都支持事务。于是我加了一层判断不支持时走普通提交分支QSqlDatabase db QSqlDatabase::database(); if (db.driver()-hasFeature(QSqlDriver::Transactions)) { // 支持事务走事务分支 } else { // 不支持事务只能逐条提交 model-submitAll(); }SQLite 和 MySQL 的驱动天然支持事务所以日常项目里基本都能走通。真正要留神的是未提交就被程序杀掉的情况这个可以通过在程序退出前检查模型是否有未提交修改提示用户确认能避免一部分数据丢失风险。3.3 大数据量导入优化insertRow 的正确姿势批量导入大文件是另一个高频场景。比如要从 CSV 或 Excel 导入几百上千条配置数据到表里这种需求如果走用户交互界面一条条录入毫无效率可言。正确做法是脱离界面直接对模型操作。db.transaction(); model-setEditStrategy(QSqlTableModel::OnManualSubmit); model-select(); for (const Record rec : records) { int row model-rowCount(); model-insertRow(row); model-setData(model-index(row, 1), rec.name); model-setData(model-index(row, 2), rec.age); model-setData(model-index(row, 3), rec.city); } if (model-submitAll()) { db.commit(); } else { db.rollback(); model-revertAll(); }实测中我注意到一个提升点直接在 model-select() 之后用 rowCount() 作为起始插入行比反复调用 rowCount() 更高效。另外如果在界面初始化时就执行大量 insertRow记得用 setUpdatesEnabled(false) 暂停界面刷新全部处理完后再恢复避免界面卡顿。大量插入时每插一行界面重绘一次是隐藏的性能杀手。3.4 数据量小时更简单的做法OnRowChange 的适用场景如果你的项目只是本地单机的小工具用户一次只改几行数据用事务加持下的 OnManualSubmit 虽然也没问题但代码量确实多了。这种场景下把 setEditStrategy 换成 OnRowChange 反而更简单。我做过一个系统配置管理工具界面非常简单一个表格允许用户修改配置项的“配置值”列。设置 OnRowChange 后用户改完当前行的配置值点击另一行这一行的修改自动提交。对于这种低频小量修改的场景你连保存按钮都可以省掉交互上轻便很多。代价是用户如果在某个单元格编辑到一半就关程序修改会丢。针对这个问题可以重写 closeEvent在关闭前检查 model-isDirty()有未提交内容时提示用户。4. 视图绑定后的高频需求排序、筛选、下拉框与自定义显示4.1 点击表头排序一行代码与它的内部实现QSqlTableModel 自带按列排序的能力底层实现是执行带 ORDER BY 的 SELECT 语句。在视图上开启排序入口非常简单view-setSortingEnabled(true);但注意这里并不是 QT 自动对当前结果排序而是模型重新发起查询由数据库引擎执行排序逻辑。如果你的表数据量很大一定要确保排序字段上有数据库索引否则每次点击表头都会执行一次全表扫描数据量大了会出现卡顿。4.2 按条件筛选数据setFilter 的学问筛选是另一个高频操作。QSqlTableModel 通过 setFilter 方法实现它接收的是 SQL WHERE 子句的条件部分// 假设有一个搜索框 m_searchEdit输入姓名关键字 QString keyword m_searchEdit-text(); model-setFilter(QString(name LIKE %%1%).arg(keyword)); model-select();这段代码有两个坑。第一SQL 注入问题。如果筛选条件来自用户输入直接用 QString::arg 拼接存在注入风险稳妥做法是用 QSqlQuery 的 bindValue 机制或者至少对用户输入做转义处理。第二如果有多个筛选条件用 AND 连接的时候条件之间留好空格字符串拼接容易把 SQL 拼坏。这个框架本身没有提供 ORM 级别的筛选器所以条件拼接的谨慎程度要按手写 SQL 的标准来。4.3 表格里放下拉框用 QComboBox 代理实现枚举列编辑业务上经常有这样的需求某一列的值只能从固定的选项里选比如“城市”列限定为“北京、上海、广州、深圳”。如果让用户随意输入脏数据很难避免。解决方式是给这一列设置一个自定义委托QStyledItemDelegate在 createEditor 方法里返回一个 QComboBox。class ComboBoxDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit ComboBoxDelegate(QObject *parent nullptr) : QStyledItemDelegate(parent) {} QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem option, const QModelIndex index) const override { QComboBox *editor new QComboBox(parent); editor-addItems(QStringList() 北京 上海 广州 深圳); return editor; } void setEditorData(QWidget *editor, const QModelIndex index) const override { QComboBox *comboBox qobject_castQComboBox *(editor); comboBox-setCurrentText(index.data().toString()); } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex index) const override { QComboBox *comboBox qobject_castQComboBox *(editor); model-setData(index, comboBox-currentText()); } }; // 视图上设置委托 view-setItemDelegateForColumn(3, new ComboBoxDelegate(view));这套代码写一次可以到处复用凡是字典类字段都可以用这个思路处理。关键是 setModelData 里最后调用了 model-setData这个 setData 会直接修改 QSqlTableModel 的缓存区后续的 submitAll 会把它作为 UPDATE 提交到数据库。所以委托里的修改天然就在整个缓存机制的保护之下。4.4 用 QDataWidgetMapper 把单行数据映射到输入框表格绑定之外还有一种常见布局左侧表格右侧一堆 QLineEdit 显示当前选中行的详细字段方便编辑长文本或敏感字段。这个用 QDataWidgetMapper 可以省掉大量手动同步代码。QDataWidgetMapper *mapper new QDataWidgetMapper(this); mapper-setModel(model); mapper-addMapping(nameEdit, 1); mapper-addMapping(ageSpinBox, 2); mapper-addMapping(cityEdit, 3); mapper-toFirst(); // 表格当前行变化时同步右侧输入框 connect(view-selectionModel(), QItemSelectionModel::currentRowChanged, mapper, QDataWidgetMapper::setCurrentModelIndex);这样右侧输入框修改的数据也是在模型缓存区生效跟表格里直接改效果一样。而且两个控件操作的是同一个 model数据双向同步不需要手写任何同步代码。我第一次用 QDataWidgetMapper 时印象很深原来 Qt 的模型视图框架早就把这类痛点设计好了解法。4.5 自定义单元格显示QStyledItemDelegate 控制颜色与格式有时候我们要根据数据值来改变显示样式比如年龄超过 60 的显示成红色。这个需求同样用委托实现重写 paint 方法void paint(QPainter *painter, const QStyleOptionViewItem option, const QModelIndex index) const override { QStyleOptionViewItem opt option; initStyleOption(opt, index); if (index.column() 2) { int age index.data().toInt(); if (age 60) { opt.foreground QBrush(Qt::red); } } QStyledItemDelegate::paint(painter, opt, index); }注意这里不能直接修改传进来的 option得先拷贝一份再改否则会影响其他单元格的绘制。这是官方文档里没有细说、但实际开发中很容易踩到的坑。5. 线程、异步与大批量更新性能优化实战5.1 为什么不能在线程里直接操作 QSqlTableModel很多 Qt 初学者会问我数据库查询很慢能不能放到 QThread 里跑免得界面卡住答案是不能直接跑。QSqlTableModel 继承自 QAbstractTableModel它和视图的交互依赖信号槽而 QTableView 的 main(ui) 操作通常发生在主线程。如果在子线程里直接修改模型等于跨线程操作 GUI 对象轻则界面刷新异常重则崩溃。正确的做法是子线程负责执行耗时查询或批量导入逻辑通过信号把结果传给主线程主线程再操作 QSqlTableModel 并提交。QSqlDatabase 连接也必须在创建它的线程中使用子线程中要使用独立的连接名创建新的连接不能直接用主线程的连接。5.2 分批加载 vs 一次性加载大数据量下的策略权衡当表里的数据量超过几万行时一次 select() 全部加载到内存界面肯定卡。我整理过一批项目常见表现是程序启动后打开主窗口要卡几秒滚动表格也有明显吊顿感。QSqlTableModel 对此没有一个现成的分页方法常用的优化方法有几种数据库端分页借助 SQL 的 LIMIT OFFSET每次只加载一部分数据滚动到底部时再加载下一页。设置视图的 batchSize 属性控制模型与视图交互时请求的行数。不过这个属性对 QSqlTableModel 的实际效果在不同 Qt 版本下表现不完全一致我建议以实测为准。加筛选条件缩小数据范围这是最简单有效的方案。业务上大多数场景都能按时间、类型等维度限定查询范围。5.3 大批量更新时暂停视图重绘如果一次操作会修改很多行比如全选后批量把“状态”字段改为“已处理”在循环里每 setData 一次视图都会收到通知并重绘。数据量一大重绘开销非常明显。可以用视图的 setUpdatesEnabled 临时禁止重绘view-setUpdatesEnabled(false); // 停止重绘 // 批量修改 for (int row 0; row model-rowCount(); row) { if (condition) { model-setData(model-index(row, statusColumn), 已处理); } } model-submitAll(); view-setUpdatesEnabled(true); // 恢复重绘并触发一次重绘这段代码看起来简单实测效果却非常显著。五百行数据、五个字段的批量修改开启与不开启这个开关UI 流畅度差距体感非常明显。6. 坑与排查QSqlTableModel 使用中的常见问题速查QSqlTableModel 用久了我积累了一批特别典型的报错和异常现象这里整理成速查表按频率从高到低排列6.1 无法显示表数据且无报错最常见的原因是表名写错或大小写不一致。注意SQLite 对表名是大小写不敏感的但 MySQL 在 Linux 平台下表名区分大小写。另外就是 setTable 之后忘了调用 select()。这个函数只是指定表名并不会立即加载数据。我见过不下十次这种低级问题。6.2 保存时报 “No fields to update” 或 “Unable to find column”通常是表中存在没有任何有效数据的空行或者主键字段没有被正确识别。QSqlTableModel 内部依赖主键来生成 UPDATE 语句的 WHERE 条件如果表没有定义主键它可能无法正确构造 UPDATE 语句。解决方案是确保表结构有唯一主键。若是 SQLiteinteger primary key 会被识别为自增主键MySQL 表则尽量用 auto_increment 主键。6.3 数据修改后刷新界面没变化先检查是否调用了 select()。select() 会重新执行数据库查询并刷新模型数据如果你修改完数据库后没有重新 select界面当然不会更新。另一个原因是模型缓存里有未提交的修改界面显示的是缓存版本与数据库不同。此时可以先 revertAll 再 select。6.4 用 QDataWidgetMapper 时数据不同步QDataWidgetMapper 与 model 的绑定是单向的默认是从模型到控件。要想控件修改回写到模型必须设置 SubmitPolicy方法是调用 mapper-setSubmitPolicy(QDataWidgetMapper::AutoSubmit)。不然的话输入框改了数据但模型缓存根本没变化点击提交也会提示没有修改。6.5 关于依赖驱动的坑这个话题搜索热度一直很高顺手多说一句。Qt 5.14 以上版本对某些数据库模块的依赖库做了调整尤其是 MySQL 驱动。如果你程序启动时提示 “Driver not loaded”除了检查驱动安装路径还要留意是不是缺少 libmysql.dll 或 libmariadb.dll 这类客户端库。通过 QSqlDatabase::drivers() 确认驱动是存在的但连接的时候又失败优先怀疑客户端库缺失。6.6 表格列顺序与数据库字段顺序不一致QSqlTableModel 默认按数据库表的字段顺序排列列。如果你的表结构做过调整新增字段默认添加在末尾界面列顺序会变化。此时不要试图手动改 model 的列顺序而是用视图的 setColumnHidden 隐藏不需要的列或者直接修改表结构顺序。在 MySQL 里你可以通过 ALTER TABLE MODIFY COLUMN 调整字段顺序SQLite 则需要重建表非必要不折腾。7. 从 QSqlTableModel 到自定义模型什么时候该更进一步QSqlTableModel 适合“单表、字段不算太多、需要标准表格交互”的场景。但项目中总会出现一些更复杂的需求比如多表 JOIN 查询结果也要支持编辑、或者需要在模型中预计算派生字段这时候 QSqlTableModel 的局限就暴露了。我的习惯是当查询 SQL 中出现 JOIN 或 GROUP BY 时换成 QSqlQueryModel 做只读数据源界面展示完全没问题如果需要编辑就基于 QAbstractTableModel 自定义模型把 SQL 查询结果缓存到一个数据结构里重写 data、setData、flags、rowCount、columnCount 等核心方法。虽然代码量会增加不少但灵活度完全不一样。如果你只是想显示查询结果不要求编辑那 QSqlQueryModel 反而是更恰当的选择没必要为不需要的功能增加复杂度。8. 最后的实战清单开发 QSqlTableModel 项目的七个建议项目实战总结下来我把我自己的固定套路整理成一份清单每次新项目都按这个顺序走省了不少不必要的调试时间先确认驱动可用跑一遍 驱动列表 和 连接测试排除环境类问题。设计表结构时务必带上主键没有主键的表编辑功能基本没法稳定工作。根据编辑频率选择策略低频少量修改用 OnRowChange中高频批量改动用 OnManualSubmit。所有 submitAll 调用点都必须配套处理失败分支和 revertAll 回滚。批量写入时开启事务别舍不得这几行代码性能差距非常大。需要限定数据范围时优先用 setFilter select不要全表加载后再过滤。界面涉及行数可能超过几百行时批量修改记得用 setUpdatesEnabled(false) 缓存重绘。9. 再分享一个实用的小技巧最后一个切实提升使用体验的习惯如果你做的是工具类软件经常需要用户频繁修改同一条记录的多个字段建议在初始化时就把表格操作方式统一设置为“整行选中 双击进入编辑 回车切换行”这会很大程度减少误操作概率。我配置过太多表格交互最终验证下来这套组合是用户上手成本最低的。QSqlTableModel 作为 Qt 数据库开发里的主力方案只要你理解了它的缓存提交机制和视图绑定原理日常开发和后期维护都会顺畅很多。希望这篇实操笔记能帮你少踩一些我踩过的坑。
返回列表