基于 QxOrm 的 Qt 持久化层技术指南

基于 QxOrm 的 Qt 持久化层技术指南

1. 定位与价值

QxOrm 是面向 Qt 的 ORM:用 C++ 类型描述表结构 ,把 INSERT/UPDATE/DELETE/SELECT 从手写 SQL 中抽离出来,统一由库根据"已注册的元数据"生成或组装 SQL。底层仍依赖 Qt SQL(QSqlDatabaseQSqlQueryQSqlError ,因此与 Qt 事件循环、线程模型、数据类型(QStringQDateTimeQByteArray 等)天然一致。

在雷达仿真这类桌面客户端中,典型诉求是:本地 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 封装了 setDriverNamesetDatabaseNamesetConnectOptions 等,实质仍调用 QSqlDatabase::addDatabase

线程 :Qt 的 QSqlDatabase 连接与线程有关------同一连接名不可跨线程乱用。QxOrm 的 QxSqlDatabase 单例通常提供按线程取连接 的策略(参见其文档"connection per thread")。业务代码若在 QThread 里访问数据库,应使用 QxSession 传入显式 QSqlDatabase 或遵循 QxOrm 推荐的线程模型,避免 QSqlDatabase: database not open 类错误。


3. 核心概念:把"类"注册成"表"

QxOrm 的入口是:为持久化类注册类元数据 ------表名、主键(id)、普通列(data)、以及可选的关系(one-to-many 等)。

典型模式:

  1. .cpp 文件使用宏完成类型导出注册(如 EE_QX_REGISTER_COMPLEX_CLASS_NAME_CPP 或 QxOrm 自带的 QX_REGISTER_HPP/QX_REGISTER_CPP 系列)。
  2. 特化 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 > 0update,否则 insert,这是桌面软件里最直观的"保存一条"语义,复用性高 :所有实体可抽成模板函数 template<class Ptr> int Save(Ptr p),减少 DataRepository 里重复代码。

9. 查询全部、条件查询与排序

查全部

cpp 复制代码
QSqlError err = qx::dao::fetch_all(list); // list 为 QxOrm 支持的容器类型

带排序 :项目用 qx_queryqx::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 的交互:分层与实践

  1. 模型层QSqlTableModel/QSqlQueryModel 是 Qt 原生 SQL 模型,不必 与 QxOrm 二选一;可以 ORM 管写,QAbstractTableModel 自己绑 QList<shared_ptr<Entity>> 管读------大屏表格常用后者,类型更安全。
  2. JSON :项目 WriteJson/ReadJson 把实体与协议/导入导出打通;ORM 不替代 JSON,而是 DB ↔ 对象 ,JSON 再 对象 ↔ 网络/文件
  3. 信号槽 :Repository 在 AddOrUpdate 成功后 emit dataChanged(),多窗体同步。
  4. 异步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 下的类;.cppEE_QX_REGISTER_... + register_classsetName(NEW_TABLE)t.idt.data...);按需 WriteJson/ReadJson
3 DataModel.pro SOURCES / HEADERS 加入新 .cpp / .h
4 mydao.h #include "NewObject.h"(与其它 Object 一样)。
5 mydao.cppInitTable() create_table<EE::NewObject>(NEW_TABLE);
6 datarepository.h / datarepository.cpp 增加:指针类型别名、m_NewObjListGetNewObjList()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 注册表 等。

相关推荐
csbysj20201 小时前
Python 列表(List)
开发语言
m0_748554811 小时前
uni-app怎么实现App指纹登录 uni-app生物识别API接入流程【详解】
jvm·数据库·python
2301_809204701 小时前
c++字符串运算_连接、比较、输入输出等运算符重载应用
jvm·数据库·python
a7963lin2 小时前
PHP怎么实现单例模式_PHP常用设计模式之单例模式【方法】
jvm·数据库·python
辞旧 lekkk9 小时前
【Qt】信号和槽
linux·开发语言·数据库·qt·学习·mysql·萌新
2zcode10 小时前
运动模糊图像复原的MATLAB仿真与优化
开发语言·matlab
袁雅倩199710 小时前
当吸尘器、筋膜枪都用上Type-C,供电方案该怎么选?浅谈PD取电芯片ECP5702的应用
c语言·开发语言·支持向量机·动态规划·推荐算法·最小二乘法·图搜索算法
2301_8092047010 小时前
JavaScript中严格模式use-strict对引擎解析的辅助.txt
jvm·数据库·python
zjy2777711 小时前
mysql如何选择合适的索引类型_mysql索引设计实战
jvm·数据库·python