289 lines
15 KiB
Markdown
289 lines
15 KiB
Markdown
# 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": `UPDATE objekt SET zustaendiger_server_id = <satellit> WHERE id = ...` – ab diesem Zeitpunkt lehnt der Hauptserver Schreibzugriffe auf dieses Objekt ab.
|
||
- **Synchronisation ist NICHT reines Batch-Insert** (Korrektur nach Gesamtprüfung): zwei unterschiedliche Sync-Fälle müssen unterschieden werden:
|
||
1. **Neue Zeilen** (Kontrolle, Kontrollposition, Fehlbestand, Nachfüllung, Mindermengen-Genehmigung, Historie, neu hinzugefügte Objektposition): 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.
|
||
- 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 neue Kontrollen/Fehlbestände/etc. per Insert (referenzielle Integrität).
|
||
- Nach vollständiger Übertragung: `UPDATE objekt SET zustaendiger_server_id = <hauptserver>`.
|
||
- **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 sowie das Hinzufügen individueller Zusatzpositionen (Prompt 10, eigene UUID-Zeile) möglich.
|
||
- Benötigte PostgreSQL-Version: ≥ 13 für eingebautes `gen_random_uuid()` (siehe oben).
|
||
- Der Satellit selbst führt eine vollständige, aber auf die ihm zugeordneten Objekte beschränkte Kopie der relevanten Stammdaten (Material, Vorlagen, betroffene Objekte/Objektpositionen) – Details der Vorab-Bestückung offen (siehe [[13_satelliten_server]]).
|
||
|
||
## Referenzen
|
||
Bezug: [[06_datenmodell]], [[19_technische_architektur]], [[13_satelliten_server]]
|