Datenbankschema des Adapters
Zweck
Der Adapter hält eine eigene PostgreSQL‑Datenbank mit sechs Tabellen. Sie enthält keine Messwerte — die stehen ausschließlich im FROST‑Server jedes Mandanten. Was hier liegt, ist die Mandanten‑Registry und die Stammdaten, die eine ThingsBoard‑Telemetrie in einen SensorThings‑Begriff übersetzen.
Diese Seite beschreibt die Tabellen und ihre Beziehungen. Welche Regeln beim Schreiben und Löschen gelten, steht in Geschäftsregeln; wie diese Zeilen auf SensorThings‑Entitäten abgebildet werden, in Datenmodell. Alle Modelle stehen in model/sql/models.py; das Schema wird beim Start von bootstrap() angelegt, es gibt keine Migrationswerkzeuge.
Überblick
tenant
│ 1
│ (tenant_id, observed_property_id) = PK
│ UNIQUE (tenant_id, frost_id)
▼ n
tenantobservedproperty ──────┐
│ n
▼ 1
unitofmeasurementkey observedproperty
│ n ▲ 1
│ │
▼ 1 │ n
unitofmeasurement ◄──── n ───── telemetrykey
default_uom_id │
└── observed_property_id (RESTRICT)
Zwei Gruppen: tenant und tenantobservedproperty bilden die Registry, die übrigen vier die mandantenübergreifenden Stammdaten. Die einzige Verbindung zwischen beiden Gruppen ist tenantobservedproperty — die Tabelle, die festhält, welche FROST‑Entität in welchem Mandanten die Kopie einer ObservedProperty ist.
Die Registry
tenant
Ein Mandant ist die Zusammenfassung eines ThingsBoard‑Mandanten mit genau einem FROST‑Server.
| Spalte | Constraint | Bedeutung |
|---|---|---|
|
PK |
Fortlaufende Ganzzahl. |
|
|
Der Bezeichner in der Ingest‑URL. Unveränderlich — er steht in der Rule‑Chain‑URL eines fremden Systems, weshalb |
|
Anzeigename. Darf von |
|
|
|
Die Service‑Root des FROST‑Servers. Die Eindeutigkeit ist es, die die strikte 1:1‑Topologie erzwingt — zwei Mandanten können nicht in denselben Server schreiben. |
|
Der Keycloak‑Client dieses FROST‑Servers. Der Benutzer dahinter ist deployment‑weit, nur der Client ist mandantenspezifisch. |
|
|
nullable |
Nur bei |
|
nullable |
Nur bei |
|
nullable |
Nur bei |
Zwei der drei Zugangsdatenspalten sind immer NULL, und welche zwei entscheidet nicht die Zeile, sondern die Konfiguration: Es gibt genau eine THINGSBOARD_URL, also genau ein Deployment, also genau eine Edition. Alle Zeilen eines Deployments haben damit zu jedem Zeitpunkt dieselbe Form. Eine Aufteilung in zwei Tabellen oder zwei Modelle würde eine Variation abbilden, die nicht vorkommen kann — und dafür eine Diskriminatorspalte, zwei CRUD‑Pfade und Polymorphie auf einer Tabelle kosten, in die tenantobservedproperty einen Fremdschlüssel hält. Erzwungen wird die Form in services/tenant_service.py, nicht im Schema.
Die Geheimnisse sind über die API nur schreibbar: TenantPublic erbt sie nicht und trägt stattdessen has_thingsboard_credentials.
Drei abgeleitete Eigenschaften stehen am Modell und nicht in der Tabelle:
decrypted_api_key/decrypted_password-
entschlüsselt mit
TENANT_SECRET_KEY. Schlägt fehl, sobald der Schlüssel gewechselt hat — pro Mandant, beim Bau seiner Clients. Ist die Spalte leer, nennt der Fehler die fehlende Spalte, statt in der Chiffre aufzulaufen. has_thingsboard_credentials-
ob die Zeile sich überhaupt anmelden kann. Ein Benutzername ohne Passwort ist keine halbe Anmeldung, sondern keine.
fingerprint-
ein SHA‑256 über genau die Felder, aus denen die Clients eines Mandanten gebaut werden — alle drei Zugangsdatenspalten inbegriffen, nicht nur die der konfigurierten Edition. Der Client‑Cache ist danach geschlüsselt, damit ein
PATCHin allen Workern greift und nicht nur in dem, der ihn bedient hat; ein rotiertes Passwort, das den Fingerabdruck nicht bewegt, ließe jeden Worker unbegrenzt mit dem alten anmelden.
|
Ein leeres Geheimnis ist unrepräsentierbar: |
tenantobservedproperty
Die Zuordnungstabelle. Sie beantwortet: Welche FROST‑Entität in welchem Mandanten ist diese kanonische Observed Property?
| Spalte | Constraint | Bedeutung |
|---|---|---|
|
PK, FK → |
Der Mandant. Das Löschen einer Mandantenzeile räumt seine Zuordnungen mit ab. |
|
PK, FK → |
Die kanonische Zeile. |
|
|
Die ID der Kopie im FROST dieses Mandanten. Die Eindeutigkeit verhindert, dass zwei kanonische Zeilen auf dieselbe FROST‑Entität zeigen. |
|
Identität wird hier festgehalten, niemals aus dem Namen erschlossen. Eine fehlende Zeile bedeutet: die Übertragung ist nie angekommen — Reparatur ist Anlegen. Eine vorhandene Zeile bedeutet: die Kopie existiert und darf nie ein zweites Mal entstehen — Reparatur ist das Überschreiben der Attribute. Eine namensbasierte Auflösung kann diese beiden Fälle nicht unterscheiden und erzeugt im zweiten Fall ein Duplikat, das eine Messgröße auf zwei Entitäten aufteilt. |
Die Stammdaten
observedproperty
Die kanonische Zeile: name (eindeutig), definition, description. Jeder Mandant hält in seinem FROST eine abgeleitete Kopie, adressiert über tenantobservedproperty. Lesende Zugriffe sind reine Postgres‑Selects und sprechen keinen FROST‑Server an.
Es gibt bewusst keine Beziehung observed_property.telemetry_keys. Eine solche Sammlung wäre ein Lazy Load, der auf jede abgelöste kanonische Zeile wartet, die die Schreibpfade zurückgeben.
telemetrykey
Die Übersetzung eines ThingsBoard‑Telemetrie‑Schlüssels in einen SensorThings‑Begriff.
| Spalte | Constraint | Bedeutung |
|---|---|---|
|
|
Der Telemetrie‑Schlüssel aus ThingsBoard. Er ist zugleich der Name des zugehörigen Datastreams in FROST. |
|
|
Die gemessene Größe. |
|
FK → |
Die Standard‑Maßeinheit. |
Die Spalte trägt die Id und nicht mehr den Namen der Observed Property. Damit ist eine Umbenennung ein Schreibvorgang auf eine Zeile, und es bleibt kein Übertragungsschritt übrig, der halb greifen könnte. RESTRICT macht „zeigt auf eine Observed Property, die es nicht mehr gibt“ unrepräsentierbar — der Wächter darüber ist das, was daraus eine Meldung mit den betroffenen Schlüsseln macht statt eines nackten IntegrityError.
unitofmeasurement und unitofmeasurementkey
unitofmeasurement trägt name (eindeutig), symbol und definition — genau die drei Felder, die in FROST als eingebettetes unitOfMeasurement‑Objekt eines Datastreams landen.
unitofmeasurementkey bildet ein Suffix eines Telemetrie‑Schlüssels auf eine Maßeinheit ab: key (eindeutig) und ein nullables uom_id. Auf diese Tabelle verweist nichts zurück, weshalb sie als einzige keine Löschsperre hat.
|
Eine Maßeinheit hat keine eigene Identität in FROST. Sie steht als eingebetteter Wert in jedem Datastream, der sie benutzt — nicht als verknüpfte Entität. Dasselbe gilt für einen Telemetry Key, der nur als Name eines Datastreams existiert. Genau das ist der Grund, warum es für diese beiden keinen Wiederholungslauf gibt, für |
Ladeverhalten
Ein Repository gibt ein vollständig geladenes Objekt zurück, weil die Zeile nach dem Session‑Block abgelöst ist. Drei Beziehungen sind deshalb auf lazy=False gesetzt:
TelemetryKey.default_uom
TelemetryKey.observed_property
UnitOfMeasurementKey.uom
Die beiden Sammlungen auf UnitOfMeasurement (telemetry_keys, uom_keys) bleiben lazy und werden dort, wo sie gebraucht werden, innerhalb der Session berührt. Der Vertrag dazu steht in Architektur im Detail.
Die Antwortmodelle
Im selben Modul liegen die Modelle, die die REST‑API nach außen gibt. Zwei Formen fallen auf, weil sie eine Eigenschaft des Systems tragen und nicht nur eine Zeile:
| Modell | Trägt zusätzlich |
|---|---|
|
|
|
|
|
|