Skip to Content

Backend danych

IronFlock tworzy prywatną bazę danych dla każdego projektu, obsługiwaną przez TimescaleDB. Twoja aplikacja definiuje schemat danych; IronFlock tworzy tabele i zaczyna zbierać dane w momencie gdy urządzenie jest dodawane do aplikacji.

Jak to działa

  1. Zdefiniuj swój schemat danych w .ironflock/data-template.yml.
  2. Użyj SDK IronFlock do publikowania danych z kodu brzegowego.
  3. IronFlock automatycznie konfiguruje tabele bazy danych w każdym projekcie gdzie aplikacja jest zainstalowana.
  4. Dane przepływają z urządzeń przez system komunikatów do bazy danych projektu.

Każdy projekt otrzymuje własną fizyczną bazę danych — nie ma współdzielenia danych między projektami.

Użytkownik ma pełną kontrolę nad danymi zbieranymi przez twoją aplikację w swoim projekcie. Jako deweloper nie masz dostępu do tych danych.

Definiowanie schematu danych

Utwórz plik data-template.yml w katalogu .ironflock/:

data: tables: - tablename: sensordata columns: - id: tsp name: Timestamp description: Czas pomiaru path: args[0].timestamp dataType: timestamp - id: temperature name: Temperature description: Odczyt temperatury w Celsjuszach path: args[0].temperature dataType: numeric - id: humidity name: Humidity description: Procentowa wilgotność względna path: args[0].humidity dataType: numeric - id: device_id name: Device ID description: Identyfikator urządzenia źródłowego path: args[0].device_id dataType: string

Opcje kolumn

PoleOpis
idWewnętrzny identyfikator kolumny (użyj tsp dla kolumn znacznika czasu)
nameCzytelna dla człowieka nazwa kolumny wyświetlana w panelach
descriptionOpcjonalny opis
pathŚcieżka do wartości w opublikowanym obiekcie danych (np. args[0].temperature)
dataTypeJeden z: timestamp, numeric, string, boolean
secretSzyfruje tę kolumnę w spoczynku i nigdy nie zwraca jej w postaci jawnej przy zwykłym odczycie — zobacz Tajne kolumny poniżej

Tajne kolumny

Niektóre wartości trzeba przechowywać, ale nigdy nie wolno ich pokazywać: token API, hasło urządzenia, klucz licencyjny. Oznacz kolumnę jako secret: true, a zaplecze danych zaszyfruje ją przy wstawianiu:

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

Od tego momentu żaden zwykły odczyt nie zwraca wartości jawnej — nawet aplikacji, która ją zapisała:

Ścieżka odczytuCo otrzymujesz w zamian
Panele, widżety i wszystko inne przechodzące przez router komunikatówsymbol zastępczy __secret__
Dostęp SQL (login Postgres w FleetDB Access)przechowywany szyfrogram, ifsec:1:…
Funkcja ujawniająca z SDKodszyfrowana wartość

NULL pozostaje NULL na każdej ścieżce, więc „ustawione, ale ukryte” nadal daje się odróżnić od „nigdy nie zapisane”.

Odczytanie tajnej wartości w postaci jawnej jest możliwe wyłącznie z kontenerów samej aplikacji, poprzez SDK — zobacz Tajne kolumny w dokumentacji SDK. Panele ani inne aplikacje nie są w stanie jej uzyskać w żaden sposób, i to właśnie sprawia, że „nigdy nie wyświetlane w postaci jawnej” jest prawdą, a nie jedynie zabiegiem kosmetycznym.

Aktualizacja wiersza bez utraty jego sekretu. W tabeli encji klient edytujący wiersz nigdy nie może odesłać prawdziwego sekretu — odczyty zawsze dawały mu jedynie symbol zastępczy. Zaplecze danych traktuje więc przy zapisie symbol zastępczy __secret__ (oraz maskę •••••••• wyświetlaną przez panele) jako zachowaj poprzednią wartość; pominięta tajna kolumna również ją zachowuje. Wysłanie jawnego null czyści sekret. Edycja opisu maszyny w formularzu na panelu nigdy więc nie wymazuje jej kodu dostępu.

