电力设备运维场景:变电站设备经 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 连接达梦

对比项达梦 DM8MySQL
Qt 驱动无官方原生驱动,走 ODBC(QODBC)官方 QMYSQL 驱动
端口52363306
驱动来源达梦安装目录自带 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 对比

对比项QSqlQueryModelQSqlTableModelQSqlRelationalTableModel
只读/读写只读读写读写
表范围任意 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();   // 关键:重新查库,界面自动刷新
}

关键点:

  1. model->select() 会重新执行 SELECT,最新入库的数据立即出现在表格。
  2. 信号槽跨线程自动排队(Qt::QueuedConnection),数据库写操作不阻塞界面。
  3. 高并发上报时,可在写库线程内做 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 对比表(汇总)

对比项达梦 DM8MySQL
Qt 驱动QODBC(ODBC 桥接)QMYSQL
端口52363306
连接串驱动+服务器+端口+Database(库名,非模式)+用户口令,模式随登录用户定(见 2.3 要点)主机+端口+数据库名
默认管理员SYSDBAroot
大小写默认敏感(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,放到工作线程。
  • 批量入库用事务,但事务要小(几千行一次),避免锁持有过久。

七、延伸阅读