news 2026/10/20 8:51:01

Qt与SQLite实战:连接管理、事务优化与锁问题排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt与SQLite实战:连接管理、事务优化与锁问题排查

1. Qt与SQLite的天然结合

Qt开发者几乎没有不认识数据库的,但在桌面应用里选型,SQLite往往是绕不开的那个选项。它是嵌入式数据库,整个库就是一个单独的库文件,现在Qt本身就内置了SQLite驱动,不需要额外安装任何插件或第三方依赖,只需要在项目文件里加一句QT += sql,然后include几个头文件,就能完成连接建表查询整套操作。

有人会问,既然Qt封装了QSqlDatabase、QSqlQuery这些类,为什么还要单独写一篇关于SQLite的文章?因为QSqlDatabase这套接口是通用的,同时支持MySQL、PostgreSQL、ODBC这些数据库,但SQLite和它们不一样——它是进程内数据库,没有独立的服务器进程,没有网络端口,连驱动的加载方式都和其他数据库有差别。如果不了解SQLite的存储模型、锁机制、事务特性,很有可能在Qt里写出了“能跑但性能很差”或者“莫名其妙锁库”的代码。

这篇内容就是围绕“在Qt项目里用SQLite”这一个主题展开,适合刚接触Qt数据库开发的同学跟着做一遍,也适合写过几轮数据库代码但总觉得哪里不对劲的开发者回来对一下自己的写法。从连接管理、CURD操作、事务批量写入,到模型视图绑定、常见锁库异常排查,每一段都是我实际项目里踩过坑之后总结出来的东西。

2. QSqlDatabase连接管理的正确打开方式

2.1 连接参数不是越多越好

Qt里创建一个SQLite数据库连接,常规写法是这样的:

QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName("E:/work/data/app.db"); if (!db.open()) { qDebug() << "打开数据库失败:" << db.lastError().text(); return false; }

驱动名称用的是QSQLITE,这个是Qt内置驱动的固定写法。数据库名不是IP地址和端口,而是指向一个本地文件路径。这里有一个隐藏的细节:如果路径指向一个不存在的文件,SQLite会自动创建这个空文件,正因为这个特性,很多人第一次写的时候就漏了检查路径权限。如果程序运行目录没有写入权限,open()会返回false,而且错误信息里给出的提示有时候并不直观,看到类似“unable to open database file”的时候,先检查路径权限,不要急着检查代码逻辑。

关于连接还有一个容易忽略的概念——连接名。addDatabase()如果不传第二个参数,默认连接名是qt_sql_default_connection。同一个进程里,默认连接是唯一的,如果你在第一处代码里用默认连接创建了QSqlDatabase实例,第二处代码又想创建另一个SQLite库的连接,仍然调用不带连接名的addDatabase(),Qt会直接抛出一个QSqlDatabasePrivate::addDatabase: duplicate connection name的警告,然后返回空对象。

所以多数据库并存的项目里,务必养成给连接命名的好习惯:

QSqlDatabase configDb = QSqlDatabase::addDatabase("QSQLITE", "configDb"); configDb.setDatabaseName("config.ini.db"); QSqlDatabase cacheDb = QSqlDatabase::addDatabase("QSQLITE", "cacheDb"); cacheDb.setDatabaseName("cache/data.db");

2.2 连接生命周期与多线程注意事项

SQLite的连接是轻量的,每连接一个文件句柄,但如果你在Qt里频繁地open()和close()同一个数据库文件,性能损耗会比想象中更大。我的习惯是在程序启动阶段初始化数据库,把QSqlDatabase对象保存在一个全局的访问点或者某个数据库管理类中,整个生命周期内复用这一个连接。

涉及多线程时需要特别注意:Qt的QSqlDatabase文档中明确写了“QSqlDatabase对象不能在持有它的线程之外使用”,但更安全的做法是每个线程创建自己的连接(连接名不同),或者每次使用前临时addDatabase()再removeDatabase()。SQLite本身支持多线程读写,但Qt的连接对象与线程绑定,底层是依靠线程局部存储来管理连接句柄的,跨线程传递同一个QSqlDatabase对象,轻则警告,重则崩溃。

提示:不要试图在子线程中用主线程创建的连接。如果需要并发访问数据库,用连接名区分各自线程的连接,例如子线程里用QSqlDatabase::addDatabase("QSQLITE", "thread1")。用完记得移除连接,否则下次同名添加时会报重复。

再补一个容易踩的坑——QSqlDatabase::removeDatabase()必须在所有使用该连接的QSqlQuery对象销毁之后再调用,否则Qt会给出警告“connection still in use”。实际项目里最常见的触发场景是:局部变量QSqlQuery还没析构,就调用removeDatabase()了,排查这种问题最省事的方式就是把数据库关闭和移除的代码放在一个专门的清理函数里,并且先用作用域确保所有查询对象提前释放。

3. 增删改查实操:QSqlQuery的细节与坑

3.1 exec之后一定要检查结果

先说最容易犯的错误——只调用exec()不看返回值。下面这种写法在我见过的项目代码里出现频率非常高:

QSqlQuery query(db); query.exec("INSERT INTO user(name, age) VALUES('张三', 25)");

如果这条插入语句因为某种原因失败了(字段名写错、表不存在、磁盘满了),上面这行代码不会有任何提示,数据丢失得无声无息。正确姿势是:

QSqlQuery query(db); if (!query.exec("INSERT INTO user(name, age) VALUES('张三', 25)")) { qWarning() << "插入失败:" << query.lastError().text(); }

lastError().text()返回的是一个完整字符串,里面包含SQLite底层错误码和错误描述,排查问题时这些信息比你自己猜要靠谱得多。

3.2 prepare与bindValue是必须养成的习惯

字符串拼接SQL语句的写法很直觉,但在Qt里我强烈建议使用prepare()加bindValue()方式:

QSqlQuery query(db); query.prepare("INSERT INTO user(name, age, score) VALUES(?, ?, ?)"); query.addBindValue("李四"); query.addBindValue(30); query.addBindValue(95.5); if (!query.exec()) { qWarning() << "插入失败:" << query.lastError().text(); }

为什么不用拼接?三个原因。

第一个是安全。用户的输入如果直接拼进SQL字符串里,等于把SQL注入的大门敞开。第二个是性能。批量插入大量数据时,预处理语句只需要编译一次SQL语句,后续只需要绑定不同的数据,效率比逐条拼接字符串再重新编译高很多。第三个是类型安全。数字类型、字符串类型、空值通过bindValue绑定的时候各自处理,不需要自己在字符串里加引号或者转义。

占位符风格有两种:一种是?按位置绑定,另一种是具名的形如:name绑定。具名方式更可读,代码维护时更具优势:

query.prepare("INSERT INTO user(name, age) VALUES(:name, :age)"); query.bindValue(":name", "王五"); query.bindValue(":age", 28);

3.3 查询结果遍历与字段读取

执行查询后,用next()逐行遍历结果集,读取字段用value(index),参数是列的序号(从0开始)。字段多的时候硬编码下标容易错位,我一般配合record()获取字段名的索引:

QSqlQuery query(db); if (!query.exec("SELECT id, name, age FROM user WHERE age > ?")) { qWarning() << "查询失败:" << query.lastError().text(); return; } QSqlRecord rec = query.record(); int idCol = rec.indexOf("id"); int nameCol = rec.indexOf("name"); int ageCol = rec.indexOf("age"); while (query.next()) { int id = query.value(idCol).toInt(); QString name = query.value(nameCol).toString(); int age = query.value(ageCol).toInt(); qDebug() << id << name << age; }

value()返回的是QVariant,所以读取的时候一定要显式调用toInt()、toString()这些转换函数,SQLite没有强类型约束,存进去的类型和读出来再转,容易在数字和字符串之间出现隐式转换不符合预期的情况。

3.4 自增主键的获取

SQLite支持AUTOINCREMENT,但更常见的用法是INTEGER PRIMARY KEY,这个声明下如果不显式插入id,SQLite会自动生成一个单调递增的rowid。要在插入之后立刻拿到这个id,可以用lastInsertId():

query.prepare("INSERT INTO user(name) VALUES(?)"); query.addBindValue("测试"); if (query.exec()) { QVariant newId = query.lastInsertId(); qDebug() << "新用户id:" << newId.toLongLong(); }

注意lastInsertId()返回的QVariant在SQLite下是qint64类型,转换成整型时要小心溢出,特别是当数据量很大的时候,int可能放不下。

3.5 批量删除与更新

删除和更新的写法和插入类似,核心是带WHERE条件,而且一定要先写WHERE再写条件值:

query.prepare("DELETE FROM user WHERE id = ?"); query.addBindValue(100); query.exec();

批量删除多个id时,SQLite对IN子句的支持也ok:

query.prepare("DELETE FROM user WHERE id IN (?, ?, ?)"); query.addBindValue(1); query.addBindValue(2); query.addBindValue(3); query.exec();

注意IN子句的占位符数量是固定的,如果id列表长度动态变化,最简单的方式是用循环拼接占位符字符串。还有一点,执行UPDATE/DELETE后可以通过query.numRowsAffected()获取受影响的行数,用来判断删除条件是否匹配到了预期的记录。

4. 事务处理:让批量写入性能起飞

4.1 SQLite的每次写入其实很“重”

SQLite数据写入涉及磁盘同步,默认情况下每一条INSERT/UPDATE语句都会触发一次完整的磁盘写入事务,这种模式在批量插入几千条甚至几万条数据时,速度会慢到让人怀疑人生。我做过一个批量导入测试,逐条INSERT插入1万条数据大概需要几十秒甚至更久,而放进一个事务里批量提交,时间是毫秒级——两者差了不是一个量级。

这两个数据的对比背后是SQLite的日志机制。每一条独立的INSERT都会产生日志文件、写日志、同步磁盘、清理日志,这一整套流程是为保证数据完整性而设计的,却也是性能杀手。事务的作用是把这一整套流程合并成一个批次,写日志和同步磁盘只发生一次。

4.2 Qt事务三兄弟:beginTransaction / commit / rollback

Qt里用QSqlDatabase直接可以操作事务:

if (!db.transaction()) { qWarning() << "开启事务失败:" << db.lastError().text(); return; } QSqlQuery query(db); query.prepare("INSERT INTO user(name, age) VALUES(?, ?)"); for (int i = 0; i < 10000; ++i) { query.addBindValue(QString("user_%1").arg(i)); query.addBindValue(i % 80); if (!query.exec()) { db.rollback(); qWarning() << "批量插入失败,已回滚:" << query.lastError().text(); return; } } if (!db.commit()) { qWarning() << "提交事务失败:" << db.lastError().text(); }

三个方法名对应了SQL标准事务的三段流程。transaction()开启事务块,commit()确认所有写入生效,rollback()回滚所有未提交的修改。中间任何一条SQL出错,都应该立即回滚,否则事务一直挂着会持有数据库锁,导致其他连接无法写入。

事务块的粒度要控制好。我见过把整个程序的启动初始化数据全放在一个大事务里的写法,数据量大的时候锁时间太长,程序会卡住甚至表现为“未响应”。事务块的合理粒度是:一个业务任务即开即闭,不要跨多个业务模块。

4.3 写事务的时候读数据要注意锁

SQLite默认的提交模式在写事务期间会对数据库加排他锁。在同进程不同连接中,一个连接持有写事务,另一个连接去读数据可能会被阻塞或者立即返回database is locked错误。Qt中这种场景的典型表现是:主线程UI操作正在写数据,后台工作线程尝试读数据库,后台线程直接报错。这个问题放到后面的常见问题部分继续聊。

5. 查询模型绑定:从SQL语句到界面显示一步到位

5.1 QSqlQueryModel只读模型

Qt的model/view框架对数据库开发非常友好。QSqlQueryModel可以把一条SELECT的结果直接变成表格数据,绑定到QTableView上:

QSqlQueryModel *model = new QSqlQueryModel(this); model->setQuery("SELECT id, name, age FROM user", db); model->setHeaderData(0, Qt::Horizontal, "ID"); model->setHeaderData(1, Qt::Horizontal, "姓名"); model->setHeaderData(2, Qt::Horizontal, "年龄"); ui->tableView->setModel(model); ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

就这么几行,一个带数据的表格就出现在界面上了。QSqlQueryModel默认是只读的,适合展示查询结果和报表类的场景。注意setQuery()的第二个参数传db,指定了数据库连接,如果没传或者传错连接,它会使用默认连接。

5.2 QSqlTableModel可编辑模型

如果需要对表做增删改并在界面上直接操作,用QSqlTableModel:

QSqlTableModel *tableModel = new QSqlTableModel(this, db); tableModel->setTable("user"); tableModel->setEditStrategy(QSqlTableModel::OnFieldChange); tableModel->select(); tableModel->setHeaderData(0, Qt::Horizontal, "ID"); tableModel->setHeaderData(1, Qt::Horizontal, "姓名"); tableModel->setHeaderData(2, Qt::Horizontal, "年龄"); ui->tableView->setModel(tableModel);

setEditStrategy()的可选值有OnFieldChange(字段一改立刻提交数据库)、OnRowChange(切换行时提交)和OnManualSubmit(手动调用submitAll()提交)。日常开发中OnManualSubmit模式最常见,配合界面的“保存”按钮统一提交,可以批量修改多行后再提交,避免每敲一个字就触发一次数据库IO:

if (tableModel->submitAll()) { qDebug() << "保存成功"; } else { qDebug() << "保存失败:" << tableModel->lastError().text(); tableModel->revertAll(); }

界面上添加一行、删除一行的操作在model层面对应:

tableModel->insertRow(tableModel->rowCount()); tableModel->removeRow(rowIndex);

5.3 模型绑定背后的SQL细节

QSqlTableModel的select()实际上生成了一条SELECT * FROM user,并且默认不带WHERE子句。数据量很大的表,直接select()会一次性把所有行加载进内存,界面会卡。我处理过一张二十万行的表用QSqlTableModel直接展示,结果程序启动加载花了将近十秒钟。

解决办法是给模型加过滤条件:

tableModel->setFilter("age > 20"); tableModel->select();

或者在SQL层面就按需加载,不让它拉全表。实际项目里我一般会配合分页或者懒加载思路:先只加载前两百行,用户滚动到底部时再继续加载后面的数据。这个场景QSqlTableModel用起来要小心它的缓存机制,比不上彻底手动用QSqlQuery翻页来得可控。

6. SQLite独有特性:PRAGMA与WAL模式

6.1 PRAGMA语句怎么用

SQLite有一类特殊的命令叫PRAGMA,用来查询和设置数据库的内部参数。在Qt里执行PRAGMA和执行普通SQL没有区别:

QSqlQuery query(db); query.exec("PRAGMA journal_mode=WAL");

常用的PRAGMA有几个:

  • PRAGMA journal_mode:查询或者设置日志模式,常见值有delete、truncate、persist、wal
  • PRAGMA synchronous:设置同步级别,取值0(OFF)、1(NORMAL)、2(FULL)
  • PRAGMA cache_size:查询或者设置页缓存大小

WAL模式是我在Qt项目中强烈推荐开启的。默认的delete日志模式,读操作和写操作之间是互斥的,写事务开始后,整个数据库文件对其它连接不可读。换成WAL模式后,写入发生在WAL文件中,原来的数据库文件保持可读,这极大改善了多线程同时读写时的阻塞问题。

db.exec("PRAGMA journal_mode=WAL"); db.exec("PRAGMA synchronous=NORMAL");

注意PRAGMA journal_mode=WAL这个模式的切换不是每次都生效的。如果程序之前已经以delete模式打开了这个数据库文件,需要关闭所有连接之后执行一次切换再重新连接。Qt程序里最稳妥的时机是首次创建连接并open()之后立即执行。

6.2 数据完整性:事务与checkpoint

开启WAL模式后,WAL文件会逐渐增长。SQLite在达到一定阈值后会自动触发checkpoint操作,把WAL文件中的数据合并回主数据库文件并清空WAL文件。使用PRAGMA wal_autocheckpoint可以控制自动checkpoint的页数。我在长期跑的数据采集程序里会让自动checkpoint保持默认,并在每天深夜空闲时间手动执行一次:

db.exec("PRAGMA wal_checkpoint(TRUNCATE)");

TRUNCATE参数相比默认的PASSIVE强在checkpoint完成后会把WAL文件直接截断归零,而不是只合并数据但保留文件大小。长时间不checkpoint且不重启程序,磁盘上可能同时存在主库文件、WAL文件和SHM文件三个文件,备份时如果只拷贝主库文件,数据可能是不完整的。这个问题经常在移动数据目录时被忽略。

6.3 SQLite的数据类型与Qt类型映射

SQLite是弱类型数据库,建表时可以声明INTEGER、TEXT、REAL、BLOB、NUMERIC这些存储类型。Qt的QVariant类型映射关系大致为:

SQLite类型Qt读取后的QVariant类型常用转换方法
INTEGERint / qlonglongtoInt() / toLongLong()
REALdoubletoDouble()
TEXTQStringtoString()
BLOBQByteArraytoByteArray()
NULL无效QVariantisValid()判断

一个常见问题是:INTEGER字段在Qt中读取可能返回qlonglong(64位),直接toInt()在数据值较大的时候会溢出。稳妥的做法是整型字段统一用toLongLong()读取,再按需转成int。

NULL值在Qt中表现出来的是一个无效QVariant,判断时不要用value(col).isNull(),要用value(col).isValid()。isNull()对无效QVariant返回false——被这个坑折磨过的朋友一定会记得。

7. 常见问题与排查技巧实录

7.1 database is locked

这个错误提示几乎所有用过SQLite的人都碰到过。在Qt里它的完整报错一般是“database is locked”。出现时机通常是两个连接同时想要写数据库,其中一个连接已经持有了写锁。

解决思路如下:

第一步,检查是否开启了长时间事务没提交。事务开启后没commit()也没rollback(),锁一直不释放。程序里全局搜索transaction(),逐个排查对应的commit()和rollback()是不是都走到了。

第二步,确认是否有多线程并发写。SQLite在同一时间只允许一个连接执行写操作。Qt项目里如果多个线程各自持有连接且都会写库,建议加一个全局写锁(QMutex),或者把写操作全部集中到同一个线程里做串行化处理。

第三步,开启WAL模式。前面讲过,WAL模式下读写并发的问题会小很多。

第四步,给数据库连接设置超时。QSQLITE驱动默认超时时间是5秒,可以通过设置连接选项修改:

db.setConnectOptions("QSQLITE_BUSY_TIMEOUT=30000");

这个选项的数值单位是毫秒,把超时时间拉长到30秒,可以减少因为短暂锁冲突导致的失败。需要知道这只是缓解手段,真正的锁冲突还得靠前面的写入串行化解决。

7.2 unable to open database file

这个错误最常出现在相对路径的使用上。Qt程序的工作目录在很多情况下不等于程序可执行文件所在目录。如果setDatabaseName("data/app.db")用的是相对路径,工作目录一变,路径就找不到。

解决方式是把数据库路径处理成绝对路径:

QString appDir = QCoreApplication::applicationDirPath(); QString dbPath = appDir + "/data/app.db"; QDir().mkpath(appDir + "/data"); db.setDatabaseName(dbPath);

Windows上路径分隔符要用反斜杠或者正斜杠混用都行,Qt内部做了兼容。macOS和Linux下建议用正斜杠连接路径,避免反斜杠转义问题。还有一个隐蔽因素:数据库所在目录没有写权限时,SQLite尝试创建文件会失败并返回这个错误,所以要做到先创建目录,再确认权限。

7.3 driver not loaded

QSqlDatabase: QSQLITE driver not loaded的报错说明Qt编译时没有启用SQLite驱动。正常安装的官方Qt版本都内置了SQLite,但如果看到这个错误,可以检查两点:

一是插件目录。开发环境下Qt安装目录的plugins/sqldrivers下面应该有qsqlite.dll(Windows)或者libqsqlite.so(Linux)。打包分发时手动拷贝了Qt运行库但漏掉了sqldrivers目录,是最常见的丢失场景。

二是插件搜索路径。程序运行时插件没有找到,添加一行代码指定路径:

QCoreApplication::addLibraryPath("plugins路径");

或者在代码里强制加载一下驱动:

QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");

如果驱动没有加载,addDatabase会返回一个无效的连接对象,后续操作全是空操作。

7.4 程序崩溃或闪退的常见隐含原因

SQLite在Qt中如果遭遇到写入时程序被强制终止(比如直接kill掉进程),可能留下一个未完成的WAL文件或者日志文件,下次打开数据库时可能会报告类似“database disk image is malformed”的错误。最好的保护方式是程序里做好事务管理,异常退出时尽可能让事务回滚,并且定期备份数据库文件。

断电或者崩溃修复的方法很少,SQLite本身的恢复能力有限,少量损坏可以通过导出导入方式抢救:用sqlite3命令行工具将正常数据dump成SQL文件,再把SQL导入到新库中。这种操作我在生产环境做过不止一次,成功率取决于损坏范围。

7.5 SQL大小写与保留字注意事项

SQLite对表名和字段名大小写不敏感,但字段值的比较默认是大小写敏感的。WHERE name='abc'和WHERE name='ABC'是两个结果,这一点和很多数据库不同。需要大小写不敏感匹配时可以用LIKE子句,LIKE在SQLite里对ASCII字符默认不区分大小写。

SQLite的保留字比想象的多。字段命名为order、group、index、select这类关键词时,SQL语句直接摆放会报语法错误。解决方式是给字段名加双引号或方括号:

SELECT "order" FROM table_name;

Qt的SQL字符串拼接时注意双引号在字符串里的转义,我一般建议在数据库设计阶段就避开保留字字段命名。

8. 一个完整的例子:从建表到查询展示

最后写一个完整的小工程示例,把前面讲的内容串起来。功能是:创建用户表,批量写入数据,用QSqlTableModel展示,支持删除选中行。

数据库管理部分:

class DatabaseManager { public: static bool initDatabase(const QString &dbPath) { QDir().mkpath(QFileInfo(dbPath).absolutePath()); QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "mainConn"); db.setDatabaseName(dbPath); if (!db.open()) { qWarning() << "数据库打开失败:" << db.lastError().text(); return false; } QSqlQuery query(db); if (!query.exec("CREATE TABLE IF NOT EXISTS user(" "id INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT NOT NULL," "age INTEGER DEFAULT 0)")) { qWarning() << "建表失败:" << query.lastError().text(); return false; } query.exec("PRAGMA journal_mode=WAL"); return true; } };

批量导入部分:

bool insertUsersBatch(const QString &connName, int count) { QSqlDatabase db = QSqlDatabase::database(connName); if (!db.isValid()) return false; db.transaction(); QSqlQuery query(db); query.prepare("INSERT INTO user(name, age) VALUES(?, ?)"); for (int i = 0; i < count; ++i) { query.addBindValue(QString("用户%1").arg(i)); query.addBindValue(18 + i % 50); if (!query.exec()) { db.rollback(); return false; } } return db.commit(); }

界面绑定部分在上一节已经写过,这里只提示一个重要细节:QSqlTableModel::setTable()之后需要调用select()才会真正执行查询。在这两者之间可以补充setFilter()和setSort(),setSort()传入字段序号和排序方式:

tableModel->setSort(0, Qt::AscendingOrder); tableModel->select();

删除选中行:

int rowIndex = currentIndex.row(); if (tableModel->removeRow(rowIndex)) { tableModel->submitAll(); }

removeRow如果后面没有submitAll(),数据不会真正从数据库删除,只会在内存标记为待删除,这个点很多新手会漏。

9. 踩坑经验总结

代码写到这里,再说几个我在实际项目中沉淀下来的经验。

第一,SQLite文件路径不要带中文和空格。Windows环境下中文路径在绝大多数情况没问题,但某些场景下QSQLITE驱动和中文路径组合会出现难以解释的读写异常。如果项目允许,统一用英文目录名,能省掉不少麻烦。

第二,数据库版本管理要做。项目迭代中表结构会变化。我习惯在主数据库旁建一个meta_version表,记录schema版本号。应用启动时读取版本号,如果版本低于当前期望值,依次执行迁移SQL脚本。这个做法简单但极其有用,不然用户升级程序的时候数据文件不兼容,客服电话会被打爆。

第三,大字段BLOB数据尽量避免放在主表里。二进制数据写进SQLite虽然方便,但会导致数据库文件膨胀快、查询拖慢整体性能。大文件我倾向于存文件路径,文件本身放在数据目录,小而美这个原则在SQLite场景下也非常适用。

第四,QSqlQuery尽量短生命周期。每次执行SQL创建局部QSqlQuery,用完立即析构,避免长时间持有查询对象导致连接资源被占用。特别是在循环里,把QSqlQuery声明在for循环外面复用可以节约开销,但一定不要在循环未结束时对数据库做结构性变更。

第五,不到万不得已不要用QSqlDatabase的默认连接名来管理多个数据库。默认连接名是全局的,事件循环里某个地方的代码随手addDatabase("QSQLITE"),就会覆盖掉另一个地方正在使用的默认连接。保持命名连接,程序可维护性和安全性都会高很多。

SQLite看起来简单,但加上Qt的多线程、模型视图、事务机制之后,细节很多。把连接管理做好、事务用好、SQL写规范,这个组合在桌面软件开发里是非常能打的。后面有时间的话,我再把多线程并发访问和加密数据库这两块单独拿出来拆解,这两个方向也是我实际项目中折腾最深的部分。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:50:05

产线串码写入与校验工具包实践:从SN设计到ADB写码与MES对接

简介&#xff1a;面向创维电视等智能终端的工厂产线场景&#xff0c;这份串码写入与校验工具包专注解决SN码、MAC地址等关键信息的批量写入、读取与校验问题&#xff0c;覆盖扫码、写码、打标到工位联调的完整流程&#xff0c;并针对批量产线操作设计了简洁的命令交互方式。压缩…

作者头像 李华
网站建设 2026/10/20 8:49:44

nginx网站部署实战:从HTTP请求原理到反向代理配置全指南

上周帮一个做前端的朋友排查线上问题时&#xff0c;他发来一张截图让我崩溃了半小时——页面能正常打开&#xff0c;但所有接口全部报404&#xff0c;静态资源却加载得飞起。他用的就是从某云服务器上直接解压前端包、然后跑http-server这种临时方案。这已经不是第一次有人问“…

作者头像 李华
网站建设 2026/10/20 8:49:43

神秘模型Pony Alpha被识破:GLM-5底座与OpenClaw调教组合解析

Pony Alpha 在榜单上挂了两周&#xff0c;头像是一匹粉色小马&#xff0c;英语和中文混着来&#xff0c;代码能力强得不像开源货。当时几乎所有跑榜的人都在猜同一个问题&#xff1a;它到底是什么模型&#xff1f;有人猜是某实验室偷偷放出来的新旗舰&#xff0c;有人猜是某个闭…

作者头像 李华
网站建设 2026/10/20 8:49:35

榆林企业选GEO服务商防坑指南:五个信号识别真假AI搜索优化

这两年&#xff0c;AI搜索给企业获客带来的变化&#xff0c;比很多人感受的要大得多。拿着同样一个问题去问ChatGPT、Perplexity、文心一言这类工具&#xff0c;得到的答案不再是一个网址列表&#xff0c;而是一段"有来源、有依据"的总结。谁的名字被这段总结提到&am…

作者头像 李华
网站建设 2026/10/20 8:48:31

Delphi视频采集控件TVideoGrabber:从摄像头到RTSP拉流与帧处理实战

简介&#xff1a;Delphi 12.3 环境下可直接集成的视频处理控件包&#xff0c;来自 Datastead 的 TVideoGrabber SDK v15.2.5.3&#xff0c;支持 Windows、macOS、iOS、Android 等多平台&#xff0c;主要面向需要快速实现视频采集、摄像头预览、录制与流媒体推流的 Delphi 开发者…

作者头像 李华
网站建设 2026/10/20 8:48:17

Unity嵌入式Office/PDF渲染方案:跨平台文档集成技术

简介&#xff1a;本资源是一套面向Unity开发者&#xff08;尤其聚焦Android平台&#xff09;的文档文件内嵌显示技术方案&#xff0c;解决教育类、企业展示类应用中需在Unity运行时加载并渲染Word、Excel、PDF、PPT等办公文档的核心需求。方案基于C#脚本集成FreeSpire、Aspose.…

作者头像 李华