Wynikają z tego dwie konsekwencje: dosłownych łańcuchów __secret__ i •••••••• nie da się samych w sobie zapisać jako tajnych wartości (a surowy szyfrogram ifsec:… jest odrzucany jako dane wejściowe), zaś w tabeli bez klucza encji zapis symbolu zastępczego jest odrzucany — nie istnieje poprzedni wiersz, który można by zachować.

Warto zawczasu zaplanować trzy konsekwencje, ponieważ są to twarde ograniczenia, a nie wskazówki:

  • Tajne kolumny mogą być wyłącznie łańcuchami znaków. Szyfrowanie daje w wyniku tekst, więc kolumny numeric, boolean i timestamp nie mogą być tajne. Obowiązkowa kolumna tsp również nie może być tajna.
  • Po tajnej kolumnie nie da się filtrować, grupować ani sortować. Każdy wiersz jest szyfrowany pod własną losową wartością, więc dwa wiersze przechowujące ten sam sekret zapisują różne szyfrogramy. Filtry równości, GROUP BY, ORDER BY oraz DISTINCT na tajnej kolumnie po prostu nie mogą działać. Aby odpowiedzieć na pytanie „czy ta wartość się zgadza?”, użyj funkcji weryfikującej z SDK, która porównuje wewnątrz zaplecza danych zamiast cokolwiek zwracać.
  • Tajna kolumna nie może być kluczem encji. Ponieważ szyfrogram każdego wiersza jest inny, odczyty zwracające najnowszy wiersz dla każdej encji traktowałyby każdy wiersz jako osobną encję — dlatego wskazanie takiej kolumny w maintainLatestFlagFor jest wprost odrzucane.

Dwa ostatnie ograniczenia są sprawdzane podczas walidacji twojego szablonu danych, więc tabela, która je narusza, kończy się niepowodzeniem przy wydaniu, zamiast działać wadliwie później.

Opcje tabel

Poza columns tabela przyjmuje kilka opcjonalnych kluczy, które określają, jak jest opisana i jak starzeją się jej dane:

data: tables: - tablename: sensordata description: Odczyty środowiskowe z hali produkcyjnej chunkTimeInterval: 1 hour dropAfter: 30 days columns: # ...
PoleOpis
tablenameNazwa tabeli
descriptionOpcjonalny opis, wyświetlany w interfejsie i wykorzystywany przez agentów AI do zrozumienia tabeli
chunkTimeIntervalRozmiar partycji czasowych, na które dzielona jest tabela. Domyślnie 7 days
dropAfterOkno retencji — starsze partycje są automatycznie usuwane
downsampleUtrzymuje wstępnie zagregowaną kopię na potrzeby szybkich wykresów z długim oknem czasowym — zobacz Ciągły downsampling poniżej
maintainLatestFlagForKolumny identyfikujące unikalną encję — zobacz Śledzenie aktualnego stanu encji poniżej
privateUkrywa tę tabelę przed innymi aplikacjami — zobacz Udostępnianie danych innym aplikacjom poniżej

chunkTimeInterval decyduje o tym, jak dane szeregów czasowych są partycjonowane na dysku. Dobierz go tak, aby jedna partycja odpowiadała mniej więcej temu, co odczytujesz w jednym zapytaniu: dane o wysokiej częstotliwości zbierane co sekundę zyskują na małych chunkach (minuty do godzin), a dane zmieniające się powoli — na dużych (tygodnie). To jedynie wartość domyślna aplikacji — właściciel projektu może ją później zmienić we własnym data backendzie.

dropAfter zamienia tabelę w przesuwane okno. Usuwane są całe partycje starsze niż podany interwał, co jest znacznie tańsze niż kasowanie pojedynczych wierszy. Zadanie czyszczące działa w cyklu dropAfter / 4, więc rekord może przetrwać swój termin ważności nawet o jedną czwartą interwału, zanim jego partycja zostanie usunięta. Pomiń dropAfter, aby przechowywać dane bezterminowo.

Oba przyjmują łańcuchy interwałów PostgreSQL — 30 minutes, 1 hour, 7 days, 6 months.

Ciągły downsampling

Panele mogą poprosić bazę danych o agregację danych — średnie godzinowe, sumy dzienne, liczbę rekordów na maszynę. Obliczanie ich z surowych rekordów jest w porządku dla jednej doby, ale kosztowne dla roku. Dodaj downsample do tabeli, a platforma będzie utrzymywać jej stale aktualizowaną, wstępnie zagregowaną kopię i odpowiadać z niej na zapytania z długim oknem czasowym:

data: tables: - tablename: sensordata dropAfter: 30 days downsample: bucket: 1 minute keepFor: 2 years paths: - payload.temperature columns: # ...
PoleOpis
bucketZiarnistość wstępnie zagregowanej kopii. Domyślnie 1 minute
keepForJak długo przechowywać historię po downsamplingu. Pomiń, aby przechowywać ją bezterminowo
pathsŚcieżki pól JSON do uwzględnienia, w tej samej notacji, której używają panele

bucket to najdrobniejsza rozdzielczość, z jakiej może zostać obsłużony wykres — wykres proszący o przedziały znacznie drobniejsze niż ta wartość odczyta zamiast tego tabelę surowych danych. Przyjmuje interwały o stałej szerokości od 1 second do 1 day, które dzielą dobę bez reszty (1 minute, 5 minutes, 1 hour). Domyślna wartość 1 minute odpowiada praktycznie każdemu panelowi; grubszy przedział kosztuje mniej miejsca na dysku i mniejszą przepustowość zapisu.

keepFor jest tym, co w ogóle umożliwia długie historie. Surowe rekordy znikają wraz z dropAfter, ale kopia po downsamplingu ma własną retencję: przechowuj surowe dane przez 30 dni, a dane po downsamplingu przez 2 lata, a panel nadal narysuje dwa lata średnich godzinowych, zajmując ułamek miejsca. Ustaw ją na dłuższą niż dropAfter — sytuację odwrotną platforma odrzuca jako błędną konfigurację.

paths rozszerza downsampling na wartości wewnątrz kolumn JSON. Kolumny liczbowe są uwzględniane automatycznie; pola JSON trzeba wskazać wprost, ponieważ kolumna JSON nie ma stałego zestawu kluczy. Niezadeklarowane pola nadal działają w panelach — są po prostu obliczane z tabeli surowych danych.

Cała reszta dzieje się automatycznie. Dla każdej kolumny liczbowej utrzymywane są statystyki (średnia, suma, minimum, maksimum, pierwsza i ostatnia wartość oraz liczba rekordów), pogrupowane według klucza encji tabeli (maintainLatestFlagFor albo publikujące urządzenie). Panele nie wymagają żadnej konfiguracji ani nawet wiedzy o tym mechanizmie: widżet odpytuje bazę jak zwykle, a platforma decyduje przy każdym zapytaniu, czy wstępnie zagregowana kopia jest w stanie na nie odpowiedzieć — przezroczyście wracając do tabeli surowych danych, gdy nie jest, na przykład gdy filtr odwołuje się do kolumny, według której kopia nie grupuje.

Zmiany schematu przebudowują kopię. Dodanie, usunięcie lub zmiana typu kolumny w tabeli objętej downsamplingiem — albo edycja samego bloku downsample — powoduje przebudowanie wstępnie zagregowanej kopii z tabeli surowych danych. Wszystkiego, co jest starsze niż dropAfter, nie da się odtworzyć i zostaje bezpowrotnie utracone. Tam, gdzie to możliwe, skonfiguruj ten blok razem z tabelą, a późniejsze zmiany schematu w długo żyjących tabelach traktuj jako świadomą decyzję.

Publikowanie danych z kodu brzegowego

Użyj SDK IronFlock, aby wysyłać dane z twojej aplikacji:

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" })

W przypadku danych o wysokiej częstotliwości wysyłaj wiele wierszy w jednym komunikacie zamiast jednego cyklu na wiersz, używając publish_rows_to_table / publishRowsToTable (wyślij i zapomnij) lub append_rows_to_table / appendRowsToTable (zwraca wynik wstawienia). Każda partia jest wstawiana atomowo — wszystko albo nic. Szczegóły znajdziesz w dokumentacji SDK.

Tabele transformacyjne

Możesz definiować transformacje SQL, które automatycznie agregują lub przetwarzają surowe dane:

data: tables: - tablename: sensordata columns: # ... kolumny surowych danych ... 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
PoleOpis
tablenameNazwa tabeli pochodnej
materializeJeśli true, wyniki są utrwalane jako tabela
scheduleWyrażenie cron określające kiedy uruchamiać transformację
sqlZapytanie SQL obliczające transformację
columnsDefinicje kolumn dla danych wyjściowych

Tabele transformacyjne są dostępne w panelach i przez SDK, tak jak zwykłe tabele.

Transformacja jest odczytywana dokładnie tak, jak zwraca ją jej własny SQL: okna czasowe widżetów, filtry kalendarza i tryb latest dotyczą tabel i dla transformacji są ignorowane. Wynikają z tego trzy zasady. Ogranicz zakres czasu wewnątrz zapytania (WHERE tsp > now() - interval '7 days'), sortuj szereg czasowy od najnowszych (ORDER BY <kolumna czasu> DESC), aby limit wierszy zachowywał najnowsze wiersze, oraz wybierz każdą kolumnę, którą panel ma wykreślać lub według której ma filtrować — transformacja nie ma domyślnych kolumn ze znacznikiem czasu ani z urządzeniem. Pojedynczy odczyt zwraca najwyżej 3000 wierszy, więc agreguj w zapytaniu.

Dostarczanie transformacji wraz z aplikacją nie jest jedynym sposobem, aby ją mieć: członek projektu z uprawnieniem Dostęp do danych może zapisać taką samą transformację z widoku danych projektu, bez żadnej aplikacji, a asystent AI również. Zobacz Niestandardowe transformacje.

Śledzenie aktualnego stanu encji

Dla tabel reprezentujących aktualny stan rzeczywistych encji — maszyn, zasobów, zleceń produkcyjnych — IronFlock obsługuje wzorzec zwany śledzeniem najnowszego stanu.

Zamiast nadpisywania wiersza gdy coś się zmienia, zawsze dołączasz nowy wiersz. Deklarujesz, które kolumny identyfikują unikalną encję, a IronFlock wyznacza najnowszy wiersz dla każdej encji przy każdym odczycie tabeli. Daje to pełną historię każdej zmiany, jednocześnie ułatwiając zapytanie tylko o aktualny stan.

Włącz to na tabeli za pomocą 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 przyjmuje listę kolumn, które razem identyfikują unikalną encję. Do samego wiersza nie jest przy tym nic zapisywane: IronFlock indeksuje tabelę według tego klucza encji oraz znacznika czasu i przy wykonywaniu zapytania wybiera najnowszy wiersz dla każdej encji. Wiersz, który przyjdzie z opóźnieniem lub poza kolejnością, nigdy nie może więc pozostawić po sobie nieaktualnego oznaczenia.

Aktualizowanie encji częściowym wierszem

Wiersz dołączony do tabeli encji to nowa wersja tej encji — i nie musi być kompletny. Kolumny, których wiersz nie dostarcza, są dziedziczone z poprzedniego najnowszego wiersza encji, więc aktualizacja pojedynczego pola sprowadza się do opublikowania samego klucza encji, znacznika czasu i tego jednego pola:

Kolumna nowego wiersza jestZapisana wersja zawiera
podana (0, false i "" liczą się jako podane)podaną wartość
jawnie ustawiona na nullNULL — kolumna zostaje wyczyszczona
nieobecnawartość z poprzedniego najnowszego wiersza

Warto znać dwa szczegóły:

  • Wiersz, który dostarcza wszystkie kolumny, całkowicie pomija wyszukiwanie poprzedniego wiersza, więc kompletne wiersze pozostają dokładnie tak tanie, jak były zawsze — na ścieżkach o wysokiej częstotliwości nadal wysyłaj kompletne wiersze.
  • Dziedziczenie respektuje czas: wiersz, który przychodzi ze starszym znacznikiem czasu (uzupełnienie danych wstecz), dziedziczy wyłącznie z wierszy o tsp równym lub wcześniejszym niż jego własny, nigdy z nowszych.

Tabele bez maintainLatestFlagFor zachowują zwykłą semantykę dołączania: kolumna, której wiersz nie dostarcza, jest zapisywana jako NULL.

Aby zapytać tylko o aktualne stany maszyn:

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

Aby wyświetlić pełną historię konkretnej maszyny:

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

Rzadko piszesz to zapytanie ręcznie. Widgety na panelu połączone z tą tabelą mają w ustawieniach filtrów przełącznik latest, dzięki czemu użytkownicy zawsze widzą aktualne wartości bez dodatkowej pracy. Z poziomu SDK zażądasz tego samego trybu, dodając {"latest": true} do filterAnd — zobacz getHistory.

