Files
MABEA/ergebnisse/20_datenbank_schema.md
T

297 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Prompt 20 Datenbankschema
Bezug: [[06_datenmodell]], [[07_materialstamm]], [[08_beladungsvorlagen]], [[09_duplizieren]], [[10_individuelle_beladung]], [[03_fehlbestandsmanagement]], [[04_mindermengen]], [[05_rollen_rechte]], [[13_historie_audit]]. Basis: PostgreSQL (Prompt 19).
Konkretes relationales Schema, abgeleitet aus dem fachlichen Datenmodell. Migrationstool (z. B. Alembic) wird bei Code-Umsetzung eingesetzt, hier reines Ziel-Schema.
**Update (Karte 13 Satelliten-Server):** Tabellen, die auch an einem getrennten Satelliten-Server neue Zeilen erzeugen können (Kontrolle, Kontrollposition, Fehlbestand, Nachfüllung, Mindermengen-Genehmigung, Historie, **sowie Objektposition**), verwenden `UUID` statt `SERIAL` als Primärschlüssel, damit beim späteren Zusammenführen keine ID-Kollisionen zwischen Hauptserver und Satellit entstehen. Reine Stammdaten-Tabellen (Bereich, Kategorie, Standort, Objekttyp, Material, Vorlage, Benutzer, Objekt selbst) bleiben `SERIAL`, da sie nur zentral am Hauptserver gepflegt werden. `gen_random_uuid()` ist seit PostgreSQL 13 fest eingebaut, keine Extension nötig (bei älteren Versionen `CREATE EXTENSION pgcrypto`).
**Korrektur nach Gesamtprüfung (siehe Punkt 10/11):** Reines Batch-Insert reicht als Sync-Mechanismus NICHT aus, da am Satelliten auch bestehende `objektposition`-Zeilen per UPDATE verändert werden (Ist-Menge, Seriennummer, Ablaufdatum, Chargennummer). Details siehe Punkt 10.
## 1. Stammdaten
```sql
CREATE TABLE bereich (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
beschreibung TEXT
);
CREATE TABLE kategorie (
id SERIAL PRIMARY KEY,
bereich_id INTEGER NOT NULL REFERENCES bereich(id),
name TEXT NOT NULL,
ueberkategorie_id INTEGER REFERENCES kategorie(id),
UNIQUE (bereich_id, name)
);
CREATE TABLE standort (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
adresse TEXT
);
CREATE TABLE objekttyp (
id SERIAL PRIMARY KEY,
bereich_id INTEGER NOT NULL REFERENCES bereich(id),
kategorie_id INTEGER REFERENCES kategorie(id),
name TEXT NOT NULL,
UNIQUE (bereich_id, name)
);
CREATE TYPE materialtyp AS ENUM ('standard', 'ablauf_charge', 'geraet_sn');
CREATE TABLE material (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
artikelnummer TEXT,
einheit TEXT NOT NULL,
materialtyp materialtyp NOT NULL,
kategorie_id INTEGER REFERENCES kategorie(id),
hersteller TEXT,
beschreibung TEXT,
code TEXT UNIQUE, -- QR/Barcode, Code128 (Karte 10)
warnzeitraum_tage INTEGER, -- Standard-Vorwarnung bei Ablaufdatum (Prompt 14)
aktiv BOOLEAN NOT NULL DEFAULT TRUE
);
```
## 2. Vorlagen
```sql
CREATE TYPE vorlage_status AS ENUM ('aktiv', 'veraltet');
CREATE TABLE beladungsvorlage (
id SERIAL PRIMARY KEY,
objekttyp_id INTEGER NOT NULL REFERENCES objekttyp(id),
name TEXT NOT NULL,
version INTEGER NOT NULL,
gueltig_ab TIMESTAMPTZ NOT NULL DEFAULT now(),
status vorlage_status NOT NULL DEFAULT 'aktiv',
UNIQUE (objekttyp_id, name, version)
);
CREATE TABLE vorlagenposition (
id SERIAL PRIMARY KEY,
vorlage_id INTEGER NOT NULL REFERENCES beladungsvorlage(id),
material_id INTEGER NOT NULL REFERENCES material(id),
fach TEXT, -- Kategorie/Fach innerhalb der Vorlage
sollmenge NUMERIC NOT NULL,
UNIQUE (vorlage_id, material_id)
);
```
## 3. Objekte (Rucksäcke/Fahrzeuge/Ressourcen)
```sql
CREATE TYPE knoten_typ AS ENUM ('haupt', 'satellit');
CREATE TABLE systemknoten (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- z. B. 'Hauptserver', 'Satellit Einsatzort Nord'
typ knoten_typ NOT NULL DEFAULT 'satellit'
);
-- genau eine Zeile mit typ='haupt' vorgesehen.
CREATE TYPE objekt_status AS ENUM ('aktiv', 'ausser_dienst');
CREATE TABLE objekt (
id SERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE, -- QR/Barcode, Code128 (Karte 10)
name TEXT NOT NULL,
objekttyp_id INTEGER NOT NULL REFERENCES objekttyp(id),
vorlage_id INTEGER REFERENCES beladungsvorlage(id),
standort_id INTEGER NOT NULL REFERENCES standort(id),
status objekt_status NOT NULL DEFAULT 'aktiv',
zustaendiger_server_id INTEGER NOT NULL REFERENCES systemknoten(id) -- Karte 13: aktuell schreibberechtigter Knoten
);
-- objektposition ist UUID (nicht SERIAL): kann am Satelliten sowohl per UPDATE (Ist-Menge/SN/Ablauf)
-- als auch per INSERT (neues Material am Objekt hinzugefügt, Prompt 10) verändert werden.
CREATE TYPE objektposition_status AS ENUM ('aktiv', 'entfernt');
CREATE TABLE objektposition (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Karte 13: sync-relevant
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
sollmenge_override NUMERIC, -- NULL = folgt Vorlage dynamisch
ist_status objektposition_status NOT NULL DEFAULT 'aktiv', -- korrigiert: Standard ist 'aktiv', nicht 'entfernt' (Prompt 10)
istmenge NUMERIC NOT NULL DEFAULT 0,
seriennummer TEXT, -- nur Materialtyp geraet_sn
ablaufdatum DATE, -- nur Materialtyp ablauf_charge
chargennummer TEXT, -- nur Materialtyp ablauf_charge
zuletzt_geaendert_am TIMESTAMPTZ NOT NULL DEFAULT now(), -- Basis für Delta-Sync (Punkt 10)
UNIQUE (objekt_id, material_id)
);
```
## 4. Zuständigkeiten, Benutzer, Rollen
```sql
CREATE TABLE benutzer (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
login TEXT NOT NULL UNIQUE,
passwort_hash TEXT NOT NULL,
aktiv BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TYPE rolle_typ AS ENUM ('mitarbeiter', 'materialverantwortlicher', 'leitungsverantwortlicher', 'administration');
CREATE TABLE benutzer_rolle (
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
rolle rolle_typ NOT NULL,
PRIMARY KEY (benutzer_id, rolle)
);
-- zustaendigkeit/kontrollverantwortung sind UUID: Administration könnte theoretisch auch am
-- Satelliten Zuordnungen anlegen/ändern (z. B. Kontrollverantwortung vor Ort neu vergeben).
CREATE TABLE zustaendigkeit (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
standort_id INTEGER REFERENCES standort(id),
objekt_id INTEGER REFERENCES objekt(id),
CHECK (standort_id IS NOT NULL OR objekt_id IS NOT NULL)
);
CREATE TABLE kontrollverantwortung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
benutzer_id INTEGER REFERENCES benutzer(id), -- Einzelperson, ODER
gruppe TEXT -- Gruppe (frei benannt, V1-Vereinfachung ohne eigene Gruppen-Entität; echte Gruppen-Verwaltung Roadmap), Karte 01
);
```
## 5. Kontrolle
```sql
CREATE TYPE kontroll_status AS ENUM ('nicht_gestartet', 'in_bearbeitung', 'abgeschlossen', 'abgebrochen');
CREATE TABLE kontrolle (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Karte 13: sync-relevant
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
status kontroll_status NOT NULL DEFAULT 'in_bearbeitung',
gestartet_am TIMESTAMPTZ NOT NULL DEFAULT now(),
beendet_am TIMESTAMPTZ,
abbruch_grund TEXT,
signatur BYTEA -- optionales Touch-Signaturbild am Kontrollnachweis (Prompt 16), nullable, standardmäßig ungenutzt
);
CREATE TABLE kontrollposition (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Karte 13: sync-relevant
kontrolle_id UUID NOT NULL REFERENCES kontrolle(id),
material_id INTEGER NOT NULL REFERENCES material(id),
sollmenge_snapshot NUMERIC NOT NULL,
istmenge_erfasst NUMERIC NOT NULL,
abweichung BOOLEAN NOT NULL
);
```
## 6. Fehlbestand, Nachfüllung, Mindermenge
```sql
CREATE TYPE fehlbestand_status AS ENUM ('offen', 'in_bearbeitung', 'nachgefuellt_teilweise', 'erledigt');
CREATE TABLE fehlbestand (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Karte 13: sync-relevant
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
standort_id INTEGER NOT NULL REFERENCES standort(id),
sollmenge NUMERIC NOT NULL,
istmenge NUMERIC NOT NULL,
fehlmenge NUMERIC NOT NULL,
entstanden_am TIMESTAMPTZ NOT NULL DEFAULT now(), -- Basis Eskalation, Karte 12
festgestellt_von INTEGER NOT NULL REFERENCES benutzer(id),
kontrolle_id UUID REFERENCES kontrolle(id),
ursache TEXT,
verantwortlicher_id INTEGER REFERENCES benutzer(id),
status fehlbestand_status NOT NULL DEFAULT 'offen',
erledigt_am TIMESTAMPTZ
);
CREATE TABLE nachfuellung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Karte 13: sync-relevant
fehlbestand_id UUID REFERENCES fehlbestand(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
menge NUMERIC NOT NULL,
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
zeitpunkt TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TYPE mindermenge_status AS ENUM ('aktiv', 'abgelaufen', 'beendet_durch_erledigung');
CREATE TABLE mindermengen_genehmigung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Karte 13: sync-relevant
fehlbestand_id UUID NOT NULL REFERENCES fehlbestand(id),
genehmigt_von INTEGER NOT NULL REFERENCES benutzer(id),
begruendung TEXT NOT NULL,
genehmigt_am TIMESTAMPTZ NOT NULL DEFAULT now(),
ausloesende_kontrolle_id UUID NOT NULL REFERENCES kontrolle(id),
status mindermenge_status NOT NULL DEFAULT 'aktiv',
beendet_am TIMESTAMPTZ,
beendende_kontrolle_id UUID REFERENCES kontrolle(id)
);
```
## 7. Historie/Audit
```sql
CREATE TABLE historie (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Karte 13: sync-relevant
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
zeitpunkt TIMESTAMPTZ NOT NULL DEFAULT now(),
benutzer_id INTEGER REFERENCES benutzer(id), -- NULL bei System-Ereignissen
ereignistyp TEXT NOT NULL, -- z. B. 'fehlbestand_entstanden', 'mindermenge_genehmigt', ...
entitaet_typ TEXT NOT NULL, -- z. B. 'fehlbestand', 'objektposition', 'vorlage'
entitaet_id TEXT NOT NULL, -- TEXT statt INTEGER: referenzierte Entität kann INTEGER- oder UUID-ID haben
alter_wert JSONB,
neuer_wert JSONB,
begruendung TEXT
);
-- Append-only: Anwendungs-DB-Rolle erhält nur INSERT-Recht auf diese Tabelle, kein UPDATE/DELETE.
```
## 8. Indizes (Auswahl, wichtigste Zugriffspfade)
```sql
CREATE INDEX idx_fehlbestand_status ON fehlbestand(status);
CREATE INDEX idx_fehlbestand_objekt ON fehlbestand(objekt_id);
CREATE INDEX idx_fehlbestand_entstanden ON fehlbestand(entstanden_am); -- für Alter-Sortierung, Prompt 12
CREATE INDEX idx_objektposition_objekt ON objektposition(objekt_id);
CREATE INDEX idx_historie_entitaet ON historie(entitaet_typ, entitaet_id);
CREATE INDEX idx_zustaendigkeit_benutzer ON zustaendigkeit(benutzer_id);
```
## 9. Wichtige Constraints/Prinzipien
- Kein Fremdschlüssel-Löschen mit CASCADE auf historie-relevante Tabellen (fehlbestand, kontrolle, historie) Löschung von Stammdaten (Material/Objekt) erfolgt nur über `aktiv = false`, nie physisches DELETE, damit Historie referenzierbar bleibt (Prompt 13).
- `mindermengen_genehmigung` referenziert Fehlbestand 1:0..n, in der Praxis max. 1 mit Status `aktiv` gleichzeitig wird auf Anwendungsebene erzwungen (kein reiner DB-Constraint, da Historie mehrerer vergangener Genehmigungen erhalten bleiben muss).
- `objektposition.sollmenge_override IS NULL` bedeutet: Sollmenge wird zur Laufzeit aus aktueller `vorlagenposition` der referenzierten Vorlage berechnet (Prompt 10).
## 10. Satelliten-Server Objekt-Sperre und Synchronisation (Karte 13)
- `objekt.zustaendiger_server_id` bestimmt, welcher Server (Haupt oder ein bestimmter Satellit) aktuell Schreibrechte für dieses Objekt hat. Anwendungslogik (nicht reiner DB-Constraint) prüft vor jedem Schreibzugriff auf `kontrolle`, `fehlbestand`, `nachfuellung`, `mindermengen_genehmigung`, `objektposition`, ob der ausführende Server mit `objekt.zustaendiger_server_id` übereinstimmt.
- „Objekt auslagern" (Platzhalter `:satelliten_id`/`:objekt_id` durch konkrete Werte ersetzen):
```sql
UPDATE objekt SET zustaendiger_server_id = :satelliten_id WHERE id = :objekt_id;
```
Ab diesem Zeitpunkt lehnt der Hauptserver Schreibzugriffe auf dieses Objekt ab.
- **Synchronisation ist NICHT reines Batch-Insert** (Korrektur nach Gesamtprüfung): drei unterschiedliche Sync-Fälle müssen unterschieden werden:
1. **Neue Zeilen** (Kontrolle, Kontrollposition, Nachfüllung, Historie, neu hinzugefügte Objektposition, neue Zuständigkeits-/Kontrollverantwortungs-Zuordnung, falls am Satelliten durch Administration vergeben): einfacher Insert am Hauptserver, da UUID bereits eindeutig vergeben keine Kollision möglich.
2. **Geänderte bestehende Zeilen** (`objektposition`: Ist-Menge, Seriennummer, Ablaufdatum, Chargennummer vom Hauptserver vor Auslagerung an den Satelliten kopiert, dort per UPDATE verändert): werden beim Sync per **Upsert** übertragen (`INSERT ... ON CONFLICT (id) DO UPDATE`), nicht als reiner Insert. `zuletzt_geaendert_am` dient als Erkennungsmerkmal, welche Zeilen sich am Satelliten geändert haben.
3. **Fehlbestand und Mindermengen-Genehmigung, die bereits vor der Auslagerung existierten** (offener Fehlbestand am Objekt, evtl. aktive Genehmigung) und am Satelliten weiterbearbeitet werden (Nachfüllung, neue Genehmigung, Statuswechsel auf `erledigt`): ebenfalls **Upsert**, nicht reiner Insert dieselben UUIDs, `status`/`istmenge`/`erledigt_am` bzw. `status`/`beendet_am` können sich am Satelliten ändern.
- Da ein Objekt während der Auslagerung exklusiv einem Server zugeordnet ist (siehe Objekt-Sperre), gibt es keine gleichzeitige Änderung derselben Zeile an zwei Orten Upsert ist damit konfliktfrei, kein Merge nötig.
- Reihenfolge beim Sync: zuerst Objektpositionen-Upsert, danach Fehlbestand/Mindermengen-Genehmigung-Upsert, danach neue Kontrollen/Nachfüllungen/Historie per Insert (referenzielle Integrität).
- Nach vollständiger Übertragung:
```sql
UPDATE objekt SET zustaendiger_server_id = :hauptserver_id WHERE id = :objekt_id;
```
- **Einschränkung für Struktur­änderungen am Satelliten:** Anlegen komplett neuer Vorlagen/Materialstamm-Einträge bleibt dem Hauptserver vorbehalten (diese Tabellen sind SERIAL, nicht sync-fähig). Am Satelliten sind nur Änderungen an bereits vorhandenen `objektposition`-Zeilen, das Hinzufügen individueller Zusatzpositionen (Prompt 10, eigene UUID-Zeile) sowie neue Zuständigkeits-/Kontrollverantwortungs-Einträge möglich.
- Benötigte PostgreSQL-Version: ≥ 13 für eingebautes `gen_random_uuid()` (siehe oben).
- **Vorab-Bestückung des Satelliten (Klärung L2):** neben Materialstamm, Vorlagen und den betroffenen Objekten/Objektpositionen müssen auch **offene Fehlbestände und aktive Mindermengen-Genehmigungen** der ausgelagerten Objekte in die Satelliten-Kopie übernommen werden sonst kann am Satelliten weder nachgefüllt noch genehmigt werden, ohne einen Duplikat-Fehlbestand zu erzeugen. Details der Bestückungslogik bleiben offen (siehe [[13_satelliten_server]]).
## Referenzen
Bezug: [[06_datenmodell]], [[19_technische_architektur]], [[13_satelliten_server]]