基于 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 注册表 等。

相关推荐
AI人工智能+电脑小能手4 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
yyuuuzz4 小时前
独立站的技术基础与常见运维问题
大数据·运维·服务器·网络·数据库·aws
isyangli_blog5 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008116 小时前
FastAPI APIRouter
开发语言·python
Benszen6 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木6 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充6 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~6 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball6166 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang