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
Odczyty historii i wiersze w czasie rzeczywistym: to, co otrzymują panele, widżety, SDK i inne aplikacje odczytujące tabelęsymbol zastępczy __secret__
SQL (konsola zapytań SQL, eksporty danych i login Postgres w FleetDB Access) oraz kopie zapasowe bazy danychprzechowywany 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() await flock.start() await flock.publish_to_table("sensordata", { "timestamp": "2025-01-15T10:30:00Z", "temperature": 23.5, "humidity": 62.1, "device_id": "sensor-001" })

Każdy wiersz potrzebuje wartości dla obowiązkowej kolumny tsp: zaplecze danych odrzuca wiersz bez niej. Zapis do tabeli przez SDK kończy się wtedy błędem; wiersz opublikowany przez panel zostaje porzucony, zostawiając jedynie wpis w tabeli error-logs aplikacji. Powyższa definicja sensordata mapuje tę kolumnę przez path: args[0].timestamp, więc tsp pochodzi z pola timestamp wiersza. Kolumna bez path odczytuje pole wiersza o tej samej nazwie (args[0].<id>): bez takiego mapowania każdy wiersz potrzebowałby własnego pola tsp.

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 / PublishRowsToTable lub append_rows_to_table / appendRowsToTable / AppendRowsToTable — obie metody działają tak samo i zwracają 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, wynik jest przechowywany i odświeżany zgodnie z harmonogramem. Jeśli false, transformacja jest widokiem, obliczanym przy każdym odczycie
scheduleWyrażenie cron określające uruchomienia transformacji: pięć pól (minuta, godzina, dzień miesiąca, miesiąc, dzień tygodnia) lub sześć z polem sekund na początku. */5 * * * * uruchamia ją co 5 minut, */10 * * * * * co 10 sekund. Każde uruchomienie odświeża zmaterializowany wynik i wysyła aktualny wynik do paneli powiązanych z transformacją. Kod aplikacji może też uruchomić taki przebieg na żądanie, zobacz Odświeżanie na żądanie
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.

Transformacja służy do tego, czego nie da się wyrazić odczytem tabeli: do złączenia, ilorazu, agregacji po aktualnych wierszach kilku tabel. Nie jest sposobem na pokazanie aktualnego stanu encji ani na ukrycie usuniętych rekordów. To są tryby odczytu samej tabeli, przełącznik latest i filtr na kolumnie deleted (zobacz niżej), i działają one na żywo: widget powiązany z tabelą śledzi każdy nowy wiersz w ciągu sekundy. Transformacja dociera do panelu tylko zgodnie ze swoim harmonogramem (albo gdy kod aplikacji odświeża ją po własnych zapisach, czego zapis z panelu nigdy nie wyzwala), więc widok, który wybiera najnowszy wiersz dla każdej encji lub odrzuca usunięte wiersze, pokazuje ten sam wynik z opóźnieniem. Gdy transformacja naprawdę potrzebuje aktualnego stanu tabeli encji, na przykład aby złączyć ją z inną tabelą, wybierz go w jej SQL za pomocą DISTINCT ON i tam zastosuj sprawdzenie deleted, w kolejności pokazanej w sekcji Miękkie usuwanie rekordów.

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.

Kiedy odświeża się zmaterializowana transformacja

Zaplanowane uruchomienie odświeża zmaterializowaną transformację tylko wtedy, gdy do którejś z czytanych przez nią tabel coś zapisano od jej ostatniego odświeżenia, a niezależnie od tego co najmniej co 10 minut. Uruchomienie, które nie ma nic nowego do odczytania, pozostawia przechowywany wynik bez zmian: transformacja zaplanowana co minutę nad tabelą, do której trafia jeden wiersz na godzinę, odświeża się po nadejściu tego wiersza, a poza tym co 10 minut.

Zmaterializowana transformacja odświeża się przy każdym uruchomieniu, gdy jej wynik może się zmienić bez żadnego zapisu:

  • jej SQL używa bieżącego czasu lub losowości, na przykład now(), CURRENT_DATE lub random();
  • wywołuje funkcję, która dla tych samych danych wejściowych może zwrócić inny wynik: funkcję niestabilną (volatile) albo funkcję stabilną (stable), która nie jest wbudowana w PostgreSQL;
  • czyta coś innego niż tabele aplikacji i zmaterializowane transformacje, na przykład niezmaterializowaną transformację, kopię po downsamplingu albo jedną z własnych tabel platformy.

