Skip to Content
IoT 应用开发数据后端

数据后端

IronFlock 为每个项目配置一个由 TimescaleDB 驱动的私有数据库。您的应用定义数据模式;IronFlock 在设备加入应用的那一刻就创建表并开始采集数据。

工作原理

  1. .ironflock/data-template.yml 中定义数据模式。
  2. 使用 IronFlock SDK 从边缘代码发布数据。
  3. IronFlock 在安装该应用的每个项目中自动创建数据库表。
  4. 数据从设备通过消息系统流入项目数据库。

每个项目都有自己的物理数据库——项目之间不共享数据。

用户对其项目中由您的应用采集的数据拥有完全控制权。作为开发者,您无法访问这些数据。

定义数据模式

.ironflock/ 目录中创建 data-template.yml 文件:

data: tables: - tablename: sensordata columns: - id: tsp name: Timestamp description: Timestamp of measurement path: args[0].timestamp dataType: timestamp - id: temperature name: Temperature description: Temperature reading in Celsius path: args[0].temperature dataType: numeric - id: humidity name: Humidity description: Relative humidity percentage path: args[0].humidity dataType: numeric - id: device_id name: Device ID description: Source device identifier path: args[0].device_id dataType: string

列选项

字段描述
id内部列标识符(时间戳列使用 tsp
name人类可读的列名,显示在仪表板中
description可选描述
path发布的数据对象中值的路径(例如 args[0].temperature
dataType可选值:timestampnumericstringboolean
secret将该列加密存储,普通读取永远不会以明文返回——参见下文机密列

机密列

有些值必须存储下来,却绝不能被展示出来:API 令牌、设备密码、许可证密钥。把该列标记为 secret: true,数据后端就会在插入时对它加密:

- tablename: credentials columns: - id: tsp dataType: timestamp - id: device_id dataType: string - id: api_token dataType: string secret: true

从此没有任何普通读取会返回明文——即使是写入它的那个应用也不例外:

读取路径你得到的内容
仪表板、组件,以及其他一切经由消息路由器的读取占位符 __secret__
SQL 访问(FleetDB Access 的 Postgres 登录)存储的密文,ifsec:1:…
SDK 的明文读取函数解密后的值

NULL 在所有路径上都保持为 NULL,因此“已设置但被隐藏”与“从未写入”依然可以区分。

只有从应用自己的容器中、通过 SDK 才可能以明文读回机密值——参见 SDK 参考中的机密列。仪表板和其他应用完全无法获取它,正是这一点让“绝不以明文展示”成为事实,而不只是表面功夫。

**在不丢失机密值的前提下更新行。**在实体表上,编辑某行的客户端永远无法把真正的机密值发回来——读取时它拿到的只有占位符。因此,数据后端在写入时会把占位符 __secret__(以及仪表板显示的掩码 ••••••••)视为保留原有值,省略机密列同样会保留原有值。显式发送 null 则会清除机密值。因此,在仪表板表单中编辑一台机器的描述,绝不会抹掉它的访问码。

由此产生两个后果:字面字符串 __secret__•••••••• 本身无法作为机密值存储(原始的 ifsec:… 密文作为输入也会被拒绝);而在没有实体键的表上,写入占位符会被拒绝——因为不存在可以保留的前一行。

有三点后果值得在设计时就考虑进去,因为它们是硬性限制,而非建议:

  • 机密列只能是字符串。 加密的产物是文本,因此 numericbooleantimestamp 列不能设为机密列。必需的 tsp 列同样不能设为机密列。
  • 你不能按机密列过滤、分组或排序。 每一行都用它自己的随机值加密,因此两行持有相同的机密值时,存储的是不同的密文。对机密列使用等值过滤、GROUP BYORDER BYDISTINCT 根本无法工作。要回答“这个值是否匹配?”,请使用 SDK 的校验函数——它在数据后端内部完成比较,而不返回任何内容。
  • 机密列不能作为实体键。 由于每一行的密文都不相同,按实体取最新行的读取会把每一行都当成一个独立的实体,因此在 maintainLatestFlagFor 中指定机密列会被直接拒绝。

后两条限制会在数据模板通过校验时被检查,因此违反它们的表会在发布时失败,而不是之后才出现异常行为。

表选项

除了 columns 之外,表还接受若干可选键,用于控制表的描述方式以及数据的留存时长:

data: tables: - tablename: sensordata description: 车间的环境测量数据 chunkTimeInterval: 1 hour dropAfter: 30 days columns: # ...
字段描述
tablename表名
description可选描述,显示在界面中,并供 AI 智能体理解该表
chunkTimeInterval表被切分成的时间分区大小。默认为 7 days
dropAfter保留窗口——早于该区间的分区会被自动删除
downsample维护一份预聚合副本,用于快速绘制长窗口图表——参见下文持续降采样
maintainLatestFlagFor标识唯一实体的列——参见下文跟踪实体的最新状态
private对其他应用隐藏该表——参见下文与其他应用共享数据

chunkTimeInterval 决定时序数据在磁盘上的分区方式。请选择让单个分区大致对应一次查询数据量的值:每秒采集的高频数据适合较小的分块(分钟到小时),变化缓慢的数据则适合较大的分块(周)。这只是应用的默认值——项目所有者之后可以在自己的数据后端中调整。

dropAfter 会把表变成一个滚动窗口。早于所给区间的分区会被整块删除,这远比逐行删除便宜。清理任务按 dropAfter / 4 的周期运行,因此一条记录在其分区被移除之前,最多可能超出有效期该区间的四分之一。省略 dropAfter 即可无限期保留数据。

两者都接受 PostgreSQL 的区间字符串——30 minutes1 hour7 days6 months

持续降采样

仪表板可以要求数据库聚合数据——每小时平均值、每天合计、每台机器的计数。用原始记录来计算这些,跨一天没问题,跨一年就代价高昂。为表添加 downsample,平台就会为它维护一份持续更新的预聚合副本,并改用该副本回答长窗口查询:

data: tables: - tablename: sensordata dropAfter: 30 days downsample: bucket: 1 minute keepFor: 2 years paths: - payload.temperature columns: # ...
字段描述
bucket预聚合副本的粒度。默认为 1 minute
keepFor降采样历史的保留时长。省略则无限期保留
paths要纳入的 JSON 字段路径,采用与仪表板相同的表示法

bucket 是图表能够从该副本获取的最细分辨率——若图表请求的时间桶远细于此值,则改为读取原始表。它接受从 1 second1 day 之间能整除一天的固定宽度区间(1 minute5 minutes1 hour)。默认的 1 minute 几乎适用于所有仪表板;更粗的桶占用更少存储、也带来更小的写入吞吐开销。

keepFor 才是让长历史数据成为可能的关键。原始记录会随 dropAfter 消失,而降采样副本有自己的保留期:原始数据保留 30 天、降采样数据保留 2 年,看板便仍可用极小的存储代价绘制两年的每小时平均值。请把它设得比 dropAfter 更长——相反的配置会被平台判定为错误配置并拒绝。

paths 把降采样扩展到 JSON 列内部的值。数值列会自动纳入;而 JSON 字段必须显式指定,因为 JSON 列没有固定的键集合。未声明的字段在仪表板中仍然可用——只是改为从原始表计算。

其余一切都是自动的。每个数值列都会维护其统计量(平均值、总和、最小值、最大值、第一个值、最后一个值以及记录条数),并按表的实体键(maintainLatestFlagFor,或发布数据的设备)分组。仪表板无需任何配置,也无需知晓其存在:小部件照常查询,平台会针对每次查询判断预聚合副本能否作答——不能时便透明地回退到原始表,例如当某个过滤条件引用了副本未按其分组的列时。

**模式变更会重建副本。**为降采样表添加、删除或更改列的类型——或编辑 downsample 块本身——都会从原始表重建预聚合副本。任何早于 dropAfter 的数据都无法重建,将会丢失。请尽可能在建表时就一并配置该块,并把长期存在的表上的后续模式变更视为一项需要慎重权衡的决定。

从边缘代码发布数据

使用 IronFlock SDK 从应用发送数据:

from ironflock import IronFlock flock = IronFlock() flock.publish_to_table("sensordata", { "timestamp": "2025-01-15T10:30:00Z", "temperature": 23.5, "humidity": 62.1, "device_id": "sensor-001" })

对于高频数据,请使用 publish_rows_to_table / publishRowsToTable(即发即忘)或 append_rows_to_table / appendRowsToTable(返回插入结果)在单条消息中发送多行,而非每行进行一次往返通信。每个批次都会原子地插入——全部成功或全部失败。详情请参阅 SDK 参考

转换表

您可以定义 SQL 转换,自动对原始数据进行聚合或处理:

data: tables: - tablename: sensordata columns: # ... 原始数据列 ... transforms: - tablename: hourly_averages materialize: true schedule: "0 * * * *" sql: > SELECT time_bucket('1 hour', tsp) AS hour, avg(temperature) AS avg_temp, avg(humidity) AS avg_humidity FROM sensordata GROUP BY hour columns: - id: hour name: Hour dataType: timestamp - id: avg_temp name: Average Temperature dataType: numeric - id: avg_humidity name: Average Humidity dataType: numeric
字段描述
tablename派生表的名称
materialize若为 true,结果将持久化为表
schedule转换运行的 Cron 表达式
sql计算转换的 SQL 查询
columns输出的列定义

转换表可以在仪表板和 SDK 中访问,与常规表的使用方式完全相同。

跟踪实体的最新状态

对于表示真实世界实体(机器、资产、生产订单)当前状态的表,IronFlock 支持一种名为最新状态跟踪的模式。

当数据发生变化时,您不是覆盖原有行,而是始终追加新行。您声明哪些列用于标识一个唯一实体,IronFlock 会在每次读取该表时推导出每个实体的最新行。这让您既能保留每次变更的完整历史,又能轻松查询当前状态。

通过 maintainLatestFlagFor 在表上启用此功能:

- tablename: machineform maintainLatestFlagFor: ['machinename'] columns: - id: tsp dataType: timestamp - id: machinename dataType: string - id: machinetype dataType: string - id: active dataType: boolean - id: description dataType: string

maintainLatestFlagFor 接受一组列,这些列组合起来标识一个唯一实体。行本身不会被写入任何额外内容:IronFlock 会按该实体键加时间戳为表建立索引,并在查询时选出每个实体的最新行。因此,迟到或乱序到达的行绝不会遗留下过时的标记。

用部分行更新实体

追加到实体表的每一行都是该实体的一个新版本——而且它不必是完整的。行中未提供的列会从该实体此前的最新行继承,因此更新单个字段只需发布实体键、时间戳和该字段本身:

新行中的该列存储的版本中保存的是
已提供(0false"" 均算作已提供)所提供的值
显式为 nullNULL——该列被清空
缺失此前最新行中的值

有两个细节值得了解:

  • 提供了所有列的行会完全跳过对前一行的查找,因此完整行的开销与以往完全相同——在高频路径上请继续发送完整行。
  • 继承遵循时间顺序:带着较早时间戳到达的行(即回填)只会从时间戳等于或早于其自身 tsp 的行继承,绝不会从更新的行继承。

没有 maintainLatestFlagFor 的表保持普通的追加语义:行中未提供的列会被存储为 NULL

仅查询当前机器状态:

SELECT DISTINCT ON (machinename) * FROM machineform ORDER BY machinename, tsp DESC

查看特定机器的完整历史:

SELECT * FROM machineform WHERE machinename = 'Assembly-Line-01' ORDER BY tsp

您很少需要手写这条查询。连接到此表的仪表板组件在其过滤设置中提供 latest 开关,因此用户无需任何额外操作即可始终看到当前值。在 SDK 中,只需向 filterAnd 添加 {"latest": true} 即可请求同样的模式——参见 getHistory

**从 latest_flag 升级:**IronFlock 的早期版本会存储一个名为 latest_flag 的物理布尔列。该列已不再存在——当前状态改为在 SQL 中推导得出,这样即使行乱序到达也能保持正确。现有的仪表板和按 latest_flag = true 过滤的 SDK 调用仍可继续工作:IronFlock 会识别它们并应用最新状态模式。新代码应使用 latest 开关或 {"latest": true} 过滤条目。

软删除记录

IronFlock 的仅追加模型意味着记录永远不会被物理删除。取而代之的是,使用 deleted 布尔列将记录标记为已删除。这在从仪表板中隐藏已删除记录的同时,保留了完整的审计跟踪。

在任何实体表中添加 deleted 列:

- id: deleted name: Deleted dataType: boolean

当用户删除一条记录(例如通过仪表板上的表单),您的应用会为该实体发布一个 deleted: true 的新行。结合 maintainLatestFlagFor,这个新行将成为最新状态。之后的部分行会像继承其他任何列一样继承 deleted 标记,因此未提及 deleted 的更新既不会让该实体复活,也不会把它隐藏起来。

仅查询活跃(未删除)的当前记录:

SELECT * FROM ( SELECT DISTINCT ON (machinename) * FROM machineform ORDER BY machinename, tsp DESC ) latest WHERE deleted IS NULL OR deleted = false

deleted 检查是在选出每台机器的最新行之后才执行的。这个顺序很重要:如果先过滤掉已删除的行,前一行未删除的记录就会重新浮现,成为当前状态。

仪表板组件和 SDK 会自动应用相同的顺序——将 latest 开关(或 {"latest": true})与 deleted 过滤条件结合使用,就能得到完全一致的行为。已删除的记录在表单提交后会立即从面板中消失,但仍保留在数据库中,用于历史记录和审计目的。

与其他应用共享数据

数据后端仅属于你的应用:项目中安装的其他应用都看不到你的表。data-template.yml 中的两个可选键可以改变这一点。

要读取其他应用的数据,请在顶层的 consumes: 部分列出想要读取的应用——它与 data: 平级,而不是嵌在其中:

consumes: - app: machine-monitor reason: "根据该监控应用的机器状态和计数器数据流计算 OEE" data: tables: - tablename: oee_results columns: # ... 照常定义你自己应用的表

app 是提供方应用的技术名称,或用 "*"(引号是必需的)表示项目中的所有应用。reason 会在授权对话框中展示给用户——仅有声明本身不会授予任何权限,必须由用户批准。

要保留某些表不共享,将其标记为 private: true。你定义的内容默认都是可共享的;私有的表或转换根本不会出现在其他应用看到的目录中。

data: tables: - tablename: measurements # 共享(默认) columns: [ ... ] - tablename: calibration_state # 内部使用——其他应用永远不可见 private: true columns: [ ... ]

访问权限是只读的,由用户按项目授予,并可随时撤销。完整模型以及读取提供方应用历史数据和实时数据流的 SDK 调用,请参见消费其他应用的数据

Last updated on