Qt + SQLite 数据库操作 — 实战参考
Qt + SQLite 开发参考
基于 PulseQt 项目 T008(DatabaseManager)实战总结。涵盖 Qt SQL 模块的核心 API、常见业务模式和踩坑记录。
一、工程配置
CMakeLists.txt
find_package(Qt6 REQUIRED COMPONENTS Sql) # 必须加 Sql 模块
target_link_libraries(YourApp PRIVATE Qt6::Sql)
头文件
#include <QSqlDatabase> // 数据库连接
#include <QSqlQuery> // SQL 执行
#include <QSqlError> // 错误信息
#include <QDataStream> // 复杂类型序列化到 BLOB
二、连接与初始化
最小启动流程
// 1. 创建连接(命名连接,防止多实例冲突)
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "my_connection");
db.setDatabaseName("data.db");
// 2. 打开
if (!db.open()) {
qCritical() << db.lastError().text();
return false;
}
// 3. 业务操作...
// 4. 关闭 + 清理
db.close();
QSqlDatabase::removeDatabase("my_connection");
命名连接 vs 默认连接
| 方式 | 代码 | 场景 |
|---|---|---|
| 命名连接 | addDatabase("QSQLITE", "name") | 推荐,多实例、多次 open/close 不冲突 |
| 默认连接 | addDatabase("QSQLITE") | 单例、一次性使用 |
记不住的话:永远用命名连接。
三、SQL 执行 — QSqlQuery
执行模式对比
QSqlQuery query(db);
// ── 模式 1: exec() — 直接执行字符串 ──
// 适用:一次性 DDL(CREATE TABLE)、无用户输入的语句
query.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER, name TEXT)");
// ── 模式 2: prepare() + addBindValue() — 预编译 ──
// 适用:有用户输入、循环执行的 DML(INSERT/UPDATE/DELETE)
query.prepare("INSERT INTO test (id, name) VALUES (?, ?)");
query.addBindValue(1);
query.addBindValue("hello");
query.exec(); // 每次改绑定值,重复 exec()
// ── SQLite 占位符语法 ──
// SQLite 用 ? 不能用 :name
// MySQL/PostgreSQL 用 ? 或 :name
// ODBC 风格用 ? —— Qt 统一用 ?
为什么预编译更快
exec("INSERT ... VALUES (1, 'a')") 每次重新解析 SQL
exec("INSERT ... VALUES (2, 'b')") 每次重新解析 SQL
↓ 换成预编译
prepare("INSERT ... VALUES (?, ?)")
addBindValue(1); addBindValue("a"); exec() 解析 1 次
addBindValue(2); addBindValue("b"); exec() 复用执行计划
↑ 循环 1000 次,性能差距 ~30%
四、遍历查询结果
QSqlQuery query(db);
query.prepare("SELECT id, name FROM test WHERE id > ?");
query.addBindValue(10);
query.exec();
while (query.next()) { // 逐行推进
int id = query.value(0).toInt(); // 第 0 列
QString name = query.value(1).toString(); // 第 1 列
}
// ── value() 的类型转换速查 ──
query.value(0).toInt() // INTEGER → int
query.value(0).toLongLong() // INTEGER → qint64
query.value(0).toULongLong() // INTEGER → quint64(时间戳用这个)
query.value(0).toString() // TEXT → QString
query.value(0).toByteArray() // BLOB → QByteArray
query.value(0).toDouble() // REAL → double
query.value(0).toBool() // INTEGER(0/1) → bool
五、事务
为什么需要显式事务
SQLite 默认:每行 INSERT 都是一个隐式事务
→ INSERT 1 行 → 写磁盘 → fsync → 下一行
→ 1000 行 = 1000 次 fsync ≈ 很慢
显式事务:
→ BEGIN TRANSACTION
→ INSERT 1000 行(都在内存)
→ COMMIT(1 次 fsync)
→ 性能提升 10-50 倍
标准写法
// 开启
if (!db.transaction()) {
qWarning() << "transaction failed";
return;
}
// 批量操作
for (auto &row : rows) {
query.addBindValue(row.value);
if (!query.exec()) {
db.rollback(); // 任一条失败 → 回滚全部
return;
}
}
// 提交
if (!db.commit()) {
qWarning() << "commit failed";
db.rollback();
}
六、WAL 模式
默认模式 vs WAL
| 默认(DELETE) | WAL | |
|---|---|---|
| 写操作 | 直接写主文件 | 追加到 .db-wal 文件 |
| 读操作 | 被写操作阻塞 | 不阻塞(读写并发) |
| 适合场景 | 低频操作 | 高频写入 + 同时查询 |
| 文件 | data.db | data.db + data.db-wal + data.db-shm |
开启
QSqlQuery query(db);
query.exec("PRAGMA journal_mode=WAL");
// 验证
query.exec("PRAGMA journal_mode");
if (query.next())
qInfo() << query.value(0).toString(); // 输出 "wal"
注意事项
- WAL 模式下
.db-wal文件会持续增长,需定期PRAGMA wal_checkpoint合并回主文件 - 数据库正常关闭时自动 checkpoint
- 网络文件系统(NFS)可能不支持 WAL,会静默降级
七、BLOB — 复杂类型的存储
QDataStream 可将任意 Qt 基础类型 + 容器序列化到 QByteArray,存入 BLOB 列。
// ── 序列化:QVector<double> → QByteArray ──
QByteArray serialize(const QVector<double> &v) {
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_6_0); // 锁定版本
stream << v;
return data;
}
// ── 反序列化:QByteArray → QVector<double> ──
QVector<double> deserialize(const QByteArray &data) {
QVector<double> v;
QDataStream stream(data);
stream.setVersion(QDataStream::Qt_6_0);
stream >> v;
return v;
}
// ── 写入数据库 ──
query.prepare("INSERT INTO t (data) VALUES (?)");
query.addBindValue(serialize(myVector));
query.exec();
// ── 从数据库读出 ──
QVector<double> v = deserialize(query.value(0).toByteArray());
为什么 setVersion() 很重要
不设版本 = Qt 升级可能改变序列化格式 → 旧数据读不出来。Qt_6_0 锁死后兼容 Qt 6.x 全系列。
八、常见业务模式
模式 1:采集存储(PulseQt 的做法)
class DatabaseManager {
QVector<DataPoint> m_pending; // 缓冲区
void insert(const DataPoint &dp) {
m_pending.append(dp);
if (m_pending.size() >= 100) // 满 100 条 → 批量提交
commitBatch();
}
void flush() {
if (!m_pending.isEmpty()) // 析构/手动调用 → 提交剩余
commitBatch();
}
void commitBatch() {
db.transaction();
for (auto &dp : m_pending) { /* INSERT */ }
db.commit();
m_pending.clear();
}
};
模式 2:配置存储(键值对)
CREATE TABLE config (key TEXT PRIMARY KEY, value TEXT);
QString readConfig(const QString &key, const QString &defaultVal) {
QSqlQuery q(db);
q.prepare("SELECT value FROM config WHERE key = ?");
q.addBindValue(key);
q.exec();
return q.next() ? q.value(0).toString() : defaultVal;
}
void writeConfig(const QString &key, const QString &value) {
QSqlQuery q(db);
q.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)");
q.addBindValue(key); q.addBindValue(value);
q.exec();
}
模式 3:分页查询
// 第 page 页,每页 pageSize 条(page 从 1 开始)
QVector<DataPoint> queryPage(int page, int pageSize = 100) {
QSqlQuery q(db);
q.prepare("SELECT * FROM data_points ORDER BY timestamp DESC LIMIT ? OFFSET ?");
q.addBindValue(pageSize);
q.addBindValue((page - 1) * pageSize);
q.exec();
// ...遍历...
}
模式 4:自动清理旧数据
int cleanup(int retentionDays = 7) {
QSqlQuery q(db);
uint64_t cutoff = QDateTime::currentMSecsSinceEpoch()
- retentionDays * 86400000ULL;
q.prepare("DELETE FROM data_points WHERE timestamp < ?");
q.addBindValue(cutoff);
q.exec();
return q.numRowsAffected(); // 返回删除行数
}
九、踩坑记录
🐛 坑 1:忘记 removeDatabase() 导致警告
QSqlDatabasePrivate::removeDatabase: connection 'xxx' is still in use
原因:析构时没调 QSqlDatabase::removeDatabase(),或调的时候还有活跃查询。
修复:
~DatabaseManager() {
flush(); // 先提交残留数据
m_db.close(); // 再关闭连接
QSqlDatabase::removeDatabase(m_connectionName); // 最后移除
}
🐛 坑 2:多实例变量 static 计数器
// ❌ 错误 — 所有实例共用同一个连接名
m_connectionName = "pulseqt_db";
// ✅ 正确 — 每次构造生成唯一名
static int counter = 0;
m_connectionName = QString("pulseqt_db_%1").arg(++counter);
🐛 坑 3:QDataStream 版本不一致
// 写入时 Qt 6.2,读出时 Qt 6.5 → 可能不兼容
// 解决:两端都 setVersion(QDataStream::Qt_6_0)
🐛 坑 4:BLOB 字段为 NULL
// SELECT 结果中 BLOB 列可能是 NULL(从未写入过)
QByteArray blob = query.value(0).toByteArray();
if (blob.isEmpty()) return {}; // 安全处理
🐛 坑 5:事务中忘了 rollback
// ❌ — INSERT 失败后不清空缓冲,下次 commitBatch 继续失败
if (!query.exec()) {
m_db.rollback(); // 必须 rollback
m_pending.clear(); // 必须清空缓冲
return;
}
十、API 速查表
| 操作 | 代码 |
|---|---|
| 创建连接 | QSqlDatabase::addDatabase("QSQLITE", "name") |
| 打开 | db.setDatabaseName(path); db.open() |
| 关闭 | db.close() |
| 移除连接 | QSqlDatabase::removeDatabase("name") |
| 执行 SQL | QSqlQuery q(db); q.exec("SQL") |
| 预编译 | q.prepare("INSERT ... VALUES (?, ?)") |
| 绑定值 | q.addBindValue(val) |
| 遍历结果 | while (q.next()) { q.value(0).toXxx() } |
| 事务开始 | db.transaction() |
| 提交 | db.commit() |
| 回滚 | db.rollback() |
| 错误信息 | db.lastError().text() |
| 影响行数 | q.numRowsAffected() |
| WAL | q.exec("PRAGMA journal_mode=WAL") |
| 序列化 | QDataStream(&byteArray, WriteOnly) << data |
| 反序列化 | QDataStream(byteArray) >> data |
| 锁版本 | stream.setVersion(QDataStream::Qt_6_0) |
| 毫秒时间戳 | QDateTime::currentMSecsSinceEpoch() |