Migracja z latest_flag: wcześniejsze wersje IronFlock przechowywały fizyczną kolumnę boolean o nazwie latest_flag. Ta kolumna już nie istnieje — aktualny stan jest zamiast tego wyznaczany w SQL, co utrzymuje jego poprawność, gdy wiersze przychodzą poza kolejnością. Istniejące panele i wywołania SDK, które filtrują po latest_flag = true, działają nadal: IronFlock je rozpoznaje i stosuje tryb najnowszego stanu. Nowy kod powinien używać przełącznika latest lub wpisu filtra {"latest": true}.

Miękkie usuwanie rekordów

Model tylko-do-dołączania IronFlock oznacza, że rekordy nigdy nie są fizycznie usuwane. Zamiast tego użyj kolumny boolean deleted, aby oznaczyć rekord jako usunięty. Zachowuje to pełny ślad audytu, jednocześnie ukrywając usunięte rekordy przed panelami.

Dodaj kolumnę deleted do dowolnej tabeli encji:

- id: deleted name: Deleted dataType: boolean

Gdy użytkownik usuwa rekord (na przykład przez formularz na panelu), twoja aplikacja publikuje nowy wiersz dla tej encji z deleted: true. W połączeniu z maintainLatestFlagFor ten nowy wiersz staje się najnowszym stanem. Późniejsze częściowe wiersze dziedziczą znacznik deleted jak każdą inną kolumnę, więc aktualizacja, która nie wspomina o deleted, ani nie wskrzesza encji, ani jej nie ukrywa.

Aby zapytać tylko o aktywne (nie usunięte) aktualne rekordy:

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

Sprawdzenie kolumny deleted jest wykonywane po wybraniu najnowszego wiersza dla każdej maszyny. Ta kolejność ma znaczenie: odfiltrowanie usuniętych wierszy w pierwszej kolejności sprawiłoby, że poprzedni, nieusunięty wiersz pojawiłby się ponownie jako aktualny stan.

Widgety na panelach oraz SDK stosują tę samą kolejność automatycznie — połącz przełącznik latest (lub {"latest": true}) z filtrem na kolumnie deleted, a otrzymasz dokładnie takie zachowanie. Usunięte rekordy znikają z panelu natychmiast po wysłaniu formularza, ale pozostają w bazie danych na potrzeby historii i audytu.

Udostępnianie danych innym aplikacjom

Twój data backend należy wyłącznie do Twojej aplikacji: żadna inna aplikacja zainstalowana w projekcie nie widzi Twoich tabel. Zmieniają to dwa opcjonalne klucze w data-template.yml.

Aby czytać dane innej aplikacji, wypisz aplikacje, z których chcesz czytać, w sekcji consumes: na najwyższym poziomie — obok data:, a nie w jej wnętrzu:

consumes: - app: machine-monitor reason: "Oblicza OEE na podstawie strumieni stanu maszyn i liczników z monitora" data: tables: - tablename: oee_results columns: # ... własne tabele Twojej aplikacji, jak zwykle

app to techniczna nazwa aplikacji udostępniającej albo "*" (cudzysłowy są wymagane) dla wszystkich aplikacji w projekcie. reason jest pokazywany użytkownikowi w oknie zgody — sama deklaracja niczego nie przyznaje, dopóki użytkownik jej nie zatwierdzi.

Aby zachować wybrane tabele dla siebie, oznacz je private: true. Wszystko, co zdefiniujesz, jest domyślnie udostępnialne; prywatna tabela lub transformacja w ogóle nie pojawia się w katalogu widzianym przez inne aplikacje.

data: tables: - tablename: measurements # udostępniana (domyślnie) columns: [ ... ] - tablename: calibration_state # wewnętrzna — nigdy niewidoczna dla innych aplikacji private: true columns: [ ... ]

Dostęp jest wyłącznie do odczytu, przyznawany przez użytkownika w obrębie projektu i w każdej chwili odwoływalny. Pełny model oraz wywołania SDK odczytujące historię i strumienie na żywo aplikacji udostępniającej opisano w Korzystanie z danych innych aplikacji.

Last updated on