输入关键词开始搜索

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.dbdata.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")
执行 SQLQSqlQuery 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()
WALq.exec("PRAGMA journal_mode=WAL")
序列化QDataStream(&byteArray, WriteOnly) << data
反序列化QDataStream(byteArray) >> data
锁版本stream.setVersion(QDataStream::Qt_6_0)
毫秒时间戳QDateTime::currentMSecsSinceEpoch()