第二十章:QT数据库——QSqlDatabase、QSqlQuery、SQLite操作、数据库模型与事务处理

说实话,数据库这块内容,是我当年从C++转向QT开发时觉得最「落地」的一章。你想想看,一个软件如果没有数据持久化能力,那基本就是个玩具。QT的数据库模块设计得相当优雅,它把SQLite、MySQL、PostgreSQL这些底层数据库的差异都给你屏蔽了,你只需要学会一套API,就能操作各种数据库。

我个人习惯把QT数据库分为三层来看:连接层(QSqlDatabase)、操作层(QSqlQuery)、模型层(QSqlTableModel等)。今天我们就一层一层把它讲透。

20.1 数据库连接:QSqlDatabase

QSqlDatabase,说白了就是你和数据库之间的「管道」。没有它,你的SQL语句根本送不出去。

我见过不少新手犯一个错误:每次执行查询都新建一个QSqlDatabase对象。其实QT推荐的做法是——一个数据库连接只创建一次,然后通过连接名反复使用

核心要点:QSqlDatabase是一个值对象,但内部管理的是共享的连接句柄。不要复制它,而是通过连接名来获取。

来看一个标准的SQLite连接示例:

#include <QSqlDatabase>
#include <QSqlError>
#include <QDebug>

// 创建连接(通常放在程序初始化阶段)
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("myapp.db");  // SQLite数据库文件路径

if (!db.open()) {
    qWarning() << "数据库打开失败:" << db.lastError().text();
    return false;
}

// 后续使用时,通过连接名获取
QSqlDatabase sameDb = QSqlDatabase::database(); // 默认连接
// 或者 QSqlDatabase::database("my_connection");

我的小经验:SQLite数据库文件路径建议用QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)来获取,别硬编码。我曾经有个项目,用户反馈数据丢了,查了半天发现是路径写死了,不同系统下根本找不到文件。

20.2 执行SQL:QSqlQuery

QSqlQuery是QT中执行SQL语句的主力军。它支持两种模式:直接执行预处理执行

直接执行适合一次性操作,预处理执行适合批量操作或防止SQL注入。我个人强烈建议——只要涉及用户输入,就用预处理

20.2.1 直接执行

QSqlQuery query;
if (!query.exec("CREATE TABLE IF NOT EXISTS users ("
                "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                "name TEXT NOT NULL, "
                "age INTEGER)")) {
    qWarning() << "建表失败:" << query.lastError().text();
}

20.2.2 预处理执行(推荐)

QSqlQuery query;
query.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
query.addBindValue("张三");
query.addBindValue(25);
if (!query.exec()) {
    qWarning() << "插入失败:" << query.lastError().text();
}

// 批量插入
QVariantList names, ages;
names << "李四" << "王五" << "赵六";
ages << 30 << 28 << 35;
query.addBindValue(names);
query.addBindValue(ages);
if (!query.execBatch()) {
    qWarning() << "批量插入失败";
}

注意:预处理语句中的占位符有两种写法:?(位置绑定)和 :name(命名绑定)。我个人习惯用 ?,因为代码更简洁。但如果你参数很多,命名绑定可读性更好。

20.2.3 查询结果遍历

QSqlQuery query("SELECT id, name, age FROM users WHERE age > ?");
query.addBindValue(20);
query.exec();

while (query.next()) {
    int id = query.value(0).toInt();
    QString name = query.value("name").toString();
    int age = query.value(2).toInt();
    qDebug() << id << name << age;
}

这里有个细节:value()的参数可以是列索引(从0开始),也可以是列名。列名方式可读性更好,但性能略差一点点——不过对于绝大多数应用来说,这点差异可以忽略不计。

20.3 SQLite操作实战

SQLite是QT默认集成的嵌入式数据库,不需要额外安装服务端。它的特点就是轻量、零配置、单文件存储。我做过一个嵌入式设备项目,用的就是SQLite,跑在ARM Linux上,稳得很。

SQLite在QT中的使用,其实就三步:

  1. 添加驱动QSqlDatabase::addDatabase("QSQLITE")
  2. 设置文件路径setDatabaseName("xxx.db")
  3. 打开连接open()

嗯,这里要注意:SQLite是文件级锁,并发写入时会有锁冲突。如果你的应用有多个线程同时写数据库,建议用WAL模式(Write-Ahead Logging)来提升并发性能。

// 启用WAL模式
QSqlQuery query;
query.exec("PRAGMA journal_mode=WAL");
query.exec("PRAGMA synchronous=NORMAL");

避坑指南:我曾经在项目上线后才发现,SQLite默认的同步模式是FULL,写入速度很慢。改成NORMAL后,性能提升了3倍,而且数据安全性依然有保障。如果你的数据不是银行级别的,NORMAL完全够用。

20.4 数据库模型:QSqlTableModel

QSqlTableModel是QT提供的一个高级封装,它让你不用写SQL语句就能操作数据库表。说白了,它把数据库表映射成了一个可编辑的数据模型,直接绑定到QTableView上就能用。

我刚开始用的时候觉得这玩意儿有点「黑魔法」,后来发现它其实就是在内部帮你拼SQL。但好处是——代码量少了很多,而且自动处理了数据同步。

// 创建模型
QSqlTableModel *model = new QSqlTableModel(this);
model->setTable("users");
model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 手动提交
model->select();  // 加载数据

// 设置表头显示名
model->setHeaderData(0, Qt::Horizontal, "编号");
model->setHeaderData(1, Qt::Horizontal, "姓名");
model->setHeaderData(2, Qt::Horizontal, "年龄");

// 绑定到视图
QTableView *view = new QTableView;
view->setModel(model);
view->setSelectionBehavior(QAbstractItemView::SelectRows);
view->setEditTriggers(QAbstractItemView::DoubleClicked); // 双击编辑

编辑策略有三种:

策略 说明 适用场景
OnFieldChange 字段一改就提交 实时保存,但性能差
OnRowChange 切换到下一行时提交 常用,平衡了实时性和性能
OnManualSubmit 手动调用submitAll()才提交 需要批量修改或撤销时

我个人最常用的是OnManualSubmit,配合「保存」和「撤销」按钮,用户体验更好。

// 保存所有修改
if (model->submitAll()) {
    qDebug() << "保存成功";
} else {
    qWarning() << "保存失败:" << model->lastError().text();
    model->revertAll(); // 回滚
}

// 筛选数据
model->setFilter("age > 20");
model->select();

// 排序
model->setSort(1, Qt::AscendingOrder); // 按姓名升序
model->select();

核心理解:QSqlTableModel本质上是一个「活」的数据视图。你对模型做的任何修改,最终都会映射到SQL语句上。但要注意,它只适合单表操作,多表关联查询还是得用QSqlQueryModel或自定义模型。

20.5 事务处理

事务是数据库的「后悔药」。没有事务,你可能会遇到这种情况:转账时扣了A的钱,但给B加钱时断电了——钱就丢了。

QT中事务处理非常简单,就三个函数:

QSqlDatabase::database().transaction();  // 开始事务
// ... 执行一系列SQL操作 ...
if (allSuccess) {
    QSqlDatabase::database().commit();    // 提交
} else {
    QSqlDatabase::database().rollback();  // 回滚
}

来看一个完整的转账示例:

bool transferMoney(int fromId, int toId, double amount) {
    QSqlDatabase db = QSqlDatabase::database();
    
    if (!db.transaction()) {
        qWarning() << "事务开启失败";
        return false;
    }
    
    QSqlQuery query(db);
    
    // 扣钱
    query.prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?");
    query.addBindValue(amount);
    query.addBindValue(fromId);
    if (!query.exec()) {
        db.rollback();
        return false;
    }
    
    // 加钱
    query.prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?");
    query.addBindValue(amount);
    query.addBindValue(toId);
    if (!query.exec()) {
        db.rollback();
        return false;
    }
    
    // 检查余额是否足够(假设扣钱后余额不能为负)
    query.exec("SELECT balance FROM accounts WHERE id = " + QString::number(fromId));
    if (query.next() && query.value(0).toDouble() < 0) {
        db.rollback();
        qWarning() << "余额不足,事务回滚";
        return false;
    }
    
    return db.commit();
}

重要提醒:SQLite默认是自动提交模式,每条SQL语句都是一个独立事务。如果你不显式调用transaction(),每条INSERT/UPDATE都会触发一次磁盘写入,性能很差。批量操作时一定要用事务包裹起来,速度能提升几十倍。

我曾经接手过一个项目,里面有个批量导入功能,导入1000条数据要花30秒。我一看代码,每条INSERT都单独执行,没有事务。加上事务后,1秒就搞定了。你想想看,这差距有多大。

20.6 本章知识体系

下面这张图,是我梳理的QT数据库操作核心脉络,你看一眼就能明白各个组件之间的关系:

QT数据库操作核心架构 你的应用程序 QSqlQuery(原生SQL) QSqlTableModel(表模型) QSqlQueryModel(查询模型) QSqlDatabase(数据库连接) QSQLITE / QMYSQL / QPSQL 等驱动 SQLite文件 / MySQL服务器 / ... 🔒 事务:transaction() / commit() / rollback() 📋 模型特性: setFilter / setSort / setEditStrategy

从这张图你能看到,QT数据库的层次非常清晰。上层应用通过三种方式(原生SQL、表模型、查询模型)与数据库交互,中间经过QSqlDatabase连接层,最终由具体的数据库驱动完成实际的数据读写。事务处理则贯穿在整个操作过程中,为数据一致性保驾护航。

我的建议:初学者先从QSqlQuery入手,把SQL基础打牢。等熟悉了增删改查,再过渡到QSqlTableModel,体验一下「不用写SQL」的爽快感。至于事务,从一开始就要养成习惯——凡是涉及多条数据修改的操作,一律用事务包裹。


公众号:蓝海资料掘金营,微信deep3321