SQLite是一款轻量级的嵌入式关系型数据库,整个数据库就是一个独立文件,不需要单独安装数据库服务进程。Qt内置了对SQLite的驱动支持(QSQLITE),从Qt 5到Qt 6都开箱即用,这使得它成为桌面程序、工控上位机、嵌入式设备做本地数据存储时的主流选择。本文将从连接建立、数据操作、性能优化到模型视图绑定,系统地梳理Qt中操作SQLite的完整方法。

一、建立数据库连接与初始化表结构
Qt中所有数据库操作的第一步都是获取一个QSqlDatabase对象。Qt采用了默认连接的机制,通过QSqlDatabase::addDatabase("QSQLITE")注册驱动,随后用setDatabaseName()指定数据库文件路径。如果文件不存在,SQLite会自动创建一个新的数据库文件,这一点非常省心。
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("appdata.db");
if (!db.open()) {
qDebug() << "打开数据库失败:" << db.lastError().text();
return;
}
// 创建表,IF NOT EXISTS 避免重复建表报错
QSqlQuery query;
query.exec("CREATE TABLE IF NOT EXISTS users ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"name TEXT NOT NULL, "
"age INTEGER, "
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP)");需要注意几个细节。第一,addDatabase如果多次调用同名连接会报警告,建议在程序入口处只建立一次连接,或者用addDatabase("QSQLITE", "连接名")指定命名连接。第二,数据库文件路径建议放在可写目录(如用户目录或
二、使用QSqlQuery完成增删改查
QSqlQuery是执行SQL语句的核心类,支持两种方式:直接执行完整SQL字符串,或者使用预处理语句绑定参数。对于带变量的查询,强烈建议使用prepare()加bindValue()的方式,既能防止SQL注入,又能避免手动拼接字符串时引号转义的麻烦。
QSqlQuery query;
// 插入数据(预处理方式)
query.prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
query.bindValue(":name", "张三");
query.bindValue(":age", 25);
if (!query.exec()) {
qDebug() << "插入失败:" << query.lastError().text();
}
// 查询数据
query.prepare("SELECT id, name, age FROM users WHERE age > :minAge");
query.bindValue(":minAge", 18);
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;
}
// 更新与删除
query.prepare("UPDATE users SET age = :age WHERE id = :id");
query.bindValue(":age", 26);
query.bindValue(":id", 1);
query.exec();遍历结果集时,next()每次前进一行,value()既可以用列索引也可以用列名取值。如果结果可能为空,可以先调用query.size(),但要留意SQLite驱动不支持size(),返回值是-1,正确做法是用query.next()是否为真来判断,或者先执行一次SELECT COUNT(*)。另外,空值判断要用value().isNull(),不要直接和空字符串比较,两者在业务含义上并不等价。
三、事务与批量操作的性能优化
很多人反映SQLite写入速度慢,十有八九是掉进了隐式事务的坑。SQLite默认每执行一条INSERT就自动提交一次事务,每次提交都会触发磁盘同步操作,如果循环插入一万条数据,相当于做了一万次磁盘刷写,速度会慢到无法忍受。解决办法是手动开启事务,全部插入完成后再统一提交。
db.transaction(); // 开启事务
QSqlQuery query;
query.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
QVariantList names, ages;
for (int i = 0; i < 10000; ++i) {
names << QString("用户%1").arg(i);
ages << 20 + (i % 40);
}
query.addBindValue(names);
query.addBindValue(ages);
query.execBatch(); // 批量执行
db.commit(); // 提交事务实测下来,一万条数据逐条提交可能需要十几秒,而包在事务里批量执行通常只需几百毫秒,差距非常明显。execBatch()配合addBindValue()传列表的方式是Qt提供的批量绑定接口,效率比循环调用exec更高。除了事务之外,还可以在打开数据库后执行PRAGMA synchronous = NORMAL调整同步策略,在数据安全和写入速度之间取得平衡;对读多写少的场景设置PRAGMA journal_mode = WAL,能允许读写并发,减少database is locked错误的出现。
四、QSqlTableModel实现数据绑定与界面展示
如果要在界面上展示和编辑表格数据,Qt提供了更省事的方案:QSqlTableModel配合QTableView,无需手写SQL就能实现增删改查的可视化操作。模型会自动生成SQL语句,界面上修改单元格即同步写回数据库。
QSqlTableModel *model = new QSqlTableModel(this);
model->setTable("users");
model->setEditStrategy(QSqlTableModel::OnManualSubmit);
model->setSort(3, Qt::DescendingOrder); // 按创建时间倒序
model->setFilter("age >= 18"); // 相当于 WHERE 条件
model->select(); // 加载数据
ui->tableView->setModel(model);
ui->tableView->hideColumn(0); // 隐藏主键列
// 提交修改:OnManualSubmit 策略下需手动调用
// model->submitAll();
// 回滚未提交的修改
// model->revertAll();setEditStrategy()决定了修改何时写入数据库:OnRowChange在切换行时提交,OnFieldChange在字段变化时立即提交,OnManualSubmit则完全手动控制。建议默认用OnManualSubmit,在用户点击保存按钮时统一调用submitAll(),这样既能做取消操作,也避免了编辑过程中的频繁写库。新增行用insertRow()加setData(),删除行用removeRow(),最后记得提交才会真正生效。
五、常见问题排查
实际开发中会碰到几类高频问题。一是报Driver not loaded,说明Qt运行环境中缺少SQLite插件,需要检查sqldrivers目录下有没有qsqlite.dll或libqsqlite.so,发布程序时要连同plugins一起打包。二是中文乱码,SQLite本身使用UTF-8编码,只要QSqlQuery绑定的都是QString就天然没问题,乱码多半出在用exec(QString::fromLocal8Bit(sql))之类的转换上,统一使用QString字面量即可避免。三是database is locked,通常是多个连接同时写或者有未释放的QSqlQuery还在持有结果集,确保查询对象出了作用域被析构,并考虑启用WAL模式。四是数据库文件位置问题,用相对路径时数据库文件会落在工作目录下,调试和正式运行的路径可能不同,建议始终使用绝对路径。
掌握以上内容,基本可以覆盖Qt桌面应用中本地数据存储的绝大多数场景。对于数据量更大、并发要求更高的需求,再考虑迁移到MySQL或PostgreSQL,届时只需更换驱动名称,上层QSqlQuery代码几乎不用改动,这也正是Qt数据库抽象层的价值所在。