电力设备运维场景:变电站设备经 IEC 104 规约报文上送遥测/遥信数据,Qt 后台接收解析后写入达梦数据库,界面用 Qt Model/View(MVC)框架展示。 数据库部分讲解详细,全程与 MySQL 对比。
一、场景背景
业务链路:
变电站设备 --(IEC 104 报文)--> Qt 后台 TCP 服务端 --> 报文解析
--> 达梦数据库(DM8) --> QSqlTableModel --绑定--> QTableView 界面
- 遥测(YC):电流、电压、功率等模拟量,周期上送。
- 遥信(YX):开关位置、刀闸状态等开关量,变位上送。
本篇定位:MVC 基础原理(QModelIndex/Role/Delegate 机制)已在 ../Qt/Qt_ModelView_MVC架构详解.md 详细讲解,本篇不重复, 重点放在数据库侧:达梦连接、三种数据库 Model 的用法、报文落库与界面刷新的完整链路。
二、达梦连接配置(详细)
2.1 为什么 Qt 用 ODBC 连接达梦
| 对比项 | 达梦 DM8 | MySQL |
|---|---|---|
| Qt 驱动 | 无官方原生驱动,走 ODBC(QODBC) | 官方 QMYSQL 驱动 |
| 端口 | 5236 | 3306 |
| 驱动来源 | 达梦安装目录自带 ODBC 驱动(drivers/odbc/ 下) | Qt 编译时编译 qsqlmysql 插件 |
MySQL 有 QMYSQL 驱动,直接 QSqlDatabase::addDatabase("QMYSQL") 即可。
达梦没有对应的 Qt 原生驱动,只能通过微软 ODBC 或 unixODBC 桥接,Qt 侧统一使用 QODBC。
2.2 达梦 ODBC 驱动
达梦安装后,在 安装目录/drivers/odbc/ 下提供 ODBC 驱动库:
- Windows:32 / 64 位驱动 DLL 各一(教程常写作
dm8_odbc.dll/dm8_odbc64.dll) - Linux:ODBC 驱动
.so(教程常写作libdm8_odbc.so)
⚠️ 待实测:以上文件名与 ODBC 管理器中的驱动显示名随达梦版本可能不同, 以本机
drivers/odbc/目录和 ODBC 管理器实际显示为准;连接串Driver={...}花括号内 必须与显示名逐字一致(本笔记示例Driver={DM8 ODBC DRIVER}亦待实测核对)。
ODBC 位数必须与 Qt 程序位数一致(最常见坑,详见第六章)。
注册 ODBC 驱动(Windows)——正确入口是 ODBC 数据源管理器(odbcad32.exe),不是 regsvr32:
:: 64 位 Qt 程序 → 用 64 位管理器 C:\Windows\System32\odbcad32.exe
:: 32 位 Qt 程序 → 用 32 位管理器 C:\Windows\SysWOW64\odbcad32.exe
:: "驱动程序"选项卡 → 添加 → 选择达梦 ODBC 驱动 DLL;
:: 再在"系统 DSN"新建:名称 DM8DSN,服务器 localhost,端口 5236
误区说明:
regsvr32用于注册自注册的 COM/DLL 组件,不是 ODBC 驱动的注册方式; ODBC 驱动须经 ODBC 管理器/安装程序登记,对不含DllRegisterServer入口点的驱动 DLL,regsvr32只会报错。
2.3 QSqlDatabase 连接代码(真实可用)
#include <QSqlDatabase>
#include <QSqlError>
#include <QDebug>
bool connectDameng()
{
// 1. 按 Qt SQL 插件名创建连接(ODBC 通道的插件名为 QODBC)
QSqlDatabase db = QSqlDatabase::addDatabase("QODBC");
// 2. 连接串(三种写法,任选其一)
// 写法A:使用已配置的 ODBC 数据源名(DSN)
db.setDatabaseName("DM8DSN");
// 写法B:直接写连接串(无需先建 DSN,推荐)
// 注意:Database= 填的是实例库名(dminit 的 DB_NAME,缺省 DAMENG),不是模式名;
// 模式由登录用户决定(SYSDBA 的默认模式是 SYSDBA),详见本节末"要点"
QString conn = QString(
"Driver={DM8 ODBC DRIVER};"
"Server=%1;Port=%2;Database=%3;")
.arg("127.0.0.1").arg(5236).arg("DAMENG");
db.setDatabaseName(conn);
// 写法C:带字符集与用户名
// ⚠️ 待实测:Driver/Server/Port/Database/UserID/Password/Charset 这些关键字
// 是否为达梦 ODBC 驱动所支持、拼写如何,以驱动文档/实测为准
// 建议指定 UTF-8 字符集,避免中文乱码
QString conn2 = QString(
"Driver={DM8 ODBC DRIVER};Server=127.0.0.1;Port=5236;"
"Database=DAMENG;UserID=SYSDBA;Password=***;"
"Charset=UTF8;"); // ⚠️ 待实测:达梦 ODBC 是否支持此字符集关键字,勿凭 MySQL 经验照抄
db.setDatabaseName(conn2);
// 3. 设置账号密码
db.setUserName("SYSDBA"); // 达梦默认管理员账户,类似 MySQL 的 root
db.setPassword("<达梦默认口令>"); // 占位符:按你的实际口令填写
// 4. 打开连接
if (!db.open()) {
qCritical() << "达梦连接失败:" << db.lastError().text();
return false;
}
qDebug() << "达梦连接成功, 驱动:" << db.driverName();
return true;
}
与 MySQL 对比:
// MySQL 写法(对比)
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
db.setHostName("127.0.0.1");
db.setPort(3306);
db.setDatabaseName("mydb"); // MySQL 按"库"组织表;达梦的对等层级是"模式"(见下方要点)
db.setUserName("root");
db.setPassword("***");
db.open();
要点(库名 / 模式 / 用户三者关系):达梦一个实例只有一个数据库,库名即
dminit的DB_NAME(缺省即DAMENG);连接串里的Database对应这个库名,不是模式名。 真正组织表的是模式(Schema):模式由登录用户决定,一个实例可建多个用户,每个用户默认对应同名模式(SYSDBA 的默认模式就是 SYSDBA)。登录用户不同,默认模式不同。 与 MySQL 对照:MySQL 的"database(库)“这一层对应达梦的”模式(Schema)",而不是达梦的"库名"——MySQL 里"库.表"的组织方式,到达梦里对应"模式.表"。
2.4 达梦兼容模式 COMPATIBLE_MODE
达梦 8 通过 COMPATIBLE_MODE 参数控制与其他数据库语法的兼容模式(通常在建库时经 dminit 指定,属实例级参数)。
⚠️ 取值含义待实测:社区资料与官方文档对该参数数值含义的口径存在分歧 (例如 1 对应 Oracle 还是 SQL92、“兼容 MySQL"是否为 4、当前默认值为何),且可能随 DM 版本变化。 本笔记不写死取值表,请以本机实测与所用版本的官方文档为准:
SELECT para_name, para_value FROM v$dm_ini WHERE para_name = 'COMPATIBLE_MODE';
配置 / 查询方式:
# 建库时指定(dminit 支持 COMPATIBLE_MODE 参数;取值先按官方文档/实测确认再填)
dminit PATH=/dm/data DB_NAME=DAMENG COMPATIBLE_MODE=<实测确认的取值>
# 查询当前实例的兼容模式
SELECT para_name, para_value FROM v$dm_ini WHERE para_name = 'COMPATIBLE_MODE';
注意:兼容模式只影响 SQL 语法解析层面(开启某种兼容后哪些其他数据库风格的语法可用,具体支持范围 ⚠️ 待实测)。 它不改变存储引擎架构,达梦仍是关系型行列存储,没有 MySQL 的 InnoDB/MyISAM 之分。 连接方式(ODBC)、事务、锁模型仍是达梦自己的。
2.5 达梦常用工具
| 工具 | 用途 | 类似 MySQL |
|---|---|---|
disql | 命令行 SQL 工具 | mysql 命令行 |
| DM 管理工具 | 图形化管理(建表/数据查看/调试) | Navicat / Workbench |
| DM 数据迁移工具 | MySQL → 达梦 数据迁移 | mysqldump |
| DM 控制台 / dm.ini | 实例配置 | my.ini |
三、三种数据库 Model 详解
3.1 继承关系
QObject
└── QAbstractItemModel (Item Model 基类)
├── QAbstractListModel
├── QAbstractTableModel (自己写表模型才继承它)
└── SQL 模型是挂在基类下的三级单链,并非 QAbstractTableModel 的子类:
QSqlQueryModel(只读,任意 SQL)
└── QSqlTableModel(单表读写)
└── QSqlRelationalTableModel(单表 + 外键关联显示)
三种 Model 都属于 Qt SQL 模块(QtSql),底层不区分达梦还是 MySQL——
只要 QSqlDatabase 连上了库,用法完全一致。这是 Qt 屏蔽数据库差异的最大好处。
3.2 QSqlQueryModel —— 只读查询模型
适合:统计报表、自定义复杂 SQL、不需要编辑的场景(如"今日告警数量”、“电压越限列表”)。
// 真实代码:查询遥测数据中的越限记录
QSqlQueryModel *model = new QSqlQueryModel(this);
model->setQuery(
"SELECT device_name, yc_name, yc_value, alarm_flag "
"FROM telemeter WHERE alarm_flag = 1 AND record_time > '2026-08-20'");
// 设置列头(数据库列名是英文,界面显示中文)
model->setHeaderData(0, Qt::Horizontal, tr("设备名"));
model->setHeaderData(1, Qt::Horizontal, tr("遥测名称"));
model->setHeaderData(2, Qt::Horizontal, tr("遥测值"));
model->setHeaderData(3, Qt::Horizontal, tr("越限标志"));
// 绑定视图
ui->tableView->setModel(model);
特点:只读,不能编辑单元格;setQuery 可随时换成新 SQL 刷新数据。
3.3 QSqlTableModel —— 单表读写模型(本篇主角)
适合:直接展示/编辑某张业务表(如设备信息表、遥测明细表)。
// 真实代码:绑定设备表,支持界面直接编辑
QSqlTableModel *model = new QSqlTableModel(this, db);
model->setTable("device_info"); // 表名,达梦默认大写标识符
model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 手动提交策略
model->setFilter("station_id = 1001"); // 过滤,等价于 WHERE
model->setSort(1, Qt::AscendingOrder); // 按第2列升序,等价于 ORDER BY
model->select(); // 执行 SELECT 加载数据
// 设置中文列头
model->setHeaderData(0, Qt::Horizontal, tr("设备编号"));
model->setHeaderData(1, Qt::Horizontal, tr("设备名称"));
model->setHeaderData(2, Qt::Horizontal, tr("所属站点"));
// 绑定视图
ui->tableView->setModel(model);
ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
setEditStrategy 三种编辑策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
OnFieldChange | 单元格一改就立即写库 | 单用户、数据量小 |
OnRowChange | 切行时提交本行修改 | 常见的表单式编辑 |
OnManualSubmit | 手动调用 submitAll() 才写库 | 报文批量入库、需要回滚的场景 |
// OnManualSubmit 模式下手动提交/回滚
if (model->submitAll()) {
qDebug() << "已保存到达梦";
} else {
qDebug() << "保存失败:" << model->lastError().text();
model->revertAll(); // 撤销所有未提交修改
}
3.4 QSqlRelationalTableModel —— 外键关联模型
适合:表中有外键,希望视图里直接显示关联表的名称列(而不是外键编号)。
场景:遥测表 telemeter 存的是 device_id,希望界面显示设备名称而不是编号。
// 真实代码:遥测表 + 设备表关联
QSqlRelationalTableModel *model = new QSqlRelationalTableModel(this, db);
model->setTable("telemeter");
model->setRelation(1, // 第2列是 device_id(外键)
QSqlRelation("device_info", "device_id", "device_name")); // 关联到设备表
// 用 QSqlRelationalDelegate 让视图以下拉框形式显示关联列
ui->tableView->setItemDelegate(
new QSqlRelationalDelegate(ui->tableView));
model->select();
3.5 三种 Model 对比
| 对比项 | QSqlQueryModel | QSqlTableModel | QSqlRelationalTableModel |
|---|---|---|---|
| 只读/读写 | 只读 | 读写 | 读写 |
| 表范围 | 任意 SQL/多表 | 单表 | 单表 + 外键 |
| 编辑能力 | 无 | 单元格编辑 | 单元格编辑 + 关联下拉 |
| 典型场景 | 统计报表 | 业务表维护 | 有外键的明细表 |
| 达梦/MySQL | 通用 | 通用 | 通用 |
通用性结论:三种 Model 在达梦与 MySQL 下代码完全相同,差异只在最上层连接串。 所以学会了 MySQL 的 Qt MVC 用法,达梦直接无缝迁移。
四、业务链路实现(伪代码 + 关键真码)
4.1 IEC 104 报文背景(简讲)
IEC 60870-5-104 是电力远动标准规约,基于 TCP 传输,帧结构核心(APDU):
启动字符 0x68 + 长度 + 控制域(4字节) + 类型标识 + 传送原因 + 公共地址 + 信息体
^ 帧头 ^ APDU长度 ^ 规约控制信息 ^ ASDU 部分
常用类型标识:
| 类型标识 | 含义 | 对应业务 |
|---|---|---|
| 1 (M_SP_NA) | 单点遥信 | 开关位置 |
| 9 (M_ME_NA_1) | 遥测(归一化值) | 电流/电压/功率 |
| 11 (M_ME_NB_1) | 遥测(标度值) | 电流/电压/功率 |
| 13 (M_ME_NC_1) | 遥测(短浮点) | 电流/电压/功率 |
| 100 (C_IC_NA) | 总召唤 | 启动时全量上送 |
4.2 伪代码:报文 → 解析 → 落库 → 界面刷新
// ============ 伪代码(业务逻辑示意) ============
// ① 线程A:TCP 接收报文(QUdpSocket/QTcpSocket)
void Iec104Server::onDataReady() {
QByteArray frame = socket->readAll();
emit packetParsed(parse104(frame)); // 信号携带解析结果
}
// ② 线程B:报文解析 → 组装成业务结构体
ParsedPacket parse104(const QByteArray &frame) {
ParsedPacket p;
if (frame[0] != 0x68) return p; // 帧头校验
p.typeId = frame[6]; // 类型标识
p.deviceId = 取公共地址/信息体中的设备号;
if (p.typeId == 9 || p.typeId == 11 || p.typeId == 13) p.ycValue = 按类型解析遥测值; // 9=归一化 / 11=标度 / 13=短浮点
if (p.typeId == 1) p.yxStatus = 解析开关位; // 遥信状态
return p;
}
// ③ 线程C:写入达梦(QSqlQuery 预处理,真实代码)
bool writeToDameng(const ParsedPacket &p) {
QSqlQuery q(db);
q.prepare(
"INSERT INTO telemeter(device_id, yc_name, yc_value, record_time) "
"VALUES(:dev, :name, :val, NOW())");
q.bindValue(":dev", p.deviceId);
q.bindValue(":name", "遥测1"); // 实际按点号查配置表
q.bindValue(":val", p.ycValue);
return q.exec();
}
// ④ 界面线程:报文入库后刷新表格(QSqlTableModel::select 重新查询)
// 信号槽跨线程安全:model 在界面线程,报文线程发信号触发
void MainWindow::onPacketStored() {
model->select(); // 关键:重新查库,界面自动刷新
}
关键点:
model->select()会重新执行 SELECT,最新入库的数据立即出现在表格。- 信号槽跨线程自动排队(
Qt::QueuedConnection),数据库写操作不阻塞界面。 - 高并发上报时,可在写库线程内做
QSqlDatabase事务批量提交(见 4.3)。
4.3 批量入库优化(真实代码)
104 报文可能每秒几十帧,逐条提交太慢,用事务批量提交:
// 事务批量写入(对比 MySQL 用法完全一致)
db.transaction();
QSqlQuery q(db);
q.prepare("INSERT INTO telemeter(device_id, yc_value, record_time) "
"VALUES(:dev, :val, NOW())");
for (const ParsedPacket &p : packets) {
q.bindValue(":dev", p.deviceId);
q.bindValue(":val", p.ycValue);
q.exec();
}
db.commit(); // 一次性提交,性能提升明显
// 失败回滚
// db.rollback();
4.4 界面联动:筛选、排序、点击行查看详情
// 表格按站点筛选(等价于 SQL WHERE)
model->setFilter(QString("station_id = %1").arg(currentStation));
model->select();
// 点击行获取原始数据
void MainWindow::onTableClicked(const QModelIndex &idx) {
int row = idx.row();
int deviceId = model->record(row).value("device_id").toInt();
QString ycValue = model->record(row).value("yc_value").toString();
// 弹窗或右侧面板显示该设备详情
}
五、与 MySQL 对比表(汇总)
| 对比项 | 达梦 DM8 | MySQL |
|---|---|---|
| Qt 驱动 | QODBC(ODBC 桥接) | QMYSQL |
| 端口 | 5236 | 3306 |
| 连接串 | 驱动+服务器+端口+Database(库名,非模式)+用户口令,模式随登录用户定(见 2.3 要点) | 主机+端口+数据库名 |
| 默认管理员 | SYSDBA | root |
| 大小写 | 默认敏感(CASE_SENSITIVE=Y,未加引号标识符存储时转大写) | 表名大小写敏感(Linux 下) |
| 字符集默认 | GB18030(CHARSET=0 缺省;1=UTF-8,2=EUC-KR) | utf8mb4 |
| 兼容模式 | COMPATIBLE_MODE 控制语法兼容(取值含义见 2.4,⚠️ 待实测,不写死) | 原生 |
| 事务 | 支持,默认行级锁 | InnoDB 事务 |
| 数据类型 | INT/NUMBER/VARCHAR/CLOB 等 | INT/DECIMAL/VARCHAR/TEXT |
| 自增列 | 序列 SEQUENCE;MySQL 式 auto_increment 语法是否可用取决于兼容模式与版本(⚠️ 待实测) | AUTO_INCREMENT |
| 生态 | 国产化、信创环境 | 开源生态大 |
| 许可 | 商业授权 | 开源/商业 |
六、常见坑与调试技巧
6.1 ODBC 32/64 位不匹配(最容易踩)
现象:open() 失败,报 “driver not found” 或 “无法加载 ODBC 驱动”。
原因:Qt 程序是 64 位,却注册了 32 位 DM ODBC 驱动(或反之)。
解决:查看 Qt 位数 → 在同位数的 ODBC 数据源管理器里,添加本机安装目录对应位数的达梦驱动 DLL(文件名以 drivers/odbc/ 实际内容为准,见 2.2;不要用 regsvr32,见 2.2 误区说明)。
# 64 位管理器:C:\Windows\System32\odbcad32.exe("驱动程序"选项卡 → 添加)
# 32 位管理器:C:\Windows\SysWOW64\odbcad32.exe
6.2 连接串报错排查
- 端口错误 → 确认 dm.ini 中的 PORT_NUM(默认 5236)。
Database=DAMENG的 DAMENG 是库名(dminit参数DB_NAME的缺省值);SYSDBA 的默认模式是 SYSDBA,不是 DAMENG。- 连接串里分号分隔,不要漏掉结尾分号。
- 测试连接:
disql SYSDBA/<达梦默认口令>@127.0.0.1:5236
6.3 中文乱码
- 达梦缺省字符集是 GB18030(
CHARSET=0),MySQL 默认 utf8mb4。 - 建库时建议指定 UTF-8:
dminit CHARSET=1(官方定义:0=GB18030,1=UTF-8,2=EUC-KR)。 - 连接串加
Charset=UTF8(ODBC 选项)或db.setOption设置。 - Qt 界面统一使用 UTF-8 源码(
QString::fromUtf8或代码文件存 UTF-8)。
6.4 表名/列名大小写
- 达梦
CASE_SENSITIVE缺省为 Y(大小写敏感):未加引号的标识符按规则转大写存储,加了引号则严格按引号内大小写匹配。 - 用双引号
"user_table"可保留小写:CREATE TABLE "user_table"(...)。 - QSqlTableModel 的
setTable("DEVICE_INFO")通常写大写更稳妥。
6.5 数据不刷新
- 原因 A:写库用的连接与 model 用的连接不是同一个事务上下文 → 确保提交后再
select()。 - 原因 B:
setEditStrategy(OnManualSubmit)忘了调submitAll()。 - 原因 C:写了库但没调
model->select()。 - 原因 D:用了事务没 commit,其他连接读不到未提交数据。
6.6 锁等待 / 卡死
- 达梦默认行级锁,与 InnoDB 类似;长事务持锁会导致其他会话等待。
- 界面线程不要执行长时间 SQL,放到工作线程。
- 批量入库用事务,但事务要小(几千行一次),避免锁持有过久。
七、延伸阅读
- ../Qt/Qt_ModelView_MVC架构详解.md — MVC 基础、QModelIndex/Role/Delegate、自定义 Model、代理与拖拽
- ../Qt/Qt网络编程-01-TCP客户端与服务端.md — 报文传输的 TCP 实现基础
- 达梦学习路线.md — 本系列总览
- 后续章节:02 连接与驱动、04 IEC104 规约报文实战
豫公网安备41108202000982号