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
dataTypetimestampnumericstringbooleanのいずれか
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のreveal関数復号された値

NULLはどこでもNULLのままなので、「設定済みだが非表示」と「一度も書き込まれていない」は引き続き区別できます。

シークレットを平文で読み戻せるのは、アプリ自身のコンテナからSDK経由で行う場合だけです — SDKリファレンスのシークレットカラムを参照してください。ボードや他のアプリがそれを取得する手段はまったくありません。だからこそ「平文で決して表示されない」が、見せかけではなく事実になります。

シークレットを失わずに行を更新する。エンティティテーブルでは、行を編集するクライアントが本物のシークレットを送り返すことは決してできません — 読み取りで得られたのは常にプレースホルダだけだからです。そのためデータバックエンドは、書き込み時のプレースホルダ__secret__(およびボードが表示するマスク••••••••)を直前の値を維持するという意味に解釈し、シークレットカラムが省略された場合も同様に維持します。明示的にnullを送信するとシークレットはクリアされます。したがって、ボードのフォームで機械の説明を編集しても、そのアクセスコードが消えることはありません。

ここから2つの帰結が導かれます。リテラル文字列__secret__および••••••••自体をシークレット値として保存することはできず(生のifsec:…暗号文も入力として拒否されます)、エンティティキーのないテーブルではプレースホルダの書き込みは拒否されます — 維持すべき直前の行が存在しないためです。

次の点は指針ではなくハードな制約なので、あらかじめ設計に織り込んでおいてください。

  • **シークレットカラムは文字列のみです。**暗号化はテキストを生成するため、numericbooleantimestampのカラムをシークレットにすることはできません。必須のtspカラムもシークレットにはできません。
  • **シークレットカラムでフィルタ、グループ化、ソートを行うことはできません。**各行はそれぞれ独自のランダムな値で暗号化されるため、同じシークレットを保持する2つの行は異なる暗号文を保存します。シークレットカラムに対する等価フィルタ、GROUP BYORDER BYDISTINCTは、原理的に機能しません。「この値は一致するか?」に答えるには、何も返さずにデータバックエンド内部で比較を行うSDKのverify関数を使用してください。
  • **シークレットカラムをエンティティキーにすることはできません。**行ごとに暗号文が異なるため、エンティティごとの最新の行を読み取ると、各行がそれぞれ別のエンティティとして扱われてしまいます。そのため、maintainLatestFlagForでシークレットカラムを指定することは明確に拒否されます。

最後の2つの制約はデータテンプレートの検証時にチェックされます。そのため、これらに違反するテーブルは、後から不正に動作するのではなくリリース時に失敗します。

テーブルオプション

columnsのほかに、テーブルの説明方法とデータの経年管理を制御するオプションキーがいくつかあります。

data: tables: - tablename: sensordata description: 製造現場の環境測定値 chunkTimeInterval: 1 hour dropAfter: 30 days columns: # ...
フィールド説明
tablenameテーブル名
descriptionオプションの説明。UIに表示され、AIエージェントがテーブルを理解するために使用します
chunkTimeIntervalテーブルを分割する時間パーティションのサイズ。デフォルトは7 days
dropAfter保持期間 — これより古いパーティションは自動的に削除されます
downsample長期ウィンドウのチャートを高速化するための事前集計済みコピーを維持します — 後述の継続的ダウンサンプリングを参照
maintainLatestFlagFor一意のエンティティを識別するカラム — 後述のエンティティの最新状態の追跡を参照
privateこのテーブルを他のアプリから隠します — 後述の他のアプリとのデータ共有を参照

**chunkTimeInterval**は、時系列データをディスク上でどう分割するかを決めます。1つのパーティションが一度に問い合わせるデータ量とおおよそ一致するように選んでください。毎秒収集されるような高頻度データは小さなチャンク(分〜時間)が、変化の遅いデータは大きなチャンク(週)が適しています。これはアプリのデフォルト値にすぎず、プロジェクトの所有者は後から自身のデータバックエンドで調整できます。

**dropAfter**は、テーブルをローリングウィンドウに変えます。指定した期間より古いパーティションはまるごと削除されるため、行を1件ずつ削除するよりはるかに低コストです。クリーンアップジョブはdropAfter / 4の間隔で実行されるため、レコードはパーティションが削除されるまで、最大で期間の4分の1だけ有効期限を超えて残ることがあります。データを無期限に保持するにはdropAfterを省略してください。

いずれもPostgreSQLの間隔文字列を取ります — 30 minutes1 hour7 days6 months

継続的ダウンサンプリング

ダッシュボードはデータベースにデータの集計を依頼できます — 1時間ごとの平均、1日ごとの合計、機械ごとの件数など。これを生レコードから計算するのは、1日分であれば問題ありませんが、1年分となると高コストです。テーブルに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日を均等に分割する1 secondから1 dayまでの固定幅の間隔(1 minute5 minutes1 hourなど)を指定できます。デフォルトの1 minuteは実質的にあらゆるダッシュボードに適しています。バケットを粗くすると、ストレージと書き込みスループットのコストが下がります。

**keepFor**は、そもそも長期履歴を可能にするための設定です。生レコードはdropAfterによって消えていきますが、ダウンサンプリング済みのコピーは独自の保持期間を持ちます。生データを30日、ダウンサンプリング済みデータを2年間保持すれば、ボードはごく一部のストレージで2年分の1時間平均をチャート表示できます。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" })

高頻度データの場合は、1 行ごとにラウンドトリップを行う代わりに、publish_rows_to_table / publishRowsToTable(ファイア・アンド・フォーゲット)または append_rows_to_table / appendRowsToTable(挿入結果を返す)を使用して、複数の行を 1 つのメッセージで送信できます。各バッチはアトミックに挿入されます — オール・オア・ナッシングです。詳細については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派生テーブルの名前
materializetrueの場合、結果がテーブルとして永続化される
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はそのエンティティキーとタイムスタンプでテーブルにインデックスを作成し、クエリ時にエンティティごとの最新の行を選び出します。そのため、遅れて到着した行や順序が前後した行が古いマークを残してしまうことはありません。

部分的な行によるエンティティの更新

エンティティテーブルに追加される行は、そのエンティティの新しいバージョンです — そして、行は完全である必要はありません。行に含まれていないカラムは、そのエンティティの直前の最新の行から引き継がれます。そのため、1つのフィールドだけを更新するには、エンティティキー、タイムスタンプ、そのフィールドだけを送信すれば済みます:

新しい行のカラムの状態保存されるバージョンが保持する値
提供されている(0false""も提供済みとみなされます)提供された値
明示的にnullNULL — カラムはクリアされます
存在しない直前の最新の行の値

押さえておきたい詳細が2つあります:

  • すべてのカラムを提供する行は、直前の行の参照を完全にスキップします。そのため、完全な行はこれまでどおり低コストのままです — 高頻度の経路では引き続き完全な行を送信してください。
  • 引き継ぎは時間を尊重します。古いタイムスタンプで到着した行(バックフィル)は、自身の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の2つのオプションキーがこれを変えます。

他のアプリのデータを読み取るには、読み取り元のアプリをトップレベルの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