基于 QxOrm 的 Qt 持久化层技术指南
1. 定位与价值
QxOrm 是面向 Qt 的 ORM:用 C++ 类型描述表结构 ,把 INSERT/UPDATE/DELETE/SELECT 从手写 SQL 中抽离出来,统一由库根据"已注册的元数据"生成或组装 SQL。底层仍依赖 Qt SQL(QSqlDatabase、QSqlQuery、QSqlError) ,因此与 Qt 事件循环、线程模型、数据类型(QString、QDateTime、QByteArray 等)天然一致。
在雷达仿真这类桌面客户端中,典型诉求是:本地 SQLite 存设备/任务/日志,界面用 QWidget/Qt Quick 展示;ORM 负责把 QObject 层不直接该碰的 SQL 细节 收口到 DAO/Repository 一层,界面只面对 领域对象或 JSON,这就是"与 Qt 交互"的主要边界。
2. Qt 侧前置条件
工程依赖 :QT += sql(以及你模块若要用 widgets 再加 widgets)。QxOrm 常以源码或预编译库形式放入工程(你项目中位于 ThirdParty/QxOrm),包含路径 指向其 include,链接对应库或直接把需要编译的源加入工程(视发行方式而定)。
驱动 :SQLite 对应 QSQLITE;QxOrm 的 QxSqlDatabase 封装了 setDriverName、setDatabaseName、setConnectOptions 等,实质仍调用 QSqlDatabase::addDatabase。
线程 :Qt 的 QSqlDatabase 连接与线程有关------同一连接名不可跨线程乱用。QxOrm 的 QxSqlDatabase 单例通常提供按线程取连接 的策略(参见其文档"connection per thread")。业务代码若在 QThread 里访问数据库,应使用 QxSession 传入显式 QSqlDatabase 或遵循 QxOrm 推荐的线程模型,避免 QSqlDatabase: database not open 类错误。
3. 核心概念:把"类"注册成"表"
QxOrm 的入口是:为持久化类注册类元数据 ------表名、主键(id)、普通列(data)、以及可选的关系(one-to-many 等)。
典型模式:
- 在
.cpp文件使用宏完成类型导出注册(如EE_QX_REGISTER_COMPLEX_CLASS_NAME_CPP或 QxOrm 自带的QX_REGISTER_HPP/QX_REGISTER_CPP系列)。 - 特化
qx::register_class(QxClass<T>& t):t.setName("TableName")--- 与datamodel_global.h中宏常量保持一致,便于全文搜索与参数工具按表名绑定。t.id(&T::m_ID, "ID", ...)--- 主键;自增策略与数据库相关,SQLite 下常见AUTOINCREMENT行为由生成器处理。t.data(&T::m_Field, "ColumnName", ...)--- 普通列映射。
C++ 知识要点 :这里使用的是成员指针 (&T::m_ID),模板元编程在编译期收集字段偏移与类型,从而生成绑参的 INSERT 列表。字段类型支持 Qt 常见类型及自定义类型的序列化。
"新建表格"在语义上两层含义:
- 应用第一次部署 :库文件不存在,
create_table<T>()根据注册信息执行CREATE TABLE。 - 已存在库文件 :
create_table若实现为"仅当不存在则建表",则改列不会自动反映 ------必须 迁移脚本 (ALTER TABLE)或 删库重建。这是 ORM 通用陷阱,与 QxOrm/Qt 无矛盾,是 SQLite 生命周期问题。
4. 连接数据库:QxSqlDatabase 单例
初始化流程(对应你项目 MyDao::InitDataBase):
qx::QxSqlDatabase::getSingleton()->setDriverName("QSQLITE")setDatabaseName("./DB/RadarData.db"),并QDir::mkpath保证目录存在- 可选:
setUserName/setPassword(SQLite 常忽略,但接口保留) setSessionAutoTransaction(true):与后续QxSession配合时自动开事务setSessionThrowable(true):SQL 错误是否抛异常(团队需统一风格)
与 Qt 交互点 :路径可用 QCoreApplication::applicationDirPath() + 相对子目录,避免写死盘符;配置可放 QSettings,在 InitDataBase 读取------你代码里的 TODO 即此类改进。
5. 新建表(Create Table)
调用:
cpp
QSqlError err = qx::dao::create_table<MyEntity>();
通常封装在应用启动的 InitTable():对每个实体调用一次 ,且前面用 sqlite_master 判断表是否已存在,避免重复 CREATE 报错。
后续优化 :把"检查存在 + create_table + 特殊索引"做成单一模板函数 ,新表只需在 InitTable() 增加一行 create_table<EE::NewObj>(NEW_TABLE)。
6. 插入(Insert)
cpp
MyEntity e;
// 填充字段;若 ID 为 0 或未设,视自增策略而定
QSqlError err = qx::dao::insert(e);
与业务层结合 :你项目用 GetID() <= 0 判"新增",再 InsertOne,本质是 把"无有效主键"定义为插入 。插入成功后,若使用 SQLite AUTOINCREMENT,部分配置下 ORM 会把生成的主键写回 对象,这一点要在联调时验证(读回 e.m_ID)。
批插 :循环 insert 或 QxOrm 提供的 batch 接口;性能敏感时可包裹 QxSession 单事务。
7. 更新(Update)
cpp
QSqlError err = qx::dao::update(e);
update 一般按主键定位行。若主键无效,行为依赖实现,通常应在前置校验。
项目 AddOrUpdate 模式 :ID > 0 走 update,否则 insert,这是桌面软件里最直观的"保存一条"语义,复用性高 :所有实体可抽成模板函数 template<class Ptr> int Save(Ptr p),减少 DataRepository 里重复代码。
9. 查询全部、条件查询与排序
查全部:
cpp
QSqlError err = qx::dao::fetch_all(list); // list 为 QxOrm 支持的容器类型
带排序 :项目用 qx_query(qx::QxSqlQuery):
cpp
qx_query q;
q.orderDesc("ID").limit(100);
err = qx::dao::fetch_by_query(q, list);
或简单场景:qx::QxSqlQuery query("ORDER BY Name"); 再 fetch_by_query。
条件:
cpp
qx_query q;
q.where("Status").isEqualTo(1).and_("Name").containsString(keyword);
err = qx::dao::fetch_by_query(q, list);
C++/Qt 提示 :QVariant 承载条件值,注意 类型与 SQL 绑定 一致;QString 模糊匹配注意 LIKE 通配符是否与 containsString 封装一致。
按主键查一行 :fetch_by_id(obj),要求 obj 内主键已设。
复用 :把 where + order + limit 组合成 小函数 或 建造者封装 (MyDao::querybyCondition 已是一种复用),避免界面层拼 SQL 字符串。
10. 删除与"清空表"
- 按主键删 :
qx::dao::delete_by_id(obj); - 按查询条件物理删 :
destroy_by_query<T>(query);(命名随版本,以头文件为准) - 删全表数据 :
delete_all<T>();慎用,需权限与确认对话框
项目 内存列表同步 :Delete 成功后 removeOne,保证 DB 与内存缓存一致 ------这是 Repository 模式职责,ORM 只负责 DB。
11. 事务:QxSession 与 RAII
长事务或批处理应使用:
cpp
qx::QxSession session;
session += qx::dao::insert(a, session.database());
session += qx::dao::update(b, session.database());
if (!session.isValid()) { /* 处理 session.firstError() */ }
// 析构时提交或出错回滚(视配置)
要点 :每个 qx::dao::xxx 建议传入 session.database() 同一连接,否则事务边界失真。
与 Qt :若在 UI 线程跑事务,注意 勿长时间锁 UI ;大批量导入放 Worker 线程 + 进度信号。
12. 与 Qt 的交互:分层与实践
- 模型层 :
QSqlTableModel/QSqlQueryModel是 Qt 原生 SQL 模型,不必 与 QxOrm 二选一;可以 ORM 管写,QAbstractTableModel自己绑QList<shared_ptr<Entity>>管读------大屏表格常用后者,类型更安全。 - JSON :项目
WriteJson/ReadJson把实体与协议/导入导出打通;ORM 不替代 JSON,而是 DB ↔ 对象 ,JSON 再 对象 ↔ 网络/文件。 - 信号槽 :Repository 在
AddOrUpdate成功后emit dataChanged(),多窗体同步。 - 异步 :
QtConcurrent/QThread里跑 DAO 时,保证 一线程一连接 或使用 QxOrm 连接策略,避免跨线程传递QSqlQuery。
13. 复用架构小结(对标你工程)
- 单列
MyDao单例 :封装所有qx::dao模板方法 → 技能横向复用 (任何实体共享InsertOne等)。 DataRepository单例 :内存缓存 + 启动queryAll+ 业务化AddOrUpdate/Delete→ 领域复用。- 表名使用宏定义 +
register_class一处维护 → 避免后续更新导致不一致。 InitTable一行注册一张新表 → 运维可见性高。
改已有表结构(加列 / 删列 / 改类型)
1. 实体与 ORM 映射(必改)
DataModel/某Object.h:增删改成员变量(及访问接口)。DataModel/某Object.cpp:register_class里与表字段对应的t.id(...)、t.data(...)必须和成员一致。- 该对象参与界面/协议 JSON:
WriteJson/ReadJson同步改。 - 构造函数里设置了
m_TableName = XXX_TABLE,表名常量不变则一般不用动。
2. 数据库里"旧表"要删除
InitTable()里用的是:表不存在才qx::dao::create_table<T>()。
已经存在的 SQLite 表不会因为改 C++ 而自动 ALTER。
3. 业务与界面
DataModel/datarepository.cpp:只有当你新增的字段 要参与默认加载、业务规则时,才改对应逻辑;单纯多一列、仍走同一套InsertOne/UpdateOne,有时只改实体即可。RadarClient里绑定该实体的界面 :表格列、编辑框、AutoParamBoxTool(表名, ...)等,字段变了就要跟。mydao.cpp/mydao.h:只是换表名 或换实体类型 才要动;单纯加列、同一张表、同一 C++ 类,一般不用 改MyDao。
4. 表名常量(极少)
- 表名在
DataModel/datamodel_global.h的#define XXX_TABLE。
只有当你要改名 时才改这里,并全局搜XXX_TABLE一并替换。
5. 如果表格需要联动
先假定:
A 表:TableA,有序列号列 SerialNo,还有要同步到 B 的列 SomeField
B 表:TableB,也有 SerialNo,以及同名的 SomeField
情形 A:序列号不变,只是改别的字段 ------ 用 NEW.SerialNo 去定位 B 行:
cpp
CREATE TRIGGER IF NOT EXISTS trg_after_update_tablea_sync_b
AFTER UPDATE ON TableA
FOR EACH ROW
BEGIN
UPDATE TableB
SET SomeField = NEW.SomeField
WHERE SerialNo = NEW.SerialNo;
END;
情形 B:序列号本身可能被改掉 ------ B 里还是旧序列号,要用 OLD.SerialNo 找到 B,再写成新值:
cpp
CREATE TRIGGER IF NOT EXISTS trg_after_update_tablea_sync_b_serial
AFTER UPDATE ON TableA
FOR EACH ROW
WHEN OLD.SerialNo IS NOT NEW.SerialNo OR OLD.SomeField IS NOT NEW.SomeField
BEGIN
UPDATE TableB
SET SerialNo = NEW.SerialNo,
SomeField = NEW.SomeField
WHERE SerialNo = OLD.SerialNo;
END;
装好触发器之后:只要对 A 表执行 UPDATE(含 QxOrm 的 qx::dao::update),SQLite 在这一条 UPDATE 成功后会自动跑触发器里的 UPDATE B ...,不用在 C++ 里写"调用触发器"。
新建一张表
按现有表(如 DeviceObject)照抄一遍流程:
| 步骤 | 文件 | 做什么 |
|---|---|---|
| 1 | datamodel_global.h |
#define NEW_TABLE "YourTableName" |
| 2 | DataModel/NewObject.h / .cpp |
定义 namespace EE 下的类;.cpp 里 EE_QX_REGISTER_... + register_class(setName(NEW_TABLE)、t.id、t.data...);按需 WriteJson/ReadJson。 |
| 3 | DataModel.pro |
SOURCES / HEADERS 加入新 .cpp / .h。 |
| 4 | mydao.h |
#include "NewObject.h"(与其它 Object 一样)。 |
| 5 | mydao.cpp → InitTable() |
create_table<EE::NewObject>(NEW_TABLE); |
| 6 | datarepository.h / datarepository.cpp |
增加:指针类型别名、m_NewObjList、GetNewObjList();Initialize() 里 queryAll(m_NewObjList)(或 queryLimitCount,与日志类表一致);AddOrUpdate / Delete (与现有表同一种签名风格);若有父子关系再考虑 DeleteByParentID。 |
| 7 | 其它模块 | 需要给用户看或下发设备:RadarClient 界面、网络命令等,按业务再加(不建 UI 也可以只在仓库层用)。 |
可选 :若要用 AutoParamBoxTool(NEW_TABLE, ...) 之类按表名自动绑参,还要保证 SharedModel/参数模板里能识别该表名(你工程里已有按表名工具,需与表名一致)。
14. 常见误区与排错
- 表已存在但列不一致 :运行期
no such column------需要迁移或重建。 - ID 未回写 :插入后仍用 0 做更新------检查自增与
fetch_after_insert类选项。 - 排序不写
ORDER BY:数据库返回顺序不稳定,勿依赖物理顺序。 - 在 UI 线程批量 IO:卡顿------worker + 信号进度。
- 多处打开多个单例连接名 :注意
addDatabase的连接名唯一性。
15. 结语
QxOrm 在 Qt 生态中扮演 "类型安全的 SQL 生成器 + 持久化生命周期管理" 角色:建表 依赖 create_table 与实体注册;插入/更新/保存 依赖 insert/update/save;排序与条件 依赖 QxSqlQuery/qx_query;与 Qt 的交互 应通过 Repository、JSON、Model、信号槽、线程边界 分层完成;复用 的抓手则是 单例 DAO、模板 CRUD、表名常量、InitTable 注册表 等。