Qt 数据库编程(Qt SQL 模块)深度解析

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

绑定的关键好处

  1. 防注入:值由驱动层做类型化处理,恶意输入只会被当作"值"而非"SQL 代码";
  2. 性能:同一 SQL 只解析一次,循环中反复执行只换绑定值,避免重复解析(数据库端可复用执行计划);
  3. 类型安全: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. 学习路径建议

  1. 先掌握 SQL 语言基础(CREATE/INSERT/SELECT/UPDATE/DELETE/JOIN/事务);
  2. 用本文第 5 章完整示例跑通 SQLite 全流程;
  3. 进阶:替换驱动连接 MySQL/PostgreSQL,体会"驱动隔离";
  4. 结合 Model/View 篇:自定义 QAbstractTableModel 包装查询结果 vs QSqlTableModel 的取舍;
  5. 深入:多线程连接管理(FAQ #4)、WAL 与事务性能调优、QSqlRelationalDelegate 外键编辑;
  6. 生产化:连接池、错误重试、数据迁移(schema versioning)、加密(SQLCipher)。
相关推荐
TDengine (老段)32 分钟前
TDengine vs InfluxDB — 全方位对比
大数据·数据库·物联网·时序数据库·tdengine·涛思数据·iotdb
我的xiaodoujiao37 分钟前
Django 基础知识详细图文教程 4-Django 视图定义与使用
开发语言·数据库·后端·测试工具·django·sqlite
l1t38 分钟前
DeepSeek总结的DuckDB在 CI 中为 release 和 glibc CLI 构建启用 LTO - #24225
开发语言·数据库·ci/cd·duckdb
veminhe1 小时前
`sqlalchemy` 基本使用
数据库
2601_962074681 小时前
大数据-263 实时数仓 - Canal 工作原理 工作流程 MySQL Binglog基本介绍
大数据·数据库·mysql
2601_962181961 小时前
数据库之PostgreSQL详解
数据库·postgresql
疯狂打码的少年1 小时前
【数据库技术】SQL概述与数据定义(DDL:CREATE/DROP/ALTER)
数据库·笔记·sql·oracle
数据库小学妹1 小时前
关系型数据库内置图查询来了:SQL/PGQ标准怎么用
数据库·图数据库·关系型数据库·图查询·数据库新特性
pnoker2 小时前
IoT DC3 时序存储选型:四款数据库可插拔
数据库·物联网·postgresql·时序数据库·influxdb·tdengine·iotdb