Gdy wiele transformacji ma zaplanowane uruchomienie w tym samym momencie, platforma odświeża po kilka naraz, a pozostałe czekają na swoją kolej, więc odświeżenie może zakończyć się później, niż zaplanowano. Uruchomienie przypadające w czasie, gdy poprzednie odświeżenie tej samej transformacji jeszcze trwa lub czeka, jest pomijane.

Odświeżanie na żądanie

Własny kod aplikacji może uruchomić przebieg transformacji, nie czekając na jej harmonogram: refresh_transform / refreshTransform / RefreshTransform z SDK odświeża zmaterializowany wynik (zwykły widok jest tylko odczytywany) i wysyła aktualny wynik do paneli powiązanych z transformacją, więc panel od razu pokazuje partię, którą aplikacja właśnie zapisała. Obowiązują te same zasady, co przy zaplanowanym uruchomieniu: odświeżenie jest pomijane — i zgłaszane jako pominięte — gdy do żadnej z czytanych przez transformację tabel nic nie zapisano od poprzedniego, wywołanie, które nadchodzi w czasie, gdy trwa odświeżenie tej samej transformacji, czeka na to jedno odświeżenie ustawione w kolejce za nim, a na transformację rozpoczyna się najwyżej jedno odświeżenie na sekundę. Zapisy SDK do tabel są zatwierdzane (commit), zanim wywołanie wróci, więc odświeżenie wykonane po zapisie nigdy nie pomija tego zapisu. Odświeżać transformacje aplikacji mogą wyłącznie jej własne kontenery; panel otrzymuje wiersze, a aplikacja korzystająca z danych innej aplikacji odczytuje jej transformacje, ale nie może ich odświeżać. Transformacje zapisane z widoku danych projektu nie należą do żadnej aplikacji i uruchamiają się wyłącznie zgodnie ze swoim harmonogramem. Zobacz refreshTransform / refresh_transform.

Ś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.

Tabela encji przechowuje jeden wiersz na encję i znacznik czasu. Wiersz, który powtarza zarówno klucz encji, jak i tsp zapisanego już wiersza, jest odrzucany, podobnie jak partia, w której robią to dwa wiersze: zapis przez SDK kończy się błędem URI_ENTITY_KEY_CONFLICT, a tabela error-logs aplikacji odnotowuje odrzucenie. Nadaj każdej wersji encji własny tsp. Wiersz, którego kolumna klucza encji jest pusta (NULL), nie należy do żadnej encji: nigdy nie jest odrzucany jako powtórzenie, a odczyty najnowszego stanu go pomijają.

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

Aby wyczyścić kolumnę, wyślij ją jako null (None w Pythonie, nil w Go). Pominięcie jej zachowuje poprzednią wartość. Uważaj na kod, który pomija pole niezamierzenie: na przykład właściwość JavaScript o wartości undefined wypada z ładunku. Dokumentacja SDK wymienia te przypadki dla każdego języka.

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 i nie powinieneś zapisywać go jako transformacji. 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. Przełącznik działa na żywo: widget odbiera każdy nowy wiersz tabeli, gdy tylko nadejdzie, i zastępuje nim poprzedni wiersz encji, więc zmiana pojawia się w ciągu sekundy od zapisania, a tabela zachowuje każdą wersję. Nic nie jest przeliczane według harmonogramu, a tylko tyle dodałaby transformacja. 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}.

Zmiana klucza encji

Po wydaniu wersji aplikacji klucz encji każdej z jej tabel jest ustalony. Zaktualizuj strukturę danych w panelu bocznym IDE w chmurze oraz build nowego wydania odrzucają data-template.yml, który zmienia klucz encji tabeli zawartej w najnowszym wydaniu aplikacji:

  • dodanie kolumny do maintainLatestFlagFor, usunięcie lub zastąpienie jednej z nich albo zmiana ich kolejności;
  • dodanie maintainLatestFlagFor do tabeli, która go nie miała, lub jego usunięcie.

Taka zmiana po cichu zmieniłaby to, co tabela zwraca w każdym projekcie, w którym zainstalowano aplikację. Kolumna nowa w kluczu jest pusta w każdym istniejącym wierszu, a odczyty najnowszego stanu pomijają wiersze z pustą kolumną klucza, więc wszystkie istniejące encje by zniknęły. Kolumna usunięta z klucza może scalić encje, których wiersze mają ten sam znacznik czasu, a tabela może przechowywać tylko jeden wiersz na encję i znacznik czasu.

Aby użyć innego klucza encji, utwórz nową tabelę z nowym kluczem i przenieś do niej dane, na przykład z kodu swojej aplikacji. Przeniesienie danych należy do aplikacji. Klucz encji tabeli, której nie ma jeszcze w żadnym wydaniu, możesz nadal zmieniać w trakcie prac nad aplikacją. Wersja aplikacji wydana przed wprowadzeniem tej reguły nadal może zmienić klucz encji: projekt, który się do niej aktualizuje, odrzuca aktualizację struktury danych i zachowuje poprzednią.

Wiersze powtarzające encję i znacznik czasu

Tabele zapisane, zanim IronFlock zaczął wymuszać jeden wiersz na encję i znacznik czasu, mogą nadal zawierać powtórzenia; to samo dotyczy tabeli deweloperskiej, której klucz encji zmieniłeś. Gdy usługa danych konfiguruje taką tabelę, przy swoim starcie lub po aktualizacji struktury danych, przenosi każdy wiersz każdej powtórzonej grupy do tabeli o nazwie <table>__entitykey_dupes w tym samym zapleczu danych i przywraca po jednym wierszu z każdej grupy. Od tej pory tabela odrzuca powtórzenia. Wpis w tabeli error-logs aplikacji podaje, ile wierszy przeniesiono.

Nowa tabela zachowuje każdy przeniesiony wiersz bez zmian, z dwiema dodatkowymi kolumnami: quarantined_at, czyli czasem przeniesienia, oraz restored, która oznacza wiersz przywrócony do tabeli. To, który wiersz wraca, jest dowolne, tak jak było już w przypadku odczytów najnowszego stanu. Odpytuj ją w konsoli zapytań w widoku danych projektu jako databackend_<key>.<table>__entitykey_dupes. Jeśli oryginalna tabela jest prywatna, nowa tabela również jest prywatna. Jeśli wierszy nie da się przenieść, wpis w error-logs podaje przyczynę, a usługa danych próbuje ponownie przy następnym starcie.

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.

Na panelu formularz usuwa rekord przez swoje ustawienie Delete-Flag Column, które wskazuje kolumnę deleted: formularz zapisuje true przy usuwaniu i false przy każdym zapisie, więc zapisanie usuniętego rekordu przywraca go (zobacz Usuwanie danych). Widgety, które wyświetlają listę rekordów, filtrują samą tabelę. Żaden widok ani transformacja nie są potrzebne do oczyszczenia listy, a pokazałyby usunięcie dopiero przy swoim następnym zaplanowanym uruchomieniu.

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.

Dostęp uprzywilejowany

Kontenery aplikacji działają w izolacji od urządzenia, z domyślnymi uprawnieniami Dockera. Niektóre aplikacje potrzebują więcej: portu szeregowego lub adaptera USB, GPIO, interfejsu CAN lub Bluetooth, sieci hosta. To jest dostęp uprzywilejowany. Aplikacja z dostępem uprzywilejowanym może kontrolować całe urządzenie, w tym inne aplikacje i ich dane, dlatego Twoja aplikacja musi o niego poprosić, a użytkownik decyduje, czy go otrzyma.

Aby o niego poprosić, dodaj na najwyższym poziomie sekcję privileged: z polem reason — obok data:, a nie w jej wnętrzu:

privileged: reason: "Odczytuje sterownik PLC przez port szeregowy /dev/ttyUSB0" data: tables: # ... własne tabele Twojej aplikacji, jak zwykle

reason jest wymagany. Jest pokazywany użytkownikowi w oknie zgody, więc pisz go dla osoby, która decyduje, czy zaufać Twojej aplikacji.

Aplikacja bez tabel zapisuje data: {} obok privileged: pliku data-template.yml bez klucza data: nie da się opublikować.

To, na co użytkownik się zgadza, zależy od sposobu zbudowania aplikacji:

  • Aplikacja z pojedynczym obrazem (tylko Dockerfile) po udzieleniu zgody działa w trybie uprzywilejowanym: ze wszystkimi capabilities Linuksa, katalogiem /dev urządzenia i D-Bus hosta. Bez deklaracji działa bez urządzeń hosta, bez dodatkowych capabilities i bez D-Bus hosta; wydanie i tak zostaje opublikowane, a jego log budowania o tym ostrzega.
  • Aplikacja Docker Compose otrzymuje dokładnie te ustawienia, o które prosi jej docker-compose.yml, a okno zgody je wymienia. Jeśli plik compose używa któregokolwiek z poniższych ustawień, aplikacja musi zadeklarować privileged; w przeciwnym razie publikacja wydania kończy się błędem, który wskazuje usługę i ustawienie. Zadeklarowanie privileged, gdy plik compose nie używa żadnego z nich, również kończy się błędem.

Te ustawienia compose są uznawane za uprzywilejowane:

UstawienieUprzywilejowane, gdy
privilegedma dowolną wartość oprócz false
cap_add, devices, device_cgroup_rulesma jakikolwiek wpis
security_optma wpis inny niż no-new-privileges
network_mode, pid, ipc, uts, userns_mode, cgroupma wartość host lub container:…
cgroup_parentma jakąkolwiek wartość
volumes usługimontuje ścieżkę hosta (/…, ~/…, C:\…), ścieżkę poza folderem aplikacji (../…) lub nazwany potok. /etc/localtime i /etc/timezone tylko do odczytu są dozwolone.
volumes_fromwskazuje container:…
volumes najwyższego poziomuwolumen ma driver_opts, external lub stałą nazwę name
networks najwyższego poziomusieć ustawia driver inny niż bridge (np. macvlan, ipvlan lub host) albo driver_opts, jest external lub ma stałą nazwę name
secrets, configs najwyższego poziomuwpis czyta plik (file) spoza folderu aplikacji

Nazwane wolumeny, sieci z domyślnym sterownikiem bridge, montowania bind wewnątrz folderu aplikacji (./config:/config), publikowane porty i rezerwacje GPU (gpus, deploy.resources.reservations.devices) nie są uprzywilejowane.

Symbol zastępczy ${VAR} w jednym z tych ustawień jest dozwolony i liczy się jako uprzywilejowany, więc również wymaga deklaracji. W ten sposób parametr aplikacji może pozwolić użytkownikom wybrać port szeregowy:

services: plc: build: . devices: - "${SERIAL_PORT}:/dev/ttyS0"

Pliku compose, który używa include albo extends z polem file, nie da się opublikować, z deklaracją czy bez: wydanie zawiera tylko docker-compose.yml. Połącz pozostałe pliki w ten jeden.

Proś tylko o to, czego aplikacja potrzebuje. Jeśli potrzebuje jednego portu szeregowego, napisz plik compose z jedną usługą i devices: [/dev/ttyUSB0] zamiast pojedynczego obrazu lub privileged: true. Użytkownik widzi dokładnie, co dostaje aplikacja, a aplikacja nie może zrobić nic ponad to.

Zgoda i jej cofnięcie

  • Podczas instalacji użytkownik widzi Twój reason, a w przypadku aplikacji compose także listę ustawień, i włącza lub wyłącza dostęp uprzywilejowany. Odmowa nigdy nie blokuje instalacji.
  • Podczas aktualizacji, jeśli nowa wersja prosi o dostęp uprzywilejowany, a projekt go nie przyznał, pyta o to okno aktualizacji. Mimo odmowy aplikacja zostaje zaktualizowana; nowa wersja działa wtedy bez dostępu uprzywilejowanego.
  • Później dostęp uprzywilejowany jest przełącznikiem w ustawieniach aplikacji projektu, gdzie użytkownicy mogą go w każdej chwili cofnąć lub przyznać. Działające urządzenia uruchamiają aplikację ponownie z nowym ustawieniem.

Uruchomienie deweloperskie ze środowiska IDE w chmurze kieruje się deklaracją i nie ma przełącznika: build deweloperski działa w trybie uprzywilejowanym, gdy .ironflock/data-template.yml deklaruje privileged, a bez tej deklaracji działa bez dostępu uprzywilejowanego. Jeśli plik compose używa uprzywilejowanych ustawień bez deklaracji, build deweloperski kończy się tym samym błędem co publikacja.

Bez zgody

Aplikacja i tak się uruchamia. Aplikacja z pojedynczym obrazem działa z domyślnymi ustawieniami Dockera: bez /dev, bez dodatkowych capabilities, bez D-Bus hosta. W przypadku aplikacji compose agent urządzenia usuwa uprzywilejowane ustawienia z pliku compose przed uruchomieniem aplikacji i zapisuje w logu aplikacji, które ustawienia usunął z której usługi. Zadbaj o to, by Twój kod zgłaszał, do czego nie ma dostępu, na przykład port szeregowy, którego nie może otworzyć, zamiast ulegać awarii.

Starsze wydania

Wydania opublikowane, zanim aplikacje musiały deklarować dostęp uprzywilejowany, są traktowane tak, jakby o niego prosiły, a istniejące instalacje go zachowują, więc te aplikacje działają jak dotąd. Użytkownicy mogą go cofnąć w ustawieniach aplikacji projektu, tak jak w przypadku każdej innej aplikacji. Od Twojego następnego wydania deklaruj privileged, jeśli aplikacja tego potrzebuje.

Last updated on