1. 项目概述:为什么Qt + SQLite是桌面应用开发的黄金搭档
在桌面应用开发领域,尤其是使用C++和Qt框架时,数据持久化是一个绕不开的话题。你可能需要保存用户的配置、记录日志、管理一个小型的产品目录,或者缓存一些中间计算结果。这时候,引入一个完整的MySQL或PostgreSQL就显得有些“杀鸡用牛刀”了,不仅部署复杂,还会让应用变得臃肿。而Qt内置的QSqlite模块,恰恰为我们提供了一个近乎完美的轻量级解决方案。
简单来说,这个项目就是探讨如何在Qt应用程序中,高效、稳健地集成和使用SQLite数据库。SQLite本身是一个进程内的、无服务器的、零配置的、事务性的SQL数据库引擎,它的数据库就是一个普通的文件。Qt通过QSqlDatabase、QSqlQuery、QSqlTableModel等一系列类,为我们封装了所有数据库操作,让我们能用面向对象的方式,像操作本地数据结构一样操作数据库表。
这解决了什么问题呢?首先,它让应用的部署变得极其简单,用户电脑上不需要安装任何数据库服务,你的.exe或.app旁边放一个.db文件就能运行。其次,Qt的模型/视图架构能无缝对接数据库,你很容易就能用QTableView显示一个数据库表,并实现增删改查。最后,对于中小型数据量(通常指百万条记录以内)的应用,SQLite的性能完全足够,而且其ACID事务特性保证了数据的一致性。
无论你是刚接触Qt的新手,想为你的小工具添加一个配置保存功能;还是有一定经验的开发者,正在开发需要本地数据管理的客户端软件,掌握Qt的数据库操作都是提升开发效率和软件专业度的关键一步。接下来,我会从一个实战者的角度,带你从环境准备、核心操作到高级技巧,完整地走一遍这个流程。
2. 核心组件与架构设计解析
在动手写代码之前,理解Qt SQL模块的架构至关重要。它不是一个黑盒,而是一套清晰分层的设计。最底层是各种数据库驱动(Driver),如QSQLITE、QMYSQL等,它们负责与具体的数据库进行通信。对于SQLite,这个驱动直接读写磁盘上的.db文件。
向上是QSqlDatabase类,它代表了一个数据库连接。你可以把它理解为一个通往数据库文件的“门”。一个应用可以同时打开多个连接(多个门),连接到相同或不同的数据库。
再往上,是执行具体操作的QSqlQuery类。所有SQL语句的执行,无论是SELECT、INSERT还是创建表,都通过它来完成。它是我们与数据库交互最直接的工具。
最顶层,是方便与Qt GUI组件集成的模型类,主要是QSqlTableModel和QSqlQueryModel。它们继承自QAbstractTableModel,可以直接作为QTableView或QListView的数据源,实现数据的自动显示和编辑。这种设计将数据存储和界面展示优雅地解耦了。
为什么选择这样的架构?从实践角度看,它提供了极大的灵活性。对于简单的查询和操作,你可以直接用QSqlQuery,享受最大的控制权。对于需要快速绑定表格视图的场景,QSqlTableModel几乎可以零代码实现一个功能齐全的数据管理界面。这种分层也使得代码更容易维护和测试,你可以单独测试数据库逻辑,而不必启动整个GUI。
在开始编码前,我们还需要做一个关键决策:数据库文件的管理策略。是把数据库文件放在可执行文件同级目录?还是放在系统的标准数据目录(如Windows的AppData, macOS的Application Support)?我个人的经验是,对于用户数据(如文档、项目文件),放在用户文档目录下更合适;对于应用配置和缓存,放在标准数据目录更好。这涉及到使用QStandardPaths类来获取平台无关的路径。一开始就规划好,能避免后续因文件权限或路径变动带来的麻烦。
3. 环境准备与基础配置实战
首先,确保你的Qt环境包含了SQL模块。在Qt Creator中创建项目时,在.pro项目文件中,你需要添加一行:QT += sql。这是告诉构建系统,你的项目需要链接Qt的SQL库。如果忘记添加,编译时会报错提示找不到相关的头文件或类。
接下来,我们进行数据库连接的初始化和配置。这是一切操作的起点。通常,我会在一个全局的单例类或应用启动的初始化函数中完成这项工作。
#include <QSqlDatabase> #include <QSqlError> #include <QSqlQuery> #include <QDebug> #include <QStandardPaths> #include <QDir> bool initDatabase() { // 1. 确定数据库文件路径 QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(dataDir); if (!dir.exists()) { dir.mkpath(dataDir); // 递归创建目录 } QString dbPath = dir.filePath("myapp.db"); // 2. 添加一个SQLite数据库连接,并命名为一个唯一标识(例如“main_connection”) QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "main_connection"); db.setDatabaseName(dbPath); // 3. 打开数据库连接 if (!db.open()) { qCritical() << "无法打开数据库:" << db.lastError().text(); return false; } qDebug() << "数据库连接成功,文件位于:" << dbPath; // 4. (可选)启用外键约束。SQLite默认关闭,这是一个非常重要的设置! QSqlQuery pragmaQuery(db); if (!pragmaQuery.exec("PRAGMA foreign_keys = ON;")) { qWarning() << "启用外键约束失败:" << pragmaQuery.lastError().text(); } // 5. (可选)设置WAL模式,提升并发读写性能(Qt 5.12+) if (!pragmaQuery.exec("PRAGMA journal_mode = WAL;")) { qWarning() << "设置WAL模式失败:" << pragmaQuery.lastError().text(); } return true; }这段代码有几个关键点需要注意。第一,我们使用QStandardPaths来获取跨平台的应用数据目录,这比硬编码路径要专业得多。第二,addDatabase的第二个参数是连接名。如果你的应用非常简单,只有一个数据库连接,可以省略它,Qt会使用一个默认连接。但如果你未来可能操作多个数据库,或者需要多线程访问(每个线程需要独立的连接),显式命名是一个好习惯。第三,打开数据库后立即设置PRAGMA foreign_keys = ON是强烈推荐的做法。SQLite为了向后兼容,默认关闭外键约束,这意味着你即使定义了外键,数据库也不会帮你维护引用完整性,这极易导致数据混乱。第四,WAL(Write-Ahead Logging)模式是SQLite的一种高级日志模式,它能显著提升多线程读写的性能,特别是在写操作频繁时,读操作不会被阻塞。如果你的应用是Qt 5.12及以上版本,并且有并发访问需求,建议开启。
注意:数据库连接(
QSqlDatabase)对象本身是资源。虽然Qt会在应用关闭时自动清理,但在长时间运行的应用中,如果频繁创建和销毁连接,最好在不需要时手动调用QSqlDatabase::removeDatabase()来释放资源,避免内存泄漏。
4. 数据表设计与创建的最佳实践
数据库连接建立好后,第一件事就是创建表。设计良好的表结构是高效数据操作的基础。这里我们以一个简单的“任务管理”应用为例,设计两张表:projects(项目)和tasks(任务)。
在编码创建表之前,我习惯先在SQLite数据库管理工具(比如DB Browser for SQLite或DBeaver)里画一下ER图,理清关系。tasks表通过一个project_id字段外键关联到projects表。
创建表的SQL语句,我倾向于写在一个单独的.sql文件或字符串常量中,这样结构清晰,也便于版本管理。下面是创建这两张表的代码:
bool createTables() { QSqlDatabase db = QSqlDatabase::database("main_connection"); // 获取之前建立的连接 QSqlQuery query(db); // 开启事务,保证两张表要么都创建成功,要么都失败 if (!db.transaction()) { qCritical() << "开启事务失败:" << db.lastError().text(); return false; } // 创建项目表 QString createProjectsTable = R"( CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); )"; // 创建任务表,并定义外键约束 QString createTasksTable = R"( CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, title TEXT NOT NULL, status INTEGER DEFAULT 0, -- 0:待办, 1:进行中, 2:完成 priority INTEGER DEFAULT 1, -- 优先级 due_date DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE ); )"; if (!query.exec(createProjectsTable)) { qCritical() << "创建projects表失败:" << query.lastError().text(); db.rollback(); return false; } if (!query.exec(createTasksTable)) { qCritical() << "创建tasks表失败:" << query.lastError().text(); db.rollback(); return false; } // 为任务表的 project_id 和 status 创建索引,加速查询 if (!query.exec("CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);")) { qWarning() << "创建索引idx_tasks_project_id失败:" << query.lastError().text(); // 索引创建失败不视为致命错误,可以继续 } if (!query.exec("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);")) { qWarning() << "创建索引idx_tasks_status失败:" << query.lastError().text(); } if (!db.commit()) { qCritical() << "提交事务失败:" << db.lastError().text(); db.rollback(); return false; } qDebug() << "数据表创建成功。"; return true; }这里有几个非常重要的设计细节和实操心得:
- 使用
IF NOT EXISTS:这保证了你的初始化代码可以安全地多次执行,不会因为表已存在而报错。这在开发调试阶段非常有用。 - 使用
AUTOINCREMENT:对于主键id,INTEGER PRIMARY KEY AUTOINCREMENT是标准做法。但要注意,SQLite的AUTOINCREMENT和MySQL的AUTO_INCREMENT行为略有不同,它保证了主键严格递增且永不复用,但会带来微小的性能开销。对于大多数应用,直接使用INTEGER PRIMARY KEY(没有AUTOINCREMENT)就足够了,它也会自动递增,只是可能会复用已删除行的ID。 - 外键与级联删除:注意
tasks表中FOREIGN KEY子句末尾的ON DELETE CASCADE。这意味着当projects表中的某个项目被删除时,所有属于这个项目的tasks记录会被自动删除。这极大地简化了业务逻辑,保证了数据一致性。但前提是,你必须像之前那样,已经执行了PRAGMA foreign_keys = ON;,否则这个约束是无效的。 - 时间戳字段:
created_at和updated_at是审计追踪的黄金字段。我设置DEFAULT CURRENT_TIMESTAMP让SQLite自动插入当前时间。对于updated_at,你需要在每次更新记录时手动用触发器或代码更新它。这里为了简单,创建时也用了默认值。 - 使用事务:将创建多个表的操作包裹在一个事务中。这确保了原子性:要么所有表都创建成功,要么全部回滚,不会出现创建了一半的中间状态。
- 创建索引:对于经常用于
WHERE条件(如project_id,status)或JOIN的字段,创建索引能极大提升查询速度。但索引不是免费的,它会增加插入、更新和删除操作的开销,并占用额外空间。所以需要权衡。
5. 使用QSqlQuery进行增删改查(CRUD)
QSqlQuery是执行SQL语句的瑞士军刀。它的使用看似简单,但有很多细节关乎代码的健壮性和安全性。
5.1 插入数据(Create)
插入数据时,绝对不要使用字符串拼接来构造SQL语句,这会导致SQL注入漏洞。必须使用预处理语句(Prepared Statement)。
bool addProject(const QString &name, const QString &description) { QSqlDatabase db = QSqlDatabase::database("main_connection"); QSqlQuery query(db); query.prepare("INSERT INTO projects (name, description) VALUES (:name, :description)"); query.bindValue(":name", name); query.bindValue(":description", description); if (!query.exec()) { qCritical() << "插入项目失败:" << query.lastError().text() << ",SQL:" << query.lastQuery(); return false; } qDebug() << "新项目ID:" << query.lastInsertId().toInt(); return true; }使用命名占位符(如:name)比使用?这样的位置占位符可读性更好,尤其是在语句中有很多参数时。bindValue函数会自动处理字符串转义和类型转换,安全地将值填入SQL语句。
5.2 查询数据(Read)
查询操作通常分为两种:获取单条记录和获取多条记录。
// 查询单个项目的信息 QSqlRecord getProjectById(int projectId) { QSqlDatabase db = QSqlDatabase::database("main_connection"); QSqlQuery query(db); query.prepare("SELECT * FROM projects WHERE id = :id"); query.bindValue(":id", projectId); if (!query.exec()) { qCritical() << "查询项目失败:" << query.lastError().text(); return QSqlRecord(); // 返回空记录 } if (query.next()) { return query.record(); // 返回第一条(也是唯一一条)记录 } else { qDebug() << "未找到ID为" << projectId << "的项目"; return QSqlRecord(); } } // 查询某个项目下的所有任务 QVector<QHash<QString, QVariant>> getTasksByProject(int projectId) { QSqlDatabase db = QSqlDatabase::database("main_connection"); QSqlQuery query(db); query.prepare("SELECT id, title, status, priority, due_date FROM tasks WHERE project_id = :pid ORDER BY priority DESC, created_at"); query.bindValue(":pid", projectId); QVector<QHash<QString, QVariant>> taskList; if (!query.exec()) { qCritical() << "查询任务失败:" << query.lastError().text(); return taskList; } while (query.next()) { QHash<QString, QVariant> task; QSqlRecord rec = query.record(); for (int i = 0; i < rec.count(); ++i) { task[rec.fieldName(i)] = rec.value(i); } taskList.append(task); } return taskList; }这里的关键是query.next(),它用于遍历结果集。每次调用,游标就移动到下一行。首次调用前,游标位于第一行之前。query.record()可以获取当前行的所有字段信息。我通常将多条记录的结果组织成QVector<QHash<QString, QVariant>>,这样在业务逻辑层使用起来非常灵活。当然,你也可以定义专门的结构体或类来承载数据。
5.3 更新与删除数据(Update & Delete)
更新和删除同样要使用预处理语句,并且务必带上WHERE条件,否则会操作整张表!
bool updateTaskStatus(int taskId, int newStatus) { QSqlDatabase db = QSqlDatabase::database("main_connection"); QSqlQuery query(db); query.prepare("UPDATE tasks SET status = :status, updated_at = datetime('now') WHERE id = :id"); query.bindValue(":status", newStatus); query.bindValue(":id", taskId); if (!query.exec()) { qCritical() << "更新任务状态失败:" << query.lastError().text(); return false; } // 检查是否真的有行被更新 if (query.numRowsAffected() <= 0) { qWarning() << "更新任务状态未影响任何行,可能ID不存在:" << taskId; } return true; } bool deleteProject(int projectId) { QSqlDatabase db = QSqlDatabase::database("main_connection"); // 由于设置了 ON DELETE CASCADE,删除项目会自动删除其下所有任务 QSqlQuery query(db); query.prepare("DELETE FROM projects WHERE id = :id"); query.bindValue(":id", projectId); if (!query.exec()) { qCritical() << "删除项目失败:" << query.lastError().text(); return false; } qDebug() << "删除项目,影响行数:" << query.numRowsAffected(); return true; }QSqlQuery::numRowsAffected()函数返回受最后一次UPDATE、DELETE或INSERT操作影响的行数。这是一个非常有用的调试和校验工具,可以确认你的操作是否按预期执行了。
6. 高级应用:QSqlTableModel与视图的绑定
对于需要快速构建一个可编辑的数据表格界面的场景,QSqlTableModel是神器。它能将一个数据库表映射为一个Qt的模型,直接供QTableView使用。
// 在某个窗口类(如MainWindow)的初始化函数中 void MainWindow::setupProjectTableView() { // 1. 创建模型并设置表 QSqlTableModel *model = new QSqlTableModel(this, QSqlDatabase::database("main_connection")); model->setTable("projects"); // 2. 设置编辑策略。OnFieldChange是每次字段修改后立即提交,OnRowChange是行焦点离开后提交,ManualSubmit是手动提交。 model->setEditStrategy(QSqlTableModel::OnRowChange); // 3. 设置排序(可选) model->setSort(model->fieldIndex("created_at"), Qt::DescendingOrder); // 4. 选择数据(相当于执行 SELECT * FROM projects ORDER BY created_at DESC) if (!model->select()) { qCritical() << "模型选择数据失败:" << model->lastError().text(); return; } // 5. 设置表头显示名称(可选) model->setHeaderData(model->fieldIndex("name"), Qt::Horizontal, tr("项目名称")); model->setHeaderData(model->fieldIndex("description"), Qt::Horizontal, tr("描述")); // 6. 将模型设置给视图 ui->tableView->setModel(model); // 7. 隐藏不需要的列,如id ui->tableView->hideColumn(model->fieldIndex("id")); // 8. 允许用户排序 ui->tableView->setSortingEnabled(true); // 9. 调整列宽适应内容 ui->tableView->resizeColumnsToContents(); }就这么几行代码,一个功能完整的、支持排序、编辑、删除(通过选中行按Delete键)的项目列表界面就完成了。QSqlTableModel的setEditStrategy非常重要:
OnFieldChange:实时提交,任何单元格编辑完成(焦点离开)立即写入数据库。响应快,但可能产生大量小事务。OnRowChange:行焦点离开后提交整行修改。比较平衡的策略。ManualSubmit:所有修改都缓存在内存中,直到你调用model->submitAll()或model->revertAll()。这给了你最大的控制权,适合需要“确认/取消”功能的场景。
实操心得:
QSqlTableModel虽然方便,但它生成的SQL语句可能不是最优的(例如,它可能会SELECT *)。对于数据量很大的表,或者需要复杂JOIN查询的场景,性能可能成为瓶颈。此时,可以继承QSqlQueryModel,并重写其data()和setData()方法来实现自定义的只读或可编辑模型,这样你可以完全控制执行的SQL。
7. 事务处理与并发控制
事务是保证数据库操作原子性、一致性、隔离性和持久性(ACID)的关键。在Qt中,你可以显式地控制事务。
bool transferTasks(int fromProjectId, int toProjectId, const QVector<int>& taskIds) { QSqlDatabase db = QSqlDatabase::database("main_connection"); if (!db.transaction()) { qCritical() << "开启事务失败"; return false; } QSqlQuery query(db); query.prepare("UPDATE tasks SET project_id = :toPid WHERE id = :taskId AND project_id = :fromPid"); bool allSuccess = true; for (int taskId : taskIds) { query.bindValue(":toPid", toProjectId); query.bindValue(":taskId", taskId); query.bindValue(":fromPid", fromProjectId); if (!query.exec()) { qCritical() << "转移任务失败,任务ID:" << taskId << "错误:" << query.lastError().text(); allSuccess = false; break; } if (query.numRowsAffected() != 1) { qWarning() << "转移任务未成功,可能任务不存在或不属于源项目,任务ID:" << taskId; // 这里可以根据业务逻辑决定是回滚还是继续 } } if (allSuccess) { if (!db.commit()) { qCritical() << "提交事务失败"; db.rollback(); return false; } qDebug() << "任务转移成功"; return true; } else { qCritical() << "转移过程中发生错误,回滚事务"; db.rollback(); return false; } }这个例子模拟了一个“批量转移任务”的业务。所有更新操作都在一个事务中,要么全部成功,要么全部失败回滚,避免了部分转移成功导致的脏数据。
关于并发,SQLite默认支持多线程读,但写操作是串行的。在WAL模式下,读和写可以同时进行,性能更好。但需要注意,QSqlDatabase和QSqlQuery对象本身不是线程安全的。最佳实践是:每个需要使用数据库的线程,都创建自己独立的数据库连接(使用不同的连接名)。可以在应用启动时为主线程创建连接,然后在其他线程中通过QSqlDatabase::addDatabase添加新的连接(使用相同的数据库文件路径)。切记不要跨线程共享同一个QSqlDatabase或QSqlQuery对象。
8. 常见问题排查与性能优化技巧
在实际开发中,你肯定会遇到各种问题。下面是我踩过的一些坑和总结的技巧。
8.1 连接与驱动问题
错误:
QSqlDatabase: QSQLITE driver not loaded- 原因:Qt的SQLite插件没有正确加载。在Windows上,确保
qsqlite.dll(或其它平台的等效文件)在可执行文件的plugins/sqldrivers目录下,或者位于Qt的插件路径中。 - 解决:在
main函数最开始,添加QCoreApplication::addLibraryPath(“./plugins”);,或者将插件目录放到正确位置。使用Qt Installer Framework打包时,要记得包含SQL驱动插件。
- 原因:Qt的SQLite插件没有正确加载。在Windows上,确保
错误:
database is locked- 原因:多个连接或进程同时尝试写数据库。SQLite一次只允许一个写操作。
- 解决:
- 确保写操作被正确的事务包裹,并且尽快提交或回滚。
- 启用WAL模式(
PRAGMA journal_mode = WAL;),它能极大改善并发读写性能。 - 在代码中实现重试机制。当捕获到这个错误时,等待一小段随机时间后重试操作。
8.2 查询错误与数据异常
错误:
near \"?\": syntax error或参数绑定失败- 原因:SQL语句语法错误,或者占位符使用不当。例如,表名或列名不能使用占位符。
- 解决:仔细检查SQL语句。对于表名、列名等标识符,如果需要动态生成,必须在构造SQL字符串时拼接,并极其小心地防范SQL注入(通常需要白名单校验)。对于数据值,必须使用
bindValue。
问题:中文字符存储后显示乱码
- 原因:早期版本的SQLite或Qt驱动对UTF-8路径支持问题,现在已很少见。
- 解决:确保数据库连接字符串(文件路径)和应用程序代码都使用UTF-8编码。在Qt中,
QString内部是UTF-16,与SQLite交互时驱动会处理转换,一般无需额外操作。
8.3 性能优化技巧
批量操作使用事务:这是最重要的性能优化手段。如果你需要插入1000条记录,不要执行1000次独立的
INSERT,而是将它们放在一个事务中。速度差异可能是几个数量级。db.transaction(); for (const auto &item : hugeList) { query.prepare(“INSERT ...“); // ... bind values query.exec(); } db.commit();明智地使用索引:在
WHERE、JOIN、ORDER BY子句中频繁使用的列上创建索引。但不要过度索引,尤其是对频繁更新的表。只查询需要的列:避免使用
SELECT *,特别是当表中有TEXT或BLOB大字段时。明确列出需要的字段名。使用
QSqlQueryModel替代QSqlTableModel进行复杂查询:QSqlTableModel为了支持编辑,会额外查询元数据并可能生成非最优SQL。对于复杂的、只读的报表类视图,自己用QSqlQueryModel执行优化过的SQL语句,性能会好很多。定期执行
VACUUM命令:删除或更新大量数据后,数据库文件内部会产生“碎片”。VACUUM命令可以重建数据库文件,回收空间并可能提升性能。但这是一个重量级操作,会占用大量I/O并锁定数据库,应在应用空闲时(如启动时)进行。QSqlQuery query(db); query.exec(“VACUUM;“);调整PRAGMA设置:
PRAGMA synchronous = NORMAL;或PRAGMA synchronous = OFF;:NORMAL是安全和性能的平衡(推荐),OFF最快但断电可能损坏数据库(仅用于临时数据库)。PRAGMA cache_size = -2000;:设置SQLite使用的内存缓存大小(单位为页,负值表示绝对值KB)。增加缓存可以减少磁盘I/O。
8.4 数据库维护与升级
随着应用迭代,你的数据库表结构可能需要改变。你不能简单地删除旧表,因为那样会丢失所有用户数据。你需要实现一个数据库版本迁移机制。
一个简单的方法是,在数据库中维护一个version表(或叫schema_info),记录当前数据库的版本号。应用启动时,检查当前数据库文件的版本,与代码中期望的版本对比,然后按顺序执行一系列“升级脚本”。
int getCurrentDbVersion() { QSqlQuery query(“PRAGMA user_version;“); // SQLite有一个内置的user_version,可以直接用 if (query.next()) { return query.value(0).toInt(); } return 0; } void migrateDatabase(int fromVersion, int toVersion) { QSqlDatabase db = QSqlDatabase::database(“main_connection“); db.transaction(); // 根据版本号,执行不同的SQL升级脚本 if (fromVersion < 1) { // 从版本0升级到1:创建初始表 QSqlQuery q1(db); q1.exec(“CREATE TABLE ... (v1 schema)“); } if (fromVersion < 2) { // 从版本1升级到2:添加新列 QSqlQuery q2(db); q2.exec(“ALTER TABLE tasks ADD COLUMN tags TEXT;“); } // ... 更多升级步骤 // 更新版本号 QSqlQuery qv(db); qv.exec(QString(“PRAGMA user_version = %1;“).arg(toVersion)); db.commit(); }在应用初始化时调用:int curVer = getCurrentDbVersion(); if (curVer < TARGET_VERSION) { migrateDatabase(curVer, TARGET_VERSION); }。这样就能安全、平滑地升级用户本地的数据库了。