1. 为什么Qt要自己做一套数据库接口:驱动清单与选型逻辑
先把话说在前面:我这份笔记已经写到第三十三篇了,前面三十多篇几乎都在折腾界面、信号槽、事件循环这些东西,数据库这块一直没碰。原因也很简单——桌面应用如果只是展示数据、接收用户输入,用个配置文件、JSON、甚至二进制文件都能凑合。但一旦涉及大量结构化数据的查询、统计、多表关联,再头铁也绕不开数据库。
Qt在数据库这件事上的做法,和其他C++项目有一个非常明显的差异:它没有推荐你直接去调某一个数据库的原生API,而是自己封装了一套数据库模块,也就是QtSql。这套模块的抽象目标是:你写的数据库代码,换一个底层数据库时,尽量少改甚至不改业务逻辑。说得更直白一点,你在代码里操作的是QSqlDatabase、QSqlQuery、QSqlTableModel这些Qt类,真正的数据库驱动被藏在了后面。
1.1 从SQLite到MySQL,Qt官方支持的驱动全景
打开Qt文档,你会看到QSqlDatabase::drivers()这个函数,它能列出当前Qt版本下可用的数据库驱动。常见的包括:
- QSQLITE:SQLite驱动,内置在Qt中,不需要额外安装数据库服务,是对初学者最友好、也是我个人最推荐用来入门的一个。
- QMYSQL / QMYSQL3:MySQL驱动,旧版本还区分过3和8,新版本默认都叫QMYSQL。
- QPSQL:PostgreSQL驱动。
- QODBC:通过ODBC桥接的方式连接各种数据库,包括SQL Server、Access等。
- QOCI:Oracle的驱动。
这些驱动有一个共同点:不全是静态编译进Qt的。SQLite特殊一点,它一般作为内置驱动直接可用,其余像MySQL、PostgreSQL这些,往往需要对应版本的驱动插件,而且这个插件还和Qt的编译版本、编译器位数严格绑定。很多新同学在这里栽过跟头——代码写的没问题,一运行就提示driver not loaded。
1.2 表格式对比:针对不同项目规模怎么选
我是从实际项目角度出发做了个对比表,你在选题之前可以对照着看:
| 数据库 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| SQLite | 零配置、单文件、跨平台、Qt内置驱动 | 高并发写入弱、不适合网络服务 | 桌面客户端、小工具、原型验证 |
| MySQL | 成熟稳定、并发好、生态丰富 | 需要安装服务端、驱动配置有坑 | Web后端、客户端上报数据到中心库 |
| PostgreSQL | 功能强、支持复杂查询、JSON处理好 | 学习成本略高、驱动需要单独准备 | 数据分析、需要复杂事务和约束的业务 |
| ODBC | 可以桥接到几乎任何数据库 | 配置依赖Windows系统机制、效率略低 | 企业环境对接既有系统 |
我的建议很简单:学习阶段直接选SQLite,把Qt的API逻辑吃透;如果工作项目需要对接MySQL这类服务器型数据库,再看第二部分怎么配置驱动和连接参数。
1.3 理解Qt的"连接"到底是个什么概念
在Qt里,一个数据库连接是由连接名(connection name)唯一标识的。默认连接名是qt_sql_default_connection,你调用QSqlDatabase::addDatabase()时,如果只传驱动类型,就会使用这个默认连接名。如果同一个连接名被连续添加两次,Qt会警告并替换掉之前的连接。
我的实践经验是:连接是QSqlDatabase类的实例,不是QSqlQuery的实例。你用一个连接执行了多个查询,这是完全正常的,但不要把打开的文件、数据库句柄混在一起理解。数据库连接本身也没必要频繁开关,一个应用维护一个或少数几个长连接就够了。
2. 从零建立连接:SQLite最快跑通,MySQL远程库配置实录
2.1 SQLite连接代码:不到十行就能跑
我直接贴自己笔记里的最小可运行示例,重点部分我都加了注释:
#include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError> #include <QDebug> bool initSqliteDb() { // 加载SQLite驱动 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); // 这里的路径是数据库文件路径,文件不存在会自动创建 db.setDatabaseName("my_notes.db"); if (!db.open()) { qDebug() << "open sqlite error:" << db.lastError().text(); return false; } // 建表 QSqlQuery query(db); bool ok = query.exec("CREATE TABLE IF NOT EXISTS notes (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "title TEXT NOT NULL," "content TEXT," "created_time DATETIME DEFAULT CURRENT_TIMESTAMP)"); if (!ok) { qDebug() << "create table error:" << query.lastError().text(); return false; } return true; }这里有两个容易被忽略但很关键的细节:
第一个是setDatabaseName("my_notes.db")。SQLite里这个路径可以写相对路径,也可以写绝对路径,如果路径不存在,SQLite会尝试创建文件。但在实际项目中,我建议把数据库文件放到用户数据目录,而不是写死在程序目录里,否则程序没有写权限时,open()会失败。用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)去拿目录会更稳妥。
第二个是QSqlQuery query(db)。如果你在构造函数里没有传入db,它会使用默认连接。当程序里有多个连接时,容易出现查询打到错误连接上的问题。所以我一般都会显式传入连接对象,虽然多写一点,但排查问题的时候会很省心。
2.2 MySQL连接配置:驱动插件与连接参数的细节
MySQL和SQLite完全不同,它需要你先有一个MySQL服务端,并且Qt这边还要能找到对应版本的驱动插件。这一步是很多人卡住的地方。
先看连接代码:
#include <QSqlDatabase> #include <QSqlError> #include <QDebug> bool initMysqlDb() { QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL"); db.setHostName("127.0.0.1"); db.setPort(3306); db.setDatabaseName("qt_demo"); db.setUserName("qt_user"); db.setPassword("your_password"); // 指定连接编码,避免中文乱码 db.setConnectOptions("MYSQL_OPT_RECONNECT=1"); if (!db.open()) { qDebug() << "mysql open error:" << db.lastError().text(); return false; } return true; }这段代码看着简单,实际上坑都在看不见的地方:
- QMYSQL驱动插件和Qt版本必须匹配。如果你用的是Qt自带的预编译版本,那MySQL的驱动插件不一定带全。判断方法很简单,跑一下
qDebug() << QSqlDatabase::drivers();,如果列表里没有QMYSQL,那就是插件缺失。解决办法一般是去Qt安装目录的plugins/sqldrivers下看有没有qsqlmysql.dll或libqsqlmysql.so。没有的话,需要自己用源码编译驱动,或者换一个带完整插件的Qt发行版。 - MySQL 8.0的认证插件问题。新版MySQL默认使用
caching_sha2_password认证,而旧版Qt的QMYSQL驱动可能只支持mysql_native_password。这时候你会看到类似Authentication plugin 'caching_sha2_password' cannot be loaded的报错。处理方式有两个:要么在MySQL里为这个用户改成mysql_native_password,要么用更新版本的Qt。考虑到安全性和兼容性,我建议尽量升级Qt和驱动。 - 连接编码。MySQL连接时如果不对,中文数据存进去读出来就可能变成乱码。一般打开数据库后可以执行
SET NAMES utf8mb4,或者在表设计时统一使用utf8mb4字符集。Qt侧连接选项里也可以配合设置字符集参数。
2.3 连接建立后,先验证环境再写业务
不要在还没确认连接是否正常的时候就直接开始写CRUD。我的习惯是每次新环境测试,先跑一个最小验证程序:
QSqlDatabase db = QSqlDatabase::database("qt_sql_default_connection"); if (!db.isValid()) { qDebug() << "no valid db connection"; return; } if (!db.isOpen()) { qDebug() << "database not open"; return; } QSqlQuery query; bool ok = query.exec("SELECT VERSION()"); if (ok && query.next()) { qDebug() << "db version:" << query.value(0).toString(); }这个验证程序能一次性看出驱动是否加载成功、连接是否有效、查询通路是否顺畅。如果这里都通了,后面写业务代码会踏实很多。
3. 数据库操作的三板斧:原生SQL、模型类与事务的处理逻辑
连接建好之后,真正写业务时主要会用到三种风格:直接用QSqlQuery执行SQL语句、通过QSqlTableModel做表模型映射、以及用QSqlQueryModel做只读查询模型。下面的内容我会把三者的使用场景和选择逻辑讲清楚。
3.1 QSqlQuery:最灵活也最容易写错
QSqlQuery是最接近原生数据库编程的API,你可以拼任意合法的SQL字符串、执行带占位符的预编译语句、遍历结果集。增删改查基本就是exec一条SQL的事:
// 插入一条数据 QSqlQuery query; query.prepare("INSERT INTO notes (title, content) VALUES (?, ?)"); query.addBindValue("Qt数据库学习"); query.addBindValue("这是学习Qt数据库的第一天"); if (!query.exec()) { qDebug() << "insert error:" << query.lastError().text(); }这里有一个我认为是Qt新手最容易忽略的点:为什么推荐用prepare + bindValue而不是直接把字符串拼接进来?
第一个原因是安全。如果的值来自用户输入,比如一个QLineEdit的文本,直接拼进SQL里,一旦出现引号、百分号等特殊字符,轻则SQL语法错误,重则被人利用做SQL注入。第二个原因是性能,很多数据库对预编译语句会做执行计划缓存,反复执行同一结构、不同参数的SQL时,效率明显高于一次性拼接的执行。第三个原因是代码可读性,参数和SQL模板分离,改动起来也更安全。
查询结果集的遍历也很直白:
QSqlQuery query; query.exec("SELECT id, title, content FROM notes"); while (query.next()) { int id = query.value(0).toInt(); QString title = query.value(1).toString(); QString content = query.value(2).toString(); qDebug() << id << title << content; }需要注意,QSqlQuery::value()返回的是QVariant,你几乎可以不关心数据库里存的实际类型,直接按业务需求转成int、QString、double等即可。这层QVariant封装其实是Qt数据库模块非常好用的设计,比原生API里整天对付类型转换舒服太多。
3.2 QSqlTableModel:把数据库表直接当个"视图模型"用
如果你的业务不需要写特别复杂的SQL,只是想把某张表展示在界面上,或者做简单的增删改,QSqlTableModel是一个效率极高的选择。
#include <QSqlTableModel> QSqlTableModel *model = new QSqlTableModel(this, db); model->setTable("notes"); model->setFilter("id > 0"); model->setSort(0, Qt::AscendingOrder); model->select();setFilter相当于SQL里的WHERE子句,但注意不要带WHERE关键字。setSort设置排序字段。一旦调用select(),这个model里就会映射出表中符合条件的记录。你可以直接把它和QTableView绑定:
QTableView *view = new QTableView; view->setModel(model); view->show();表格呈现出来之后,你甚至可以在界面里直接编辑单元格,然后调用model->submitAll()将修改提交到数据库。这个模型-视图分离的机制,让CRUD的代码量一下减少很多。当然,它的代价是SQL能力受限,连表、聚合、子查询这些操作它做不了,也没必要硬用。
3.3 事务:批量操作的保护伞
在Qt里,事务操作并不复杂,核心是QSqlDatabase的三个方法:
bool ok = db.transaction(); // 开启事务 if (!ok) return; // 执行多条SQL操作 QSqlQuery query; query.exec("UPDATE notes SET title='t1' WHERE id=1"); query.exec("UPDATE notes SET content='c1' WHERE id=1"); // 判断是否需要回滚 if (needRollback) { db.rollback(); } else { db.commit(); }事务能保证一组操作要么全部成功、要么全部失败。比如你要批量insert一万条数据,每一条都单独commit的话,耗时可能非常感人;但如果包在一个事务里提交,速度能提升一个数量级。
这里要特别提一个SQLite的坑:SQLite默认是自动提交模式的。如果你不主动调用transaction(),每条SQL都会被当成独立事务立即提交。一旦中途出错,已执行的无法回滚。所以做批量操作时,务必养成先transaction()再操作的习惯。而MySQL的InnoDB引擎虽然默认也开启自动提交,但事务边界会更清晰,因为DDL、DML的行为差异比较大,认真理解commit和rollback的执行时机才能避免逻辑错误。
4. 连接数据库最容易翻车的几个场景:驱动缺失、中文乱码、连接泄漏、跨线程使用
这部分是我个人踩坑记的浓缩版。网上教程很少会把这些问题写全,但实际项目里十有八九会遇到。
4.1 驱动加载失败:报"driver not loaded"
这是我见到的最高频问题,尤其出现在MySQL上。排查链路如下:
- 先确认识别的驱动列表里有没有目标驱动。执行
qDebug() << QSqlDatabase::drivers();。 - 如果列表里没有,检查Qt安装目录的
plugins/sqldrivers是否包含对应插件文件。 - 检查插件文件是否和你使用的Qt库版本匹配。比较隐蔽的问题是:编译器位数不一致,比如程序是32位编译,插件是64位的,加载就会失败。
- 检查插件文件依赖的第三方库是否存在。MySQL驱动插件通常会依赖MySQL客户端库,如果Qt运行时找不到那个动态库,插件仍然加载不出来。
处理办法不复杂:要么去Qt对应发行版官网找完整的sqldrivers包,要么自己编译驱动插件,要么换用ODBC驱动作为备选。我个人的建议是,如果允许,MySQL方案下优先用最新稳定版Qt,因为驱动插件和连接参数的支持都更完善,可以节省很多折腾时间。
4.2 中文数据乱码:不是Qt单方面的问题
乱码问题的排查难度在于,它涉及数据库字符集、客户端编码、Qt字符串编码三个环节。你只检查一段代码往往看不出结果。
我处理这类问题的固定套路是:
- 数据库表字段和表本身使用utf8mb4字符集。
- 连接建立后,先执行
SET NAMES utf8mb4,确保客户端连接传输编码和数据库一致。 - Qt侧读出来的
QString,如果仍然乱码,检查原始字节是不是被其他环节转成了GBK或Latin1,必要时用QString::fromUtf8()或fromLocal8Bit()做转换。
在SQLite上,只要程序里源文件编码统一为UTF-8,基本不会出现乱码。MySQL的乱码概率高很多,但绝大多数都能通过统一字符集解决。写数据库代码时,我的建议是永远不要在代码里写死编码转换逻辑,而是从连接和建表阶段就把字符集统一好。
4.3 连接泄漏:反复addDatabase却不close
addDatabase()每次调用,本质上是在Qt的数据库连接管理器中注册一个新连接。如果你在一个循环里重复添加同一个连接名,Qt会警告并替换掉旧连接。旧连接在替换时可能会被立即释放,但如果数据库连接还持有未提交的事务,这里就会出现资源未被正确清理的问题。
正确做法是:应用启动阶段完成连接注册,后续所有代码通过QSqlDatabase::database()按连接名获取,不重复add。程序退出时可以用QSqlDatabase::removeDatabase()清理,但注意要在所有关联的QSqlQuery销毁之后再remove,否则会报连接仍在使用。
我实际写代码时,喜欢用一个专门类封装数据库初始化和关闭逻辑,连接名定义成静态常量,尽可能避免散落各处的addDatabase调用。
4.4 跨线程使用同一个连接:Qt这个限制尤其要重视
Qt的QSqlDatabase连接默认情况下不能跨线程使用。具体表现是:你在主线程打开了一个连接,然后在工作线程里调用QSqlQuery执行查询,要么报错,要么行为诡异。
正确的处理方式有两种:
- 每个线程维护自己的连接,即在工作线程的初始化逻辑里重新addDatabase,连接名可以不同,指向同一个数据库也没问题。
- 用Qt的信号槽机制,在工作线程里完成数据库操作后通过信号把结果传回主线程。
我的建议是:谁创建连接,谁使用连接。跨线程问题不是靠加锁能简单解决的,因为内部实现可能涉及线程局部存储。与其在错误的方向上使劲,不如直接从设计上规避。
5. 把数据接到界面上:一个TableView实例的拆解
前四节讲的都是数据库操作本身,但Qt数据库模块真正强大的地方在于它和Model/View框架的天然配合。我用笔记项目里的实际界面逻辑举个例子,这个例子对新手非常友好。
5.1 需求场景
我正在写一个本地笔记管理界面,左侧是标题列表,右侧是详情编辑区。数据存在SQLite表notes里,字段为id、title、content、created_time。我的目标是:界面启动后自动加载全部笔记标题;点击某一行时,右侧显示对应内容;新增、修改后能刷新列表。
5.2 model的初始化
在界面构造函数里,我初始化了model并绑定到QTableView:
m_model = new QSqlTableModel(this); m_model->setTable("notes"); m_model->setEditStrategy(QSqlTableModel::OnManualSubmit); m_model->select(); m_tableView->setModel(m_model); m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);setEditStrategy(QSqlTableModel::OnManualSubmit)的意思是:界面上的编辑不会立即提交数据库,而是等我确认后再调用submitAll()。这个策略的优点是避免误操作直接改了库里的数据,缺点是你必须记得手动提交。
5.3 点击行加载详情
连接QTableView的clicked信号:
connect(m_tableView, &QTableView::clicked, this, [this](const QModelIndex &index) { int row = index.row(); QString title = m_model->data(m_model->index(row, 1)).toString(); QString content = m_model->data(m_model->index(row, 2)).toString(); m_titleEdit->setText(title); m_contentEdit->setPlainText(content); });这种方式比自己去执行SQL再逐行匹配要轻松很多。因为model已经映射了整张表,行的索引和数据库记录是一一对应的。
5.4 新增、修改与提交
新增时,我直接往model尾部插一行空记录:
int row = m_model->rowCount(); m_model->insertRow(row); m_model->setData(m_model->index(row, 1), m_titleEdit->text()); m_model->setData(m_model->index(row, 2), m_contentEdit->toPlainText()); if (!m_model->submitAll()) { qDebug() << "submit error:" << m_model->lastError().text(); m_model->revertAll(); }修改时,只需对当前行的字段重新setData再submitAll。删除时,用removeRow(row),同样需要submit。
结合下来你会发现,QSqlTableModel把数据库几乎变成了一个和QStringListModel一样好用的数据源模型,开发效率提升非常明显。当然,这只是在单表CRUD场景下成立,一旦要做连表查询或者复杂统计,还是老老实实回到QSqlQuery。
5.5 模型与视图的边界:什么时候用哪种方式
我做了个简单取舍表,放在笔记里提醒自己:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 简单列表展示和编辑 | QSqlTableModel | 代码量小,和view天然配合 |
| 复杂SQL查询、统计、报表 | QSqlQuery + 自定义Model | 灵活度高,SQL能力完整 |
| 只读查询结果展示 | QSqlQueryModel | 快速填充数据,语法简洁 |
| 需要事务保护的多表操作 | QSqlQuery + transaction | 保证数据一致性,表模型不适合 |
6. 最后再聊几句实战体会
如果问这段时间调试Qt数据库最深的感受,我的答案是:先把数据库本身搞熟,再来用Qt的封装就不会慌。很多人遇到问题习惯在网上搜Qt的魔改技巧,但我发现80%的数据库连接问题其实是通用的数据库基础知识没打好——字符集、事务隔离、连接生命周期,这些概念搞清楚后,Qt层的API只是表象工具。
还有一个具体的小技巧:调试数据库代码时,尽量打印db.lastError()和query.lastError()的完整错误信息,很多问题光靠猜是猜不出来的。尤其MySQL的报错信息非常明确,照着提示改,一般都能定位到根因。
这套笔记写到这里,Qt中数据库连接与使用的基本脉络已经完整了。从SQLite起步,到MySQL的驱动配置,再到QSqlQuery、QSqlTableModel和事务处理,最后用TableView把数据展示到界面上,整个过程是一个能直接照着做的完整链路。你可以在自己的机器上跟着走一遍,甚至不需要先建好数据库服务端,直接用SQLite那份代码跑通以后,再回头对付MySQL,就能明显感受到Qt的抽象设计有多省事。