Polymorpher Parent-Pattern
Zweck
In SpeamCore sind viele Datensaetze nicht fest an einen Datentyp gebunden, sondern koennen flexibel zu mehreren passen. Das Schema heisst polymorpher Parent-Pattern und besteht aus zwei Spalten:
parentId— UUID des Eltern-Datensatzes.parentType— Name des Datentyps des Eltern-Datensatzes, z. B.'Customer','Location','SalesDocument'.
Damit kann z. B. ein Kontakt sowohl zu einem Kunden als auch zu einem Standort gehören, ohne zwei separate Tabellen zu brauchen.
Wo der Pattern verwendet wird
| Tabelle | Zweck | Erlaubte parentType |
|---|---|---|
ContactParent | Kontakte | Customer, Location, Supplier, PurchaseDocument, SalesDocument |
BankAssignment | Bankverbindungen | Customer, Location |
ProductSpecialCondition | Kunden-Sonderkonditionen | Customer, Location |
DocumentFolder | Dokumentenordner | Customer, Location, Workorder, Project, Product, ... |
DocumentParent | Dokumenten-Anhänge | jede Entitaet, die Anhänge fuehren kann |
TextBlockParent | Textbloecke (Belegtexte) | SalesDocument, PurchaseDocument |
AttributeParent | strukturierte Attribute (Adresse, Telefonnummern) | viele |
SalesDocument.parentId/parentType | Beleg-Empfaenger | Customer, Location |
PurchaseDocument.parentId/parentType | Beleg-Empfaenger | Customer, Location, Warehouse |
Letter.parentId/parentType | Brief-Empfaenger (Welle 148) | Customer, Location, Supplier, Manufacturer, Employee |
BusinessCard.parentId/parentType | Visitenkarten-Owner (Welle 137) | Employee, Contact |
CalendarPage.employeeId | Buchungsseite (Welle 138) | nur Employee (kein echter polymorpher Parent — semantisch verwandt) |
AiObservation.entityRefs[] | Memory-Verknuepfungen (Welle 140) | Liste polymorpher Refs zu beliebigen Entitaeten |
Lese-Zugriff
Sie greifen auf polymorphe Verweise typischerweise so zu:
GET /api/contact-parents?parentId=:id&parentType=Customer
GET /api/document-parents?parentId=:id&parentType=Workorder
GET /api/text-block-parents?parentId=:id&parentType=SalesDocument
Das Backend filtert serverseitig auf den Polymorph-Typ, das Frontend zeigt nur die passenden Datensaetze an.
Adress-Denormalisierung
Einige Belege halten zusaetzlich eine Kopie der Adresse des Parents — z. B. SalesDocument.parentName/Street/Zip/City/CountryId oder Workorder.locationParent* und customerParent*.
Sinn:
- Rechtssicherheit — Belege frieren die Adresse zum Belegdatum ein. Ein nachtraeglicher Adresswechsel am Kunden wirkt nicht auf alte Rechnungen.
- Performance — keine Joins auf Customer/Location bei jedem Listen-Abruf.
Die Synchronisation passiert in den Lifecycle-Hooks beforeCreate / beforeUpdate der jeweiligen Belege (updateParentAddress, updateLocationAndCustomerAddress). Sie wird nicht automatisch nachgezogen, wenn sich die Quelladresse ändert — Änderungen am Kunden wirken erst auf neu erstellte Belege.
Auswirkungen auf Berechtigungen
Page-Guards muessen oft mehrere CASL-Subjects prüfen:
requiredAbility: [
['view', 'FE_PurchaseDocument'],
['view', 'PurchaseDocument'],
['view', 'TextBlockParent'], // wegen Tab "Textbloecke"
['view', 'DocumentParent'], // wegen Tab "Dokumente"
]
Wenn ein Anwender den Beleg sieht, aber den Tab Textbloecke leer findet, fehlt typischerweise TextBlockParent:view.
Drift-Risiken
- Adress-Drift: Adresse am Parent geändert → alte Belege zeigen die alte Adresse. Beabsichtigt für abgeschlossene Belege, aber stoerend für Entwurfs-Belege. Workaround: bei
status = createddie Adresse manuell aktualisieren. - Cleanup beim Parent-Löschen: Wenn ein Kunde gelöscht wird, gehen polymorphe Einträge (
ContactParent,BankAssignmentetc.) nicht automatisch mit —parentIdzeigt ins Leere. Prüfen: gibt es einen Bulk-Cleanup-Job? - Index-Strategie:
parentId-Indizes existieren,parentTypeist häufig nicht indiziert. Bei sehr grossen Tabellen kannWHERE parentType = 'X' AND parentId = Yineffizient werden.
Code-Lookup: Es gibt keinen automatischen Cleanup-Job für verwaiste polymorphe Verweise. In der Praxis fallen sie kaum ins Gewicht, weil die meisten Modelle paranoid: true (Soft-Delete) sind und parentId damit weiter aufloesbar bleibt. Bei tatsaechlichem Hard-Delete koennte ein periodischer Job über Cron-Jobs konfiguriert werden.
Praxis-Beispiel: Eine Adresse ändern
Sie ändern den Firmennamen eines Kunden von „Muster GmbH" auf „Muster Brandschutz GmbH". Auswirkungen:
| Datensatz | Aktualisierung |
|---|---|
Standorte (Location.customerId = K) | nicht direkt — Standortname ist eigene Spalte |
| Aufträge bestehender Standorte | nicht — Workorder.customerParentName bleibt alter Wert |
Verkaufsbelege im Status created | nicht automatisch — manuell speichern, Hook setzt parentName neu |
Verkaufsbelege im Status sent / accepted | nie — Beleg ist rechtlich an alten Stand gebunden |
| Auto-DocumentFolder | ja — afterUpdate-Hook synct den Foldernamen |
| ContactParent / BankAssignment / ProductSpecialCondition | bleiben unveraendert (sie referenzieren nur per ID) |
Verknuepfungen zu anderen Modulen
Praktisch alle Routen-Funktionsseiten (Kunden, Standorte, Belege, Aufträge, Produkte) referenzieren den Pattern. Pflicht-Lese-Reihenfolge für KIera AI:
- Berechtigungen verstehen (CASL)
- Diese Seite.
- Anschliessend die jeweilige Routen-Seite.
Versionshinweise
- 2026-04-29: Initiale Veroeffentlichung.