数据库字段映射
SQL 数据库有几十种数据类型, IEC 61131-3 PLC 有十几种, 两边类型系统不完全对齐。本页定义标准映射规则, 以及向导和 FB 在背后做的转换细节。
所有映射示例都是**"从数据库读配方/工单参数进 PLC"** 场景, 例如 SQL VARCHAR → PLC STRING (配方名), SQL DECIMAL → PLC REAL (配方参数), SQL INT → PLC DINT (工单号), SQL DATETIME → PLC DT (班次开始时间)。 读场景为主, 写场景仅在班次结束汇总等低频时使用。
映射总表
整数
| SQL 类型 | 位宽 | 范围 | PLC 类型 | 备注 |
|---|---|---|---|---|
| TINYINT | 8 | 0~255 | USINT | 无符号 |
| TINYINT (有符号) | 8 | -128~127 | SINT | |
| SMALLINT | 16 | -32768~32767 | INT | |
| SMALLINT UNSIGNED | 16 | 0~65535 | UINT | |
| INT / INTEGER | 32 | ±2G | DINT | |
| INT UNSIGNED | 32 | 0~4G | UDINT | |
| BIGINT | 64 | ±9E18 | LINT | |
| BIGINT UNSIGNED | 64 | 0~1.8E19 | ULINT |
浮点与定点
| SQL 类型 | 位宽 | 精度 | PLC 类型 | 警告 |
|---|---|---|---|---|
| REAL / FLOAT | 32 | ~7 位十进制 | REAL | - |
| DOUBLE / FLOAT(p>24) | 64 | ~15 位 | LREAL | - |
| DECIMAL(p,s) p≤7 | 变长 | 精确 | REAL | 近似, 可能有 0.1 型舍入 |
| DECIMAL(p,s) p≤15 | 变长 | 精确 | LREAL | 精度损失较小 |
| DECIMAL(p,s) p>15 | 变长 | 精确 | STRING + 解析 | PLC 无对应精度 |
| NUMERIC | 同 DECIMAL | 同 DECIMAL |
配方里的"必须精确"字段 (例如灌装量 µL 级, 配料重量 mg 级), 强烈建议在 PLC 侧用 DINT 存放最小粒度整数 (而不是 REAL 存小数), 避免浮点舍入累计误差。
字符串
| SQL 类型 | 编码 | 长度 | PLC 类型 | 备注 |
|---|---|---|---|---|
| CHAR(N) | 定长 | ≤255 | STRING[N] | 尾部空格需要 TRIM |
| VARCHAR(N) | 变长 | ≤65535 | STRING[N] | N ≤ 255 最佳 |
| TEXT / CLOB | 变长 | 极大 | 不直接映射 | 仅存路径或 Hash |
| NCHAR / NVARCHAR | Unicode | WSTRING[N] | 如 SQL Server 默认 | |
| NTEXT | Unicode 大 | 不直接映射 | 同 TEXT | |
| JSON (MySQL/PG) | 文本 | STRING + 解析 | 手工解析 | |
| XML (SQL Server) | 文本 | STRING + 解析 | 手工解析 | |
| UUID / GUID | 固定 16B | STRING[36] | 转为文本表示 |
时间
| SQL 类型 | 粒度 | PLC 类型 | 编码 |
|---|---|---|---|
| DATE | 日 | DATE | 年月日 |
| TIME | 时分秒 [. 毫秒] | TIME_OF_DAY (TOD) | 毫秒 (可选) |
| DATETIME / TIMESTAMP | 秒 or 毫秒 | DATE_AND_TIME (DT) | YMD + HMS[.ms] |
| DATETIME2 (SQL Server) | 纳秒 | DT (毫秒级) | 精度损失 |
| SMALLDATETIME | 分 | DT | 精度上升 |
| TIMESTAMP WITH TIMEZONE | 带时区 | DT (UTC) | 建议 UTC 存储 |
| INTERVAL | 时长 | TIME | 毫秒 |
数据库时间一般建议 UTC 存储, PLC 侧显示时再转本地时区。避免 "DT 写入时 +8 读出时 +0" 这种偏移。
布尔
| SQL 类型 | PLC 类型 | 备注 |
|---|---|---|
| BIT | BOOL | SQL Server |
| BOOLEAN / BOOL | BOOL | PostgreSQL / MySQL |
| TINYINT (0/1 约定) | BOOL | 老库常用 |
| CHAR(1) ('Y'/'N') | BOOL + 转换 | 需在 FB 里 = 'Y' |
二进制
| SQL 类型 | PLC 类型 | 备注 |
|---|---|---|
| BINARY(N) | ARRAY[0..N-1] OF BYTE | 定长 |
| VARBINARY(N) | ARRAY[0..N-1] OF BYTE + Length | 变长需单独记长度 |
| BLOB / LONGVARBINARY | 不直接映射 | 存文件路径即可 |
| IMAGE (SQL Server 旧) | 不直接映射 | 已弃用 |
| BYTEA (PostgreSQL) | ARRAY OF BYTE |
数组 / 集合
| SQL 类型 | PLC 类型 | 支持数据库 |
|---|---|---|
| ARRAY[] (PG) | ARRAY OF T | PostgreSQL |
| SET (MySQL) | STRING + 解析 | MySQL |
| ENUM (MySQL) | 枚举类型 | MySQL |
地理与特殊
| SQL 类型 | PLC 类型 | 备注 |
|---|---|---|
| GEOMETRY / GEOGRAPHY | 不直接映射 | 建议存 WKT 文本或经纬度分字段 |
| HIERARCHYID (SQL Server) | STRING + 解析 | 特殊 |
| SQL_VARIANT | STRING | 动态类型, 强转文本 |
NULL 处理
SQL 允许任意列为 NULL, PLC 类型大多没有 NULL 概念。处理策略:
策略 1: 默认值替代
| PLC 类型 | NULL → 默认值 |
|---|---|
| BOOL | FALSE |
| 整数 (SINT/INT/DINT/LINT) | 0 |
| 无符号整数 | 0 |
| 浮点 (REAL/LREAL) | 0.0 |
| STRING | '' (空串) |
| DT | 1970-01-01 00:00:00 |
策略 2: 配套 NULL 标志字段
向导可以为允许 NULL 的列生成一个配对的 BOOL 字段:
VAR
Speed : REAL; (* 实际值 *)
Speed_Null : BOOL; (* TRUE 表示原值为 NULL *)
END_VAR
PLC 程序用:
IF NOT Speed_Null THEN
(* 使用 Speed *)
END_IF;
策略 3: Sentinel 值
用不可能出现的值表示 NULL:
| 类型 | Sentinel |
|---|---|
| INT | -32768 |
| DINT | MIN_DINT |
| REAL | NaN (IEEE 754) |
查询时:
SELECT COALESCE(speed, -1) AS speed FROM ...
或在 FB 里:
IF IS_VALID_REAL(Speed) THEN ...
推荐优先用策略 2 (配套 BOOL), 最清晰。
自增 ID
大多数表有自增主键 (INT IDENTITY / SERIAL / AUTO_INCREMENT):
写入时不传 ID
Insert(
SQL := 'INSERT INTO log(ts, qty, ok) VALUES(:ts, :q, :ok)',
Execute := TRUE
);
(* 不 Bind id 字段 *)
数据库自动分配。
读回新 ID
不同引擎的语法:
| 引擎 | 方法 |
|---|---|
| SQL Server | SCOPE_IDENTITY() 或 OUTPUT INSERTED.id |
| MySQL | LAST_INSERT_ID() |
| PostgreSQL | INSERT ... RETURNING id |
| SQLite | last_insert_rowid() |
| Oracle | 用 Sequence + RETURNING |
FB 统一通过 Insert.LastInsertId 返回:
IF Insert.Done THEN
NewId := Insert.LastInsertId;
END_IF;
底层适配器根据引擎自动用对应方法。
UUID 主键
如果用 UUID 主键, 不需要读回:
NewUuid := GENERATE_UUID(); (* 在 PLC 侧生成 *)
Insert.BindString('id', NewUuid);
外键
PLC 通常不主动维护外键, 但需要处理:
读取时 JOIN
SELECT r.id, r.name, g.code
FROM recipes r JOIN recipe_groups g ON r.group_id = g.id
向导把两张表 JOIN 后的视图导入为一个"逻辑表", 生成一个合并 DB:
VAR
RecipeId : DINT;
RecipeName : STRING[64];
GroupCode : STRING[16];
END_VAR
写入时校验
写入审计记录前检查父表 (配方分组) 是否存在:
(* 1. 先查分组是否存在 *)
CheckGroup(Execute := TRUE);
IF CheckGroup.RowCount = 0 THEN
ErrorMsg := '配方分组不存在';
RETURN;
END_IF;
(* 2. 再插审计记录 *)
Insert.BindInt('group_id', CheckGroup.Row[0].Cell[0].AsDInt);
Insert(Execute := TRUE);
或者依赖数据库的外键约束 — 插入失败时 Insert 返回错误码 FOREIGN_KEY_VIOLATION, PLC 程序处理:
IF Insert.Error THEN
IF Insert.ErrorCode = FOREIGN_KEY_VIOLATION THEN
ErrorMsg := '外键约束冲突';
END_IF;
END_IF;
枚举 (ENUM)
MySQL 的 ENUM('DRAFT','APPROVED','RETIRED'), 或 SQL Server 的 CHECK 约束都是"值域限制" (例如配方状态):
策略 1: 字符串
VAR
Status : STRING[32]; (* 'DRAFT' / 'APPROVED' / 'RETIRED' *)
END_VAR
PLC 比较时:
IF Status = 'APPROVED' THEN ...
简单但性能较差 (字符串比较), 且代码易出现 typo。
策略 2: PLC 枚举类型
工程里定义枚举:
TYPE E_RecipeStatus :
(
Draft := 0,
Approved := 1,
Retired := 2
) INT;
END_TYPE
VAR
Status : E_RecipeStatus;
END_VAR
读数据库时转换:
CASE SqlStatusString OF
'DRAFT': Status := E_RecipeStatus.Draft;
'APPROVED': Status := E_RecipeStatus.Approved;
'RETIRED': Status := E_RecipeStatus.Retired;
ELSE
Status := E_RecipeStatus.Draft;
END_CASE
策略 3: 数据库端用整数
-- 新设计
CREATE TABLE recipes (
id INT PRIMARY KEY,
code VARCHAR(32),
status TINYINT -- 0=Draft 1=Approved 2=Retired
);
-- 搭配元表
CREATE TABLE status_meta (
code TINYINT PRIMARY KEY,
name VARCHAR(32)
);
PLC 直接拿整数, 元表只在 UI/报表用。推荐。
字符串编码
SQL 服务器端编码和 PLC 端编码可能不一致:
| 情况 | 后果 | 处理 |
|---|---|---|
| MySQL 默认 latin1, 写中文 | 乱码 | 改表为 utf8mb4 |
| SQL Server VARCHAR 存中文 | 按 Windows 代码页 (GB2312) | 用 NVARCHAR |
| PostgreSQL 默认 UTF-8 | 兼容 | - |
| PLC STRING 默认 UTF-8 | 兼容大多数 | - |
IDE 连接配置里指定 CharSet=utf8mb4 (MySQL) 或 CharSet=UTF-8 (PostgreSQL), 驱动负责转码。
日期时间细节
精度
- PLC 的 DT 精度是毫秒
- SQL DATETIME 精度可能是秒 (传统 MySQL) 或毫秒/纳秒
- 写入 SQL 时会按目标列的精度截断
时区
推荐策略:
- 服务器 UTC, PLC 本地: 写入时
DT_TO_UTC(localDt), 读出时UTC_TO_DT(utcDt) - 全 UTC: 显示时由 UI 层转换
- 全本地: 简单但不跨时区
向导默认策略 2, 可改。
特殊值
'0000-00-00'(MySQL 允许, 其他不允许): 视为 NULL'1970-01-01 00:00:00'(Unix 纪元): 有效但常作 sentinel'9999-12-31'(远未来): 常作 "永不过期" 的 sentinel
JSON 字段
MySQL 5.7+ / PostgreSQL 的 JSON 类型:
SELECT data FROM config WHERE id = 1;
-- data = '{"speed": 100, "mode": "auto"}'
PLC 侧手工解析:
VAR
JsonStr : STRING[512];
JsonObj : JSON_OBJECT;
END_VAR
JsonObj.Parse(JsonStr);
Speed := JsonObj.GetReal('speed');
Mode := JsonObj.GetString('mode');
或者在 SQL 侧提取:
SELECT JSON_EXTRACT(data, '$.speed') AS speed,
JSON_EXTRACT(data, '$.mode') AS mode
FROM config WHERE id = 1;
结果作为独立列, PLC 只拿最终值, 简单得多。
数组类型 (PostgreSQL)
PostgreSQL 允许列是数组:
CREATE TABLE recipes (
id SERIAL PRIMARY KEY,
tags TEXT[]
);
向导把数组映射为 ARRAY[0..N-1] OF T, 但需要指定最大长度:
VAR
Tags : ARRAY[0..9] OF STRING[32];
TagsCount : INT;
END_VAR
驱动按 N 截断或动态分配。
BLOB 处理
大文件 (图片、PDF) 不建议存数据库:
- 性能差 (BLOB 不走索引)
- 备份开销大
- 网络吞吐有限
推荐:
- 文件存 MinIO / 文件服务器
- 数据库只存文件元信息 (路径, 大小, Hash)
CREATE TABLE files (
id INT PRIMARY KEY,
path VARCHAR(512),
size BIGINT,
sha256 CHAR(64)
);
PLC 拿 path, 通过 HTTP 或文件协议下载。
UDT (结构体) 支持
某些数据库 (Oracle, PostgreSQL) 有自定义复合类型。Darra 目前只支持基础类型, 复合类型需拆分为多列。
映射冲突处理
向导遇到无法映射的类型时的行为:
| 冲突 | 向导处理 |
|---|---|
| DECIMAL(20,5) | 警告 + 默认 LREAL + 备注"精度丢失" |
| VARCHAR(10000) | 警告 + 截断到 STRING[255] + 备注 |
| BLOB | 警告 + 不生成 + 建议改表结构 |
| 未知类型 | 跳过 + 记录到导入日志 |
用户可以在字段映射步骤手工调整。
验证与测试
向导生成后建议的验证步骤:
- 插入测试数据: 手工在数据库里插入一行, 验证 FB_Read 能正确读回
- 边界值测试: NULL, 空串, 最大值, 最小值
- 并发测试: 两个 PLC 同时读写, 看是否有竞争条件
- 故障测试: 断开数据库, 看 FB 是否正确进入错误状态并重试