1. 背景与定位:Qt SQL 模块是什么
Qt SQL 模块是 Qt 提供的数据库访问层(Database Access Layer),它把"如何连接数据库、如何执行 SQL、如何把查询结果变成界面可展示的数据"抽象成一套统一、跨数据库的 C++ API。无论底层是 SQLite、MySQL、PostgreSQL、ODBC 还是 Oracle,业务代码面对的始终是同一组类:QSqlDatabase、QSqlQuery、QSqlTableModel......
1.1 模块组成总览
Qt SQL 模块由三大部分组成:
| 类别 | 代表类 | 职责 |
|---|---|---|
| 连接管理 | QSqlDatabase | 数据库连接的建立、获取、关闭、销毁;驱动注册与可用驱动查询 |
| SQL 执行 | QSqlQuery、QSqlError、QSqlDriver | 执行 SQL 语句、预处理语句与参数绑定、事务控制、错误信息;驱动层抽象 |
| 数据模型 | QSqlQueryModel、QSqlTableModel、QSqlRelationalTableModel | 把 SQL 查询/整张表包装成 Model/View 架构中的 Model,直接喂给 QTableView 等视图 |
| 元数据 | QSqlRecord、QSqlField、QSqlIndex | 描述行/列结构与字段属性(类型、长度、是否主键等),供模型层和动态建表使用 |
在 Qt 6 中该模块对应的库文件为 Qt6Sql(Qt 5 为 Qt5Sql),头文件统一以 #include <QSql...> 引入。
1.2 设计哲学:统一抽象 + 驱动插件
Qt SQL 的核心设计是**"接口统一、驱动隔离"**:
- 上层:QSqlDatabase / QSqlQuery 等类只面向驱动抽象接口 QSqlDriver 编程;
- 下层:每个具体数据库(SQLite/MySQL/PostgreSQL/ODBC)实现一个 QSqlDriver 子类,以插件形式加载(Qt 5 也可静态编译进库)。
这意味着:同一份业务代码,切换数据库后端只需改连接参数(连接名/驱动名/主机/端口/用户名/密码),其余 SQL 与模型代码几乎不用动------前提是你只使用 SQL 标准语法、不依赖特定数据库方言。
2. 使用方式:工程配置与驱动加载
2.1 CMake 配置(Qt 6 推荐)
cmake_minimum_required(VERSION 3.16)
project(QtSqlDemo)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 启用 Qt 的 AUTOMOC / AUTOUIC / AUTORCC(元对象系统需要)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Sql)
qt_standard_project_setup() # Qt 6.3+,内部自动打开 AUTOMOC 等
add_executable(QtSqlDemo main.cpp)
target_link_libraries(QtSqlDemo PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Sql)
要点:
- find_package(Qt6 COMPONENTS Sql) 中的 Sql 必须显式列出,否则链接 Qt6::Sql 会失败;
- Qt 5 对应写法为 find_package(Qt5 COMPONENTS Sql) + Qt5::Sql;
- 若同时使用 QSqlTableModel 与视图,务必同时链接 Widgets(QTableView 属于 Widgets 模块)。
2.2 qmake 配置(Qt 5/6 均可)
QT += core gui widgets sql
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = QtSqlDemo
TEMPLATE = app
SOURCES += main.cpp
2.3 数据库驱动加载流程
Qt SQL 使用插件机制加载驱动。常见驱动名与对应数据库:
| 驱动名(driver name) | 数据库 | 备注 |
|---|---|---|
| QSQLITE | SQLite 3 | Qt 自带,免配置,最常用 |
| QMARIADB / QMYSQL | MariaDB / MySQL | 需 libmysql 客户端库 |
| QPSQL | PostgreSQL | 需 libpq |
| QODBC | 通过 ODBC 访问(含 SQL Server) | Windows 常用 |
| QSQLSERVER | SQL Server(Qt 5.11+) | 基于 TDS |
| QOCI | Oracle | 需 Oracle 客户端库 |
| QDB2 | IBM DB2 | 需客户端库 |
| QSQLITE3 | SQLite 3(Qt 6.5+ 独立插件) | 与 QSQLITE 区别见 FAQ |
加载流程与检查代码:
cpp
#include <QCoreApplication>
#include <QSqlDatabase>
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
// 1. 列出当前可用的驱动(判断驱动是否编译进 Qt)
qDebug() << "Available drivers:";
const QStringList drivers = QSqlDatabase::drivers();
for (const QString &d : drivers)
qDebug() << " -" << d;
// 2. 判断特定驱动是否可用
if (!QSqlDatabase::isDriverAvailable("QSQLITE")) {
qCritical() << "QSQLITE driver not loaded!";
return 1;
}
// 3. 创建连接(此时不真正连接数据库)
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(":memory:");
qDebug() << "Driver:" << db.driverName();
return 0;
}
驱动加载机制要点:
- QSqlDatabase::drivers() 返回当前 Qt 构建中已编译/已加载的驱动名列表;
- QSqlDatabase::contains(connectionName) 判断某个连接(不是驱动)是否已注册;
- 驱动插件搜索路径:Qt 安装目录的 plugins/sqldrivers/ 子目录(Windows 上还有 sqldrivers 目录)。若部署应用时漏拷插件,运行时会报 QSqlDatabase: QSQLITE driver not loaded(详见 FAQ);
- 静态构建时需在工程中显式链接驱动插件,如 Qt6::QSQLiteDriverPlugin。
3. API 解释:核心类逐个击破
3.1 QSqlDatabase:连接管理
QSqlDatabase 是 Qt SQL 的入口,负责连接的生命周期 。注意它是个值语义句柄,内部引用一个共享的连接对象;同一个连接名对应同一个底层连接。
| 静态方法 | 签名与作用 |
|---|---|
| addDatabase | static QSqlDatabase addDatabase(const QString &type, const QString &connectionName = QLatin1String(defaultConnection)); 注册一个新连接,返回句柄。type 为驱动名(如 QSQLITE) |
| database | static QSqlDatabase database(const QString &connectionName = QLatin1String(defaultConnection), bool open = true); 获取已有连接句柄;open=true 时若未打开会自动尝试打开 |
| removeDatabase | static void removeDatabase(const QString &connectionName); 移除并销毁连接。前提是该连接上所有 QSqlQuery/QSqlDatabase 对象已析构,否则打印 connection is still in use 警告 |
| contains | static bool contains(const QString &connectionName); 判断连接是否已注册 |
| drivers | static QStringList drivers(); 返回可用驱动名列表 |
| isDriverAvailable | static bool isDriverAvailable(const QString &name); 判断驱动是否可用 |
| 实例方法 | 作用 |
|---|---|
| open() / open(user, password) | 打开连接,成功返回 true |
| close() | 关闭连接(不注销) |
| isOpen() / isOpenError() | 是否已打开 / 打开是否出错 |
| setDatabaseName / databaseName | 设置/获取数据库名(SQLite 为文件路径或 :memory:) |
| setHostName / setPort / setUserName / setPassword | 网络数据库的连接参数 |
| setConnectOptions | 驱动特定选项(如 SQLite 的 QSQLITE_BUSY_TIMEOUT、MySQL 的 MYSQL_OPT_RECONNECT) |
| lastError() | 返回最近一次操作的 QSqlError |
| transaction() / commit() / rollback() | 事务控制(部分驱动不支持,需先 driver()->hasFeature(QSqlDriver::Transactions)) |
| tables(type) | 返回数据库中表名的字符串列表(按 QSql::Tables/Views/AllTables 过滤) |
| record(tableName) | 返回表的 QSqlRecord(结构信息) |
| driver() | 返回底层 QSqlDriver* |
典型连接流程:
cpp
// 打开 SQLite 文件数据库
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); // 默认连接
db.setDatabaseName("app_data.db"); // 文件不存在会自动创建
if (!db.open()) {
qCritical() << "Open failed:" << db.lastError().text();
}
命名连接与多连接:
cpp
// 同时使用多个数据库:用 connectionName 区分
QSqlDatabase dbUser = QSqlDatabase::addDatabase("QSQLITE", "user_db");
dbUser.setDatabaseName("users.db");
dbUser.open();
QSqlDatabase dbLog = QSqlDatabase::addDatabase("QSQLITE", "log_db");
dbLog.setDatabaseName("logs.db");
dbLog.open();
// 使用时必须指定连接名
QSqlQuery q(dbUser);
// 或 QSqlQuery q; q.exec("...") 会使用默认连接,需 dbUser 为默认连接
3.2 QSqlQuery:执行 SQL 与预处理语句
QSqlQuery 是执行 SQL 的主力。两种构造方式:
- 不传连接:使用默认连接(defaultConnection)创建的连接;
- 显式传 QSqlDatabase:使用指定连接。
3.2.1 直接执行 SQL
cpp
QSqlQuery query;
// DDL:建表
bool ok = query.exec(
"CREATE TABLE IF NOT EXISTS person ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" age INTEGER,"
" email TEXT)"
);
if (!ok) qWarning() << query.lastError().text();
// DML:插入
ok = query.exec("INSERT INTO person (name, age, email) "
"VALUES ('Alice', 30, 'alice@example.com')");
3.2.2 预处理语句与参数绑定(重点)
永远不要用字符串拼接 SQL------那是 SQL 注入与转义地狱的根源。Qt 提供两种占位符:
① 命名占位符(推荐)------以 :name 形式:
cpp
QSqlQuery query;
query.prepare("INSERT INTO person (name, age, email) "
"VALUES (:name, :age, :email)");
query.bindValue(":name", "Bob");
query.bindValue(":age", 25);
query.bindValue(":email", "bob@example.com");
query.exec();
② 位置占位符------以 ? 形式,按顺序绑定:
cpp
QSqlQuery query;
query.prepare("SELECT * FROM person WHERE age > ? AND name LIKE ?");
query.addBindValue(20); // 对应第一个 ?
query.addBindValue("%o%"); // 对应第二个 ?
query.exec();
绑定参数 API:
| 方法 | 作用 |
|---|---|
| prepare(sql) | 预处理 SQL,返回 bool;失败可用 lastError() 查看 |
| bindValue(nameOrPos, value, type = QSql::In) | 按占位符名或序号绑定值;type 可为 QSql::In/Out/InOut(存储过程用) |
| addBindValue(value) | 按顺序追加绑定(位置占位符用) |
| boundValue(nameOrPos) | 获取已绑定的值 |
| boundValues() | 返回全部绑定值的 QMap |
绑定的关键好处:
- 防注入:值由驱动层做类型化处理,恶意输入只会被当作"值"而非"SQL 代码";
- 性能:同一 SQL 只解析一次,循环中反复执行只换绑定值,避免重复解析(数据库端可复用执行计划);
- 类型安全:QVariant 自动完成 C++ 类型 ↔ 数据库类型的映射。
批量插入的高效写法:
cpp
QSqlQuery query;
query.prepare("INSERT INTO person (name, age) VALUES (?, ?)");
QVariantList names{"A", "B", "C"};
QVariantList ages{20, 30, 40};
query.addBindValue(names);
query.addBindValue(ages);
query.execBatch(); // 批量执行,一次网络往返(驱动支持时)
3.2.3 遍历查询结果
cpp
QSqlQuery query("SELECT id, name, age FROM person WHERE age >= 18");
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;
}
// query.size() 在 SQLite/MySQL 上可能返回 -1(驱动不支持提前知道行数),
// 遍历过程中可用 query.at() 判断当前位置,首行前为 -1,有效行从 0 开始
相关方法:next()(前进一行,成功 true)、previous()、first()、last()、seek(n)、at()(当前位置)、isActive()(查询是否有效)、numRowsAffected()(受影响行数)、record()(结果集 QSqlRecord)、value(i) / value(name)(取值,返回 QVariant)。
3.3 QSqlError:错误处理
所有 Qt SQL 操作的失败信息都封装在 QSqlError 中。
cpp
QSqlQuery query;
if (!query.exec("SELECT * FROM no_such_table")) {
QSqlError err = query.lastError();
qDebug() << "Type:" << err.type(); // QSqlError::StatementError 等
qDebug() << "Database:" << err.databaseText(); // 数据库原生错误信息
qDebug() << "Driver:" << err.driverText(); // 驱动层错误信息
qDebug() << "Combined:" << err.text(); // databaseText + driverText 合并
qDebug() << "Native code:" << err.nativeErrorCode(); // 数据库原生错误码(如 SQLite 的 1)
}
err.type() 返回枚举 QSqlError::ErrorType:
| 枚举值 | 含义 |
|---|---|
| NoError | 无错误 |
| ConnectionError | 连接错误(驱动/网络) |
| StatementError | SQL 语句错误(语法、表不存在等) |
| TransactionError | 事务错误 |
| UnknownError | 未知错误 |
3.4 事务:beginTransaction / commit / rollback
事务保证一组操作的原子性:要么全部成功,要么全部回滚。
cpp
QSqlDatabase db = QSqlDatabase::database();
if (!db.driver()->hasFeature(QSqlDriver::Transactions)) {
qWarning() << "Driver does not support transactions";
return;
}
if (!db.transaction()) {
qCritical() << "Begin transaction failed:" << db.lastError().text();
return;
}
QSqlQuery query;
bool ok = true;
ok &= query.exec("INSERT INTO person (name, age) VALUES ('C', 33)");
ok &= query.exec("INSERT INTO person (name, age) VALUES ('D', 44)");
// 假设第二条失败:全部回滚,C 也不会写入
if (!ok) {
if (!db.rollback()) qCritical() << "Rollback failed";
qWarning() << "Transaction rolled back";
return;
}
if (!db.commit()) {
qCritical() << "Commit failed:" << db.lastError().text();
db.rollback();
}
注意:
- 三个方法都是 QSqlDatabase 的成员方法(不是 QSqlQuery 的);
- 必须保证连接是打开的;事务中任何 SQL 失败都要主动 rollback;
- SQLite 默认自动提交(每条语句一个事务),批量写入不包事务会极慢------手动事务可提升几个数量级;
- 嵌套事务:SQLite 不支持标准嵌套事务,可用 SAVEPOINT 模拟(Qt 驱动未封装,需原生 SQL)。
3.5 数据库模型:QueryModel / TableModel / RelationalTableModel
这是 Qt SQL 与 Model/View 篇衔接的核心------把数据库数据直接变成 QTableView 可展示的 Model。
| 类 | 数据源 | 可编辑 | 提交方式 | 适用 |
|---|---|---|---|---|
| QSqlQueryModel | 任意 SELECT 查询结果 | 只读 | --- | 自定义查询、报表展示 |
| QSqlTableModel | 整张数据库表 | 可编辑 | submitAll() / revertAll() | 单表 CRUD |
| QSqlRelationalTableModel | 表 + 外键关联 | 可编辑 | submitAll() | 外键显示为关联表字段 |
3.5.1 QSqlQueryModel:只读查询模型
cpp
QSqlQueryModel *model = new QSqlQueryModel;
model->setQuery("SELECT id, name, age FROM person ORDER BY age DESC");
// 设置表头(默认是数据库列名)
model->setHeaderData(0, Qt::Horizontal, tr("ID"));
model->setHeaderData(1, Qt::Horizontal, tr("姓名"));
model->setHeaderData(2, Qt::Horizontal, tr("年龄"));
QTableView *view = new QTableView;
view->setModel(model);
view->show();
3.5.2 QSqlTableModel:单表 CRUD 模型
cpp
QSqlTableModel *model = new QSqlTableModel(nullptr, db);
model->setTable("person");
model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 手动提交
model->select(); // 执行 SELECT,填充数据
// 展示
QTableView *view = new QTableView;
view->setModel(model);
view->show();
// 插入一行
int row = model->rowCount();
model->insertRow(row);
model->setData(model->index(row, 1), "NewUser"); // 列 1 为 name
model->setData(model->index(row, 2), 18); // 列 2 为 age
model->submitAll(); // 提交到数据库
// 删除一行
model->removeRow(2);
model->submitAll();
// 过滤
model->setFilter("age > 20");
model->select();
// 排序
model->setSort(1, Qt::AscendingOrder);
model->select();
setEditStrategy 三种策略:
| 策略 | 行为 | 适用 |
|---|---|---|
| OnFieldChange | 字段一改就立即写库 | 简单快速,但频繁写库 |
| OnRowChange | 当前行切换时提交 | 逐行编辑 |
| OnManualSubmit | 手动 submitAll() 提交 | 批量编辑、支持 revertAll() 撤销 |
3.5.3 QSqlRelationalTableModel:外键关联显示
场景:person 表有 city_id 外键,希望界面上直接显示城市名而不是 id。
cpp
QSqlRelationalTableModel *model = new QSqlRelationalTableModel(nullptr, db);
model->setTable("person");
// 把第 3 列(city_id)关联到 city 表的 id 字段,显示 city 表的 name 字段
model->setRelation(3, QSqlRelation("city", "id", "name"));
model->select();
QTableView *view = new QTableView;
view->setModel(model);
// 让外键列显示为下拉框(关联编辑代理)
view->setItemDelegate(new QSqlRelationalDelegate(view));
view->show();
3.6 元数据:QSqlDriver / QSqlRecord / QSqlField
3.6.1 QSqlDriver
QSqlDriver 是所有数据库驱动的抽象基类,也是能力检测入口:
cpp
QSqlDriver *driver = db.driver();
// 驱动是否支持事务
if (driver->hasFeature(QSqlDriver::Transactions)) { ... }
// 是否支持预处理语句(全部主流驱动都支持,老驱动可能只支持 SQL)
if (driver->hasFeature(QSqlDriver::PreparedQueries)) { ... }
// 是否支持 BLOB(图片/文件)
if (driver->hasFeature(QSqlDriver::BLOB)) { ... }
// 是否支持 lastInsertId(取插入的自增主键)
if (driver->hasFeature(QSqlDriver::LastInsertId)) { ... }
QSqlDriver::Feature 枚举还有:QuerySize、BatchOperations、SimpleLocking、LowPrecisionNumbers、EventNotifications、FinishQuery、MultipleResultSets、CancelQuery。
3.6.2 QSqlRecord
描述一行记录的结构(字段列表),可从表或查询结果获取:
cpp
// 从表结构获取
QSqlRecord rec = db.record("person");
qDebug() << "Field count:" << rec.count();
qDebug() << "Field name 0:" << rec.fieldName(0);
// 从查询结果获取
QSqlQuery query("SELECT * FROM person");
QSqlRecord rec2 = query.record();
int nameIdx = rec2.indexOf("name"); // 按名字找索引
常用方法:count()、field(i) / field(name)、fieldName(i)、indexOf(name)、contains(name)、value(i) / setValue(i, v)、setNull(i)、isNull(i)、isEmpty()、clear()、append(field)、insert(idx, field)、remove(i)。
3.6.3 QSqlField
描述单个字段的属性:
cpp
QSqlField f = rec.field(1);
qDebug() << "Name:" << f.name();
qDebug() << "Type:" << f.type(); // QVariant::Type,如 QMetaType::QString
qDebug() << "Required:" << f.isRequired(); // 是否 NOT NULL
qDebug() << "Length:" << f.length(); // 字段长度(部分驱动有意义)
qDebug() << "Precision:" << f.precision();
qDebug() << "AutoValue:" << f.isAutoValue(); // 是否自增列(SQLite INTEGER PRIMARY KEY)
qDebug() << "Default:" << f.defaultValue();
f.setValue("Alice"); // 修改值
QSqlField 常与 QSqlRecord 配合做动态建表 与通用导入导出(如 CSV 导入)。
4. 使用场景:什么时候该用 Qt SQL
4.1 场景决策树
需要持久化数据?
├─ 只是简单键值对(几 KB~MB)?
│ └─ 考虑 QSettings(注册表/ini/json)——不一定要上数据库
├─ 结构化、有关系、要查询/排序/统计?
│ ├─ 单机嵌入式、零配置 → SQLite(Qt SQL 的 QSQLITE 驱动)
│ ├─ 已有 MySQL/PostgreSQL 服务器 → 对应驱动
│ └─ 企业环境、走 ODBC → QODBC
└─ 数据量极大、OLAP 分析型?
└─ 见系列中 DuckDB 篇(Qt SQL 侧重 OLTP 应用内访问)
4.2 典型场景详述
| 场景 | 选型建议 | 说明 |
|---|---|---|
| 本地配置存储 | SQLite + QSqlTableModel | 配置项多、有结构时优于 QSettings;天然支持事务回滚 |
| 日志记录 | SQLite + 事务批量写 | 比文本日志更易查询;包事务 + WAL 模式大幅提升写入吞吐 |
| 桌面数据管理系统 | QSqlTableModel / RelationalTableModel + QTableView | 通讯录、进销存、订单管理等经典 CRUD 桌面应用 |
| 报表导出 | QSqlQueryModel + 查询 | 复杂聚合查询只读展示;可再导出 CSV/Excel/PDF |
| 与 Model/View 集成 | QSqlRelationalTableModel + QSqlRelationalDelegate | 外键下拉、主从表联动、搜索过滤(配 QSortFilterProxyModel) |
| 离线/边缘计算设备 | SQLite 嵌入式 | 零配置、单文件、跨平台,桌面/移动/嵌入式通吃 |
4.3 Qt SQL vs 其他数据方案的边界
| 方案 | 定位 | 与 Qt SQL 的关系 |
|---|---|---|
| QSettings | 轻量键值配置 | 简单场景替代品;Qt SQL 用于结构化数据 |
| 裸 SQLite C API(系列第 54 篇) | 直接调 sqlite3 接口 | Qt SQL 的 QSQLITE 驱动底层就是它;Qt SQL 提供面向对象封装与 Model/View 集成 |
| DuckDB(系列第 52 篇) | 嵌入式 OLAP 分析库 | 分析型场景;Qt SQL 侧重 OLTP 应用集成 |
| 手写文件(JSON/CSV) | 简单持久化 | 无查询能力;数据量增长后建议迁到 Qt SQL |
5. 可编译完整示例:SQLite 通讯录管理系统
下面给出一个可直接编译运行的完整示例:SQLite 建表 → 增删改查 → 事务 → 模型展示,覆盖本篇全部核心 API。
5.1 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(ContactManager)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Sql)
qt_standard_project_setup()
add_executable(ContactManager main.cpp)
target_link_libraries(ContactManager PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Sql)
5.2 main.cpp(完整代码)
cpp
#include <QApplication>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlTableModel>
#include <QSqlRelationalTableModel>
#include <QSqlRelationalDelegate>
#include <QTableView>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QWidget>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QDebug>
#include <QFile>
#include <QVariant>
// 1. 建表(DDL)
bool createTables(QSqlDatabase &db)
{
QSqlQuery query(db);
bool ok = query.exec(
"CREATE TABLE IF NOT EXISTS city ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL UNIQUE)"
);
if (!ok) { qCritical() << "create city failed:" << query.lastError().text(); return false; }
ok = query.exec(
"CREATE TABLE IF NOT EXISTS person ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" age INTEGER,"
" email TEXT,"
" city_id INTEGER,"
" FOREIGN KEY (city_id) REFERENCES city(id))"
);
if (!ok) { qCritical() << "create person failed:" << query.lastError().text(); return false; }
return true;
}
// 2. 初始化种子数据(带事务)
bool initData(QSqlDatabase &db)
{
if (!db.transaction()) { qWarning() << "begin tx failed:" << db.lastError().text(); return false; }
QSqlQuery query(db);
bool ok = true;
// 预处理语句:重复执行只换绑定值
ok &= query.prepare("INSERT OR IGNORE INTO city (name) VALUES (?)");
for (const QString &city : {"北京", "上海", "广州"}) {
query.addBindValue(city);
ok &= query.exec();
}
ok &= query.prepare("INSERT INTO person (name, age, email, city_id) "
"VALUES (:name, :age, :email, :city_id)");
struct Row { QString name; int age; QString email; int cityId; };
const QList<Row> rows = {
{"张三", 28, "zhangsan@example.com", 1},
{"李四", 35, "lisi@example.com", 2},
{"王五", 24, "wangwu@example.com", 3},
{"赵六", 41, "zhaoliu@example.com", 1},
};
for (const Row &r : rows) {
query.bindValue(":name", r.name);
query.bindValue(":age", r.age);
query.bindValue(":email", r.email);
query.bindValue(":city_id", r.cityId);
ok &= query.exec();
}
if (!ok) {
db.rollback();
qWarning() << "init data failed, rolled back";
return false;
}
if (!db.commit()) {
qCritical() << "commit failed:" << db.lastError().text();
db.rollback();
return false;
}
return true;
}
// 3. 用预处理语句做增删改查(控制台演示)
void demonstrateCrud(QSqlDatabase &db)
{
// 查询
QSqlQuery query(db);
query.prepare("SELECT p.name, p.age, c.name FROM person p "
"JOIN city c ON p.city_id = c.id WHERE p.age >= ? ORDER BY p.age");
query.addBindValue(25);
if (query.exec()) {
qDebug() << "== 查询年龄 >= 25 的联系人 ==";
while (query.next()) {
qDebug() << query.value(0).toString()
<< query.value(1).toInt()
<< "岁, 城市:" << query.value(2).toString();
}
} else {
qWarning() << "query failed:" << query.lastError().text();
}
// 插入 + lastInsertId
query.prepare("INSERT INTO person (name, age, email, city_id) VALUES (?, ?, ?, ?)");
query.addBindValue("新用户");
query.addBindValue(20);
query.addBindValue("new@example.com");
query.addBindValue(2);
if (query.exec()) {
QVariant newId = query.lastInsertId();
qDebug() << "== 插入成功,新记录 id =" << newId.toLongLong();
}
// 更新
query.prepare("UPDATE person SET age = :age WHERE name = :name");
query.bindValue(":age", 30);
query.bindValue(":name", "张三");
if (query.exec()) {
qDebug() << "== 更新成功,影响行数:" << query.numRowsAffected();
}
// 删除(演示:删除上一步插入的新用户)
query.prepare("DELETE FROM person WHERE name = ?");
query.addBindValue("新用户");
if (query.exec()) {
qDebug() << "== 删除成功,影响行数:" << query.numRowsAffected();
}
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// ---- 数据库准备 ----
if (!QSqlDatabase::isDriverAvailable("QSQLITE")) {
QMessageBox::critical(nullptr, "错误", "QSQLITE driver not loaded!");
return 1;
}
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("contacts.db"); // 当前目录下生成文件
if (!db.open()) {
QMessageBox::critical(nullptr, "错误", "打开数据库失败: " + db.lastError().text());
return 1;
}
createTables(db);
initData(db);
demonstrateCrud(db);
// ---- GUI 展示:QSqlRelationalTableModel 外键联动 ----
QSqlRelationalTableModel *model = new QSqlRelationalTableModel(nullptr, db);
model->setTable("person");
// person.city_id -> city.id,显示 city.name
model->setRelation(model->fieldIndex("city_id"), QSqlRelation("city", "id", "name"));
model->setHeaderData(model->fieldIndex("id"), Qt::Horizontal, "ID");
model->setHeaderData(model->fieldIndex("name"), Qt::Horizontal, "姓名");
model->setHeaderData(model->fieldIndex("age"), Qt::Horizontal, "年龄");
model->setHeaderData(model->fieldIndex("email"), Qt::Horizontal, "邮箱");
model->setHeaderData(model->fieldIndex("city_id"), Qt::Horizontal, "城市");
model->select();
QTableView *view = new QTableView;
view->setModel(model);
view->setItemDelegate(new QSqlRelationalDelegate(view)); // 外键列下拉框
view->resizeColumnsToContents();
// ---- 简单操作面板 ----
QLineEdit *nameEdit = new QLineEdit;
nameEdit->setPlaceholderText("姓名");
QLineEdit *ageEdit = new QLineEdit;
ageEdit->setPlaceholderText("年龄");
QLineEdit *emailEdit = new QLineEdit;
emailEdit->setPlaceholderText("邮箱");
QPushButton *addBtn = new QPushButton("添加");
QPushButton *delBtn = new QPushButton("删除选中");
QPushButton *commitBtn = new QPushButton("提交所有修改");
QPushButton *revertBtn = new QPushButton("撤销修改");
QObject::connect(addBtn, &QPushButton::clicked, [&]() {
int row = model->rowCount();
model->insertRow(row);
model->setData(model->index(row, model->fieldIndex("name")), nameEdit->text());
model->setData(model->index(row, model->fieldIndex("age")), ageEdit->text().toInt());
model->setData(model->index(row, model->fieldIndex("email")), emailEdit->text());
model->setData(model->index(row, model->fieldIndex("city_id")), 1); // 默认北京
nameEdit->clear(); ageEdit->clear(); emailEdit->clear();
});
QObject::connect(delBtn, &QPushButton::clicked, [&]() {
const QModelIndex cur = view->currentIndex();
if (cur.isValid())
model->removeRow(cur.row());
});
QObject::connect(commitBtn, &QPushButton::clicked, [&]() {
if (!model->submitAll())
QMessageBox::warning(nullptr, "提交失败", model->lastError().text());
});
QObject::connect(revertBtn, &QPushButton::clicked, [&]() {
model->revertAll();
});
QHBoxLayout *editLayout = new QHBoxLayout;
editLayout->addWidget(nameEdit);
editLayout->addWidget(ageEdit);
editLayout->addWidget(emailEdit);
editLayout->addWidget(addBtn);
QHBoxLayout *btnLayout = new QHBoxLayout;
btnLayout->addWidget(delBtn);
btnLayout->addWidget(commitBtn);
btnLayout->addWidget(revertBtn);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(new QLabel("联系人管理(QSqlRelationalTableModel + QTableView)"));
mainLayout->addLayout(editLayout);
mainLayout->addWidget(view);
mainLayout->addLayout(btnLayout);
QWidget window;
window.setLayout(mainLayout);
window.resize(700, 400);
window.show();
return app.exec();
}
5.3 编译运行
# 使用 CMake
cmake -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/msvc2019_64
cmake --build build --config Release
./build/ContactManager.exe # 生成 contacts.db
运行后:控制台打印增删改查日志,GUI 表格可编辑外键列(城市下拉)、添加/删除/提交/撤销。
6. 常见问题与 FAQ 速查表
| 问题 | 症状 | 原因 | 解决方案 | |
|---|---|---|---|---|
| 1 | 驱动未加载 | QSqlDatabase: QSQLITE driver not loaded | 部署时漏拷 sqldrivers 插件目录;或 Qt 构建时未编译该驱动;插件路径搜索失败 | ① qDebug() << QSqlDatabase::drivers(); 确认编译进库;② 把 plugins/sqldrivers 拷到可执行文件同级 sqldrivers/ 目录;③ Windows 部署用 windeployqt --sql;④ 静态构建需链接 Qt6::QSQLiteDriverPlugin |
| 2 | 中文乱码 | 插入/查询中文显示 ??? 或乱码 | 客户端编码与数据库编码不一致;Qt 源码文件编码问题 | ① 源码用 UTF-8,文件头加 #pragma execution_character_set("utf-8")(MSVC)或使用 QStringLiteral;② 数据库连接后执行 PRAGMA encoding='UTF-8'(SQLite)/ SET NAMES utf8mb4(MySQL);③ MySQL 用 QMYSQL 时连接选项 MYSQL_OPT_RECONNECT 外注意驱动默认按 utf8 解释,可 db.setConnectOptions("MYSQL_SET_NAMES=utf8mb4") |
| 3 | removeDatabase 警告 | QSqlDatabasePrivate::removeDatabase: connection 'qt_sql_default_connection' is still in use, all queries will cease to work | 调用 removeDatabase 时仍有 QSqlQuery / QSqlDatabase 局部句柄未析构 | ① 确保所有 QSqlQuery 在 removeDatabase 之前已析构(缩小作用域或用 {} 包裹);② 删除全局/静态的 QSqlDatabase 句柄引用;③ 惯用法:db.close(); db = QSqlDatabase(); QSqlDatabase::removeDatabase("...") |
| 4 | 线程中使用数据库连接 | 子线程访问报错、崩溃或数据错乱 | QSqlDatabase 连接不能跨线程共享:每个连接只允许在创建它的线程内使用(连接默认绑定创建线程) | ① 每个线程创建自己的连接,用不同 connectionName;② 用 QThreadPool + 每线程 addDatabase(可封装成线程局部 QSqlDatabase::database(threadName));③ 更优做法:数据库操作集中到工作对象(moveToThread)里统一执行;④ 注意 QSqlDatabase 实例本身也不可跨线程传递,只传 connectionName |
| 5 | lastInsertId | 插入后拿不到自增主键 | 驱动不支持或时机不对 | ① query.lastInsertId() 必须在同一条 QSqlQuery 的 exec 成功后立即调用;② 先用 db.driver()->hasFeature(QSqlDriver::LastInsertId) 检测;③ SQLite 返回 rowid 整数;MySQL 返回 AUTO_INCREMENT 值;④ 不支持时改用 SELECT last_insert_rowid()(SQLite)等原生语句 |
| 6 | 绑定参数与 SQL 注入 | 用户输入 ' OR '1'='1 删库跑路 | 用字符串拼接构造 SQL | ① 一律使用 prepare() + bindValue()/addBindValue();② 禁止 QString("SELECT ... WHERE name='%1'").arg(input) 形式;③ 绑定同时解决转义问题(引号/反斜杠由驱动处理) |
| 7 | QSqlTableModel 刷新 | 外部修改后表格不更新 | 模型缓存了数据,未重新 select | ① 手动刷新:model->select();;② 数据变更后自动刷新可监听数据库通知(SQLite 无内置通知,可定时 select 或自己发信号);③ 提交失败时 revertAll() 恢复 |
| 8 | QSqlTableModel 提交失败 | submitAll() 返回 false | 违反约束(NOT NULL/唯一)、外键不存在、列类型不匹配 | ① model->lastError() 查看具体原因;② 检查 insertRow 后每列是否 setData(未设置的非空列导致失败);③ 外键列必须引用存在的主键;④ setEditStrategy(OnManualSubmit) 时错误行保持未提交状态,可 revertAll() |
| 9 | 内存数据库丢失 | :memory: 数据库关闭后数据全无 | 内存数据库随连接关闭销毁 | ① 需要持久化用文件路径(contacts.db);② 多连接共享内存库需 file:memdb?mode=memory&cache=shared + QSQLITE_OPEN_URI 选项;③ 单纯测试用 :memory: 没问题 |
| 10 | 批量插入慢 | 插入 1 万条要几十秒 | SQLite 默认每条语句自动提交(写盘) | ① 包在单事务里(transaction() + 批量 exec + commit()),可提速上百倍;② 用 execBatch();③ 开启 WAL:db.exec("PRAGMA journal_mode=WAL") |
| 11 | 驱动不支持特性 | 某驱动上 execBatch()/事务/size() 失效 | 驱动能力差异 | 用 db.driver()->hasFeature(QSqlDriver::xxx) 探测后分支处理;写跨数据库代码时只依赖通用特性 |
| 12 | 表头显示字段名 | 表格表头是英文列名 | 未设置 headerData | ① model->setHeaderData(i, Qt::Horizontal, "中文");② QSqlQueryModel 需自行设置;QSqlTableModel 可直接改 |
| 13 | SQLite 并发写冲突 | database is locked | 多线程/多连接同时写 | ① 开启 WAL 模式;② 设置 QSQLITE_BUSY_TIMEOUT 连接选项(如 db.setConnectOptions("QSQLITE_BUSY_TIMEOUT=5000"));③ 写操作串行化(单写者);④ 连接复用而不是频繁开关 |
| 14 | 数值精度丢失 | 大整数/高精度小数错 | 通过 QVariant/QString 中转时类型截断 | ① 取数用 value().toLongLong()/toDouble() 而非 toInt;② 数据库字段用 INTEGER/REAL/NUMERIC 恰当类型;③ 金额用整数分存储避免浮点误差 |
| 15 | 关闭应用崩溃 | 退出时崩溃或警告 | 连接释放顺序错误 | ① 在 main 返回前关闭并移除连接;② 注意全局 QSqlDatabase 静态对象的析构顺序;③ 确保所有 model/query 先 delete |
7. 学习路径建议
- 先掌握 SQL 语言基础(CREATE/INSERT/SELECT/UPDATE/DELETE/JOIN/事务);
- 用本文第 5 章完整示例跑通 SQLite 全流程;
- 进阶:替换驱动连接 MySQL/PostgreSQL,体会"驱动隔离";
- 结合 Model/View 篇:自定义 QAbstractTableModel 包装查询结果 vs QSqlTableModel 的取舍;
- 深入:多线程连接管理(FAQ #4)、WAL 与事务性能调优、QSqlRelationalDelegate 外键编辑;
- 生产化:连接池、错误重试、数据迁移(schema versioning)、加密(SQLCipher)。