1. 灾难现场
在开发一款本地记事本应用时,我们使用了Qt自带的QSqlDatabase (SQLite) 来存储数据。
代码在Windows上运行得行云流水,但在鸿蒙真机上测试时,出现了两个严重问题:
- 无法创建数据库 :
QSqlDatabase::open()返回 false,错误信息提示unable to open database file。 - 频繁死锁 :当我们在后台线程同步数据的同时,在主线程读取列表,程序抛出
database is locked错误。
2. 陷阱一:沙箱路径
在Desktop OS上,我们可以随意在 D:/data.db 或 ./data.db 创建文件。
但在鸿蒙系统中,应用被严格限制在沙箱内。
错误代码:
cpp
db.setDatabaseName("data.db"); // 试图在当前工作目录创建
在鸿蒙应用启动时,当前工作目录(CWD)可能并不是一个可写的目录,或者是一个临时的系统路径。
正确姿势:
必须使用QStandardPaths获取应用私有数据目录。
cpp
QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
// 输出示例: /data/app/el2/100/base/com.example.note/files/
QDir dir(dataDir);
if (!dir.exists()) dir.mkpath(".");
QString dbPath = dir.filePath("note.db");
db.setDatabaseName(dbPath);
注意: 确保目录存在!QSqlDatabase不会自动创建父目录。
3. 陷阱二:并发死锁 (Database is locked)
SQLite默认使用回滚日志(Journal)模式,在这种模式下,写操作是独占的。当Qt主线程在读取(Select),而后台线程试图写入(Insert)时,就会发生冲突。
在鸿蒙的文件系统上,IO性能可能不如PC SSD,导致锁等待时间变长,更容易触发 busy 错误。
锁等待示意图
MainThread (Reader) SQLite WorkerThread (Writer) MainThread WorkerThread SELECT * FROM notes (Shared Lock) BEGIN TRANSACTION INSERT INTO notes (Reserved Lock) COMMIT (Needs Exclusive Lock) BUSY! (Reader has Shared Lock) Wait... Timeout... ERROR! MainThread (Reader) SQLite WorkerThread (Writer) MainThread WorkerThread
4. 解决方案:开启WAL模式
WAL (Write-Ahead Logging) 模式允许读写并发。读操作不再阻塞写操作,写操作也不阻塞读操作。
代码实现:
在打开数据库后,立即执行Pragma指令。
cpp
bool initDatabase() {
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(getDbPath());
if (!db.open()) {
qCritical() << "Open failed:" << db.lastError();
return false;
}
// 开启WAL模式
QSqlQuery query;
if (!query.exec("PRAGMA journal_mode = WAL")) {
qWarning() << "Failed to set WAL mode";
} else {
if (query.next()) {
qDebug() << "Journal mode set to:" << query.value(0).toString();
}
}
// 增加忙等待超时 (默认可能很短)
query.exec("PRAGMA busy_timeout = 5000"); // 5秒
return true;
}
5. 架构优化:单例模式管理连接
在多线程环境使用QSqlDatabase,Qt文档建议每个线程使用不同的连接名(Connection Name)。
但为了管理方便,我们建议封装一个线程安全的单例类。
cpp
// DBManager.h
class DBManager {
public:
static DBManager& instance();
// 获取当前线程的数据库连接
QSqlDatabase getDatabase();
private:
// 线程本地存储,确保每个线程有独立的连接名
static thread_local QString m_connectionName;
};
cpp
// DBManager.cpp
thread_local QString DBManager::m_connectionName;
QSqlDatabase DBManager::getDatabase() {
if (m_connectionName.isEmpty()) {
// 生成唯一连接名,如 "conn_0x1234abcd"
m_connectionName = QString("conn_%1").arg((quintptr)QThread::currentThreadId());
}
if (!QSqlDatabase::contains(m_connectionName)) {
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", m_connectionName);
db.setDatabaseName(getGlobalDbPath());
db.open();
// Set WAL...
}
return QSqlDatabase::database(m_connectionName);
}
6. 总结
在鸿蒙上使用SQLite,核心要点只有三个:
- 路径对不对 :必须用
QStandardPaths::AppDataLocation。 - 模式开没开 :强烈建议开启
WAL模式提升并发性能。 - 连接管没管 :多线程必须用不同的连接名,切忌跨线程共用
QSqlDatabase对象。
做好了这三点,SQLite在鸿蒙上依然是那个轻量、可靠的数据存储王者。