第17章 数据库编程:Qt SQL模块深度解析

说实话,数据库编程这块,我当年刚接触Qt时也踩过不少坑。Qt的SQL模块其实设计得很精巧,但如果你不了解它的底层逻辑,很容易写出又慢又容易崩的代码。今天我就带你把这套东西彻底搞明白。

本章核心:Qt SQL模块提供了从底层SQL执行到上层数据模型绑定的完整方案。说白了,就是让你既能手写SQL玩得转,也能用模型快速搞定表格展示。

17.1 Qt SQL模块的整体架构

先看一张图,把整个知识体系串起来。我每次讲这个模块,都会先画这张图给学生看——它帮你理清各个类之间的关系。

Qt SQL 模块核心架构 数据库驱动层 QSqlDriver · QSqlDriverCreator · 插件机制(QMYSQL、QPSQL、QSQLITE等) 连接管理层 QSqlDatabase · 连接名管理 · 多数据库支持 SQL执行层 QSqlQuery · 预处理语句 · 事务控制 · 批量操作 数据模型层 QSqlTableModel · QSqlRelationalTableModel · QSqlQueryModel

你看,从底层的驱动,到连接管理,再到SQL执行,最后到数据模型,每一层都有明确的职责。我个人习惯是从上往下学——先会用模型,再理解底层原理。

17.2 QSqlDatabase:连接管理

QSqlDatabase 说白了就是数据库连接的「管家」。它管理着连接名、驱动类型、连接参数这些东西。

// 添加一个SQLite数据库连接
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("mydb.sqlite");

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

// 使用默认连接名
QSqlDatabase::database().exec("SELECT 1");

我的经验:addDatabase() 可以传第二个参数指定连接名。如果你只用一个数据库,用默认连接名就够了。但如果你要同时连MySQL和SQLite,或者连两个不同的MySQL实例,那就必须给每个连接起个名字。

嗯,这里要注意:QSqlDatabase 是值类型,但内部是隐式共享的。千万别在多个线程里共享同一个 QSqlDatabase 对象——每个线程应该有自己的连接。

17.3 QSqlQuery:手写SQL的利器

QSqlQuery 是执行SQL语句的核心类。我刚开始用Qt时,总觉得它跟普通的数据库API差不多,但后来发现它有几个很实用的特性。

17.3.1 基本查询操作

QSqlQuery query;
query.exec("CREATE TABLE IF NOT EXISTS users ("
           "id INTEGER PRIMARY KEY AUTOINCREMENT,"
           "name TEXT NOT NULL,"
           "age INTEGER)");

// 插入数据
query.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
query.addBindValue("张三");
query.addBindValue(25);
query.exec();

// 查询数据
query.exec("SELECT * FROM users WHERE age > 20");
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;
}

避坑指南:我曾经在项目里直接用字符串拼接SQL,结果被SQL注入搞得很惨。后来我强制团队所有成员必须用 prepare() + addBindValue() 的方式。记住:永远不要用字符串拼接用户输入!

17.3.2 事务处理

事务这东西,说白了就是「要么全做,要么全不做」。我在做银行转账功能时深有体会——扣钱和加钱必须在一个事务里。

QSqlDatabase::database().transaction();

QSqlQuery query;
query.exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
query.exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");

if (QSqlDatabase::database().commit()) {
    qDebug() << "转账成功";
} else {
    QSqlDatabase::database().rollback();
    qWarning() << "转账失败,已回滚";
}

17.4 QSqlTableModel:表格数据模型

QSqlTableModel 是我个人最喜欢用的类之一。它把数据库表直接映射成Qt的模型,配合QTableView用起来特别爽。

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->setSelectionMode(QAbstractItemView::SingleSelection);
view->setEditTriggers(QAbstractItemView::DoubleClicked);

你想想看,以前写数据库展示要手动遍历结果集、填充QTableWidget,现在几行代码就搞定了。而且QSqlTableModel支持排序、过滤、编辑,功能很全。

关键点:setEditStrategy() 有三种策略:

  • OnFieldChange:编辑完一个字段立即提交。适合简单场景,但性能差。
  • OnRowChange:切换到下一行时提交。我一般用这个。
  • OnManualSubmit:手动调用 submitAll() 才提交。适合需要批量编辑的场景。

17.5 QSqlRelationalTableModel:外键关联

这个类解决了数据库编程中的一个经典痛点:外键字段显示。比如你的订单表里有个 user_id 字段,你不想显示数字,想显示用户名。

QSqlRelationalTableModel *model = new QSqlRelationalTableModel(this);
model->setTable("orders");

// 设置关联:orders.user_id 关联到 users.id,显示 users.name
model->setRelation(2, QSqlRelation("users", "id", "name"));

model->select();

// 设置代理,让关联字段显示为下拉框
QTableView *view = new QTableView;
view->setModel(model);
view->setItemDelegate(
    new QSqlRelationalDelegate(view));

我记得有一次做进销存系统,订单表关联了客户、商品、仓库三个外键。用 QSqlRelationalTableModel 配合 QSqlRelationalDelegate,直接生成了一个带下拉框的编辑界面,省了我至少两天的工作量。

17.6 性能优化与最佳实践

数据库编程光会用还不够,还得知道怎么用好。下面是我总结的几个要点:

场景 推荐做法 不推荐做法
大量插入 使用事务 + 批量绑定 逐条插入不提交
数据展示 QSqlTableModel + QTableView QTableWidget 手动填充
复杂查询 QSqlQuery + 预处理语句 字符串拼接SQL
多表关联 QSqlRelationalTableModel 手动JOIN + 数据映射
线程安全 每个线程独立连接 共享QSqlDatabase对象

我的小技巧:如果你要批量插入几万条数据,记得用 transaction() 包起来。我曾经测试过,不加事务插入1万条要30秒,加了事务只要0.5秒。差距就是这么夸张。

17.7 常见错误与调试

最后聊聊我踩过的坑,希望能帮你少走弯路:

  • 驱动未加载:检查 plugins/sqldrivers 目录下有没有对应的驱动文件。我遇到过好几次编译没问题,运行时报 "driver not loaded"。
  • 连接名冲突:addDatabase() 时用了重复的连接名,会覆盖之前的连接。建议用 unique 的连接名。
  • 模型未刷新:修改数据库后记得调用 model->select() 刷新。我刚开始经常忘了这步,界面半天不更新。
  • 内存泄漏:QSqlQuery 和 QSqlDatabase 虽然是值类型,但内部持有资源。确保在不再使用时调用 finish() 或 close()。

好了,Qt SQL模块的核心内容就这些。从连接管理到SQL执行,再到数据模型,每个环节都有它的设计哲学。你只要理解了这些,写数据库应用就不会再觉得头疼了。


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