enhancement
major
minor
defect
critical
major
minor
major
#30031
Datenbank erzwingt keine Eindeutigkeit der Werte von Referenzen ohne Duplikate: eindeutiger Index auf den Link-Tabellen, eigene Link-Tabellen für Bags
Problem
Die Links aller Referenzen mit Link-Speicherung (SetStorage, ListStorage mit und ohne isBag()) liegen standardmäßig in denselben Tabellen, z.B. hasWrapperAttValue (LinkStorage.LinkStorageConfig#getTable(), Auswahl in LinkStorage.defaultConfig()). Ihre Indizes sind nicht eindeutig (com.top_logic.element/src/main/webapp/WEB-INF/kbase/ElementMeta.xml, hasWrapperAttValueBaseAssociation): {{{#!xml <mo_index name="source" unique="false"> source, metaAttribute </mo_index> <mo_index name="dest" unique="false"> dest, metaAttribute </mo_index> }}} Die Annotation <key-attributes attributes="source,dest,metaAttribute"/> ist reine Metainformation und wird von der Datenbank nicht durchgesetzt.
Damit kann die Datenbank nicht verhindern, dass eine Referenz ohne Duplikate (TLStructuredTypePart#isBag() = false) denselben Wert mehrfach enthält. Die Eindeutigkeit hängt allein vom Code der Speicherung ab. Vor #29975 hat ListStorage sie nicht erzwungen: tAdd() eines schon enthaltenen Werts erzeugte einen zweiten Link ([a, b, a]). Solche Daten können in bestehenden Datenbanken vorhanden sein. Auf der Annahme der Eindeutigkeit aufbauende Zugriffe, z.B. der Ziel-Index hinter TLObject#tContains() (eine BidiMap Ziel → Link), liefern für sie falsche Ergebnisse.
Lösung
In den Anwendungen sind Bags die seltene Ausnahme, fast alle Referenzen sind duplikatfrei. Deshalb werden die bestehenden Link-Tabellen eindeutig, und Bags bekommen eigene Tabellen.
Eindeutiger Index, eigene Tabellen für Bags
- Der Index source von hasWrapperAttValueBaseAssociation, hasHistoricValue und hasMixedValue ist eindeutig auf (source, metaAttribute, dest). Damit gilt er für alle davon abgeleiteten Link-Tabellen (auch die unversionierten, die Kompositions-Tabellen und Link-Tabellen der Anwendungen). Für historische/gemischte Referenzen umfasst er das ganze Ziel einschließlich der Ziel-Revision (DEST_REV), da "X aktuell" und "X in Revision 5" verschiedene Werte sind. Branch und REV_MAX ergänzt die Index-Strategie (DefaultIndexColumnsStrategy) automatisch, so dass der Index nur für die aktuellen Zeilen eines Branches gilt.
- Bag-Tabellen mit den bisherigen, nicht eindeutigen Indizes: hasWrapperAttValueBag, hasWrapperDeleteRefererBag, hasWrapperVetoBag, hasHistoricValueBag, hasMixedValueBag, jeweils auch ...Unversioned; gemeinsame abstrakte Basis hasWrapperAttValueBagBaseAssociation. Komponierende Referenzen brauchen keine: uniqueParent verbietet bereits, dass ein Teil zweimal enthalten ist.
- LinkStorage.defaultConfig() wählt die Tabelle zusätzlich anhand von isBag(). Ist eine Bag-Referenz explizit (<storage table="..."/>) in einer Tabelle mit eindeutigem Index gespeichert, meldet LinkStorage.init() einen Fehler im Log.
- Die Wissensbasis schreibt einen Commit in der Reihenfolge Löschungen, Änderungen, Einfügungen über alle Tabellen (DefaultDBContext.commitTransaction()); Replikation, Import und ChangeSet laufen über denselben Commit. Ein Löschen und Wiedereinfügen desselben Werts verletzt den Index deshalb nie vorübergehend. Ein an der Speicherung vorbei doppelt angelegter Link lässt den Commit mit KnowledgeBaseException scheitern.
Änderung der Speicherung einer Referenz
Die Tabelle einer Referenz hängt von bag, deletionPolicy und historyType ab, die Art der Speicherung von multiple und ordered.
- Im laufenden System (automatisches Modell-Upgrade beim Start aus *.model.xml, Modul-Import in der Oberfläche, KI-Modell-Editor): Ändert sich eine dieser Eigenschaften einer persistenten Referenz, wird ihre Speicherung in derselben Transaktion neu berechnet (PersistentEnd, ReferenceStorageChange), und die vorhandenen Werte werden über die alte Speicherung gelesen und über die neue geschrieben. Teile einer Komposition bleiben dabei erhalten. Beim Wechsel zu einer Referenz ohne Duplikate bleibt das erste Vorkommen eines Werts; beim Wechsel historisch → aktuell wird ein Wert auf sein aktuelles Objekt abgebildet; beim Wechsel zu einer einwertigen Referenz bleibt der erste Wert (mit Warnung im Log). Eine Änderung von composite wird nicht angepasst. Bisher wurde die zwischengespeicherte Speicherung einer Referenz nur bei einer Änderung ihrer Annotationen neu berechnet, so dass solche Änderungen erst nach einem Neustart wirkten und die Werte dann nicht mehr sichtbar waren.
- In einer Migration (ApplyModelPatch mit Migrationsprozessoren): Eine Änderung von bag oder deletionPolicy einer Referenz mit Standard-Speicherung erzeugt <move-links> in die neue Tabelle, beim Wechsel zu einer Referenz ohne Duplikate davor <remove-duplicates>. Bei expliziter Tabelle, Overrides oder gleichzeitiger Änderung des historyType wird eine Warnung ausgegeben.
Migrationsprozessoren
- <align-indexes> (neu): gleicht die Indizes der ausgewählten Tabellen an das Anwendungsschema an (fehlende anlegen, geänderte neu anlegen, entfallene löschen). Die automatische Schema-Migration legt Indizes bestehender Tabellen nicht an.
- Tabellenauswahl tables, subtypes-of, exclude für <align-indexes> und <remove-duplicates>.
- <remove-duplicates>: Duplikate werden pro Attribut (Referenzspalte) und ggf. Ziel-Revision erkannt (vorher wurden gleiche Werte verschiedener Attribute desselben Objekts zusammengefasst); Fehler bei der Überlappungsprüfung nach einer gelöschten Zeile behoben; Option skip-bags; die Kandidaten werden vorab per GROUP BY ... HAVING COUNT(DISTINCT IDENTIFIER) > 1 ermittelt, so dass Tabellen ohne Duplikate nicht durchlaufen werden.
- <move-links-by-bag> (neu): verschiebt die Links aller Bag-Referenzen (oder aller Nicht-Bag-Referenzen) zwischen konfigurierten Tabellenpaaren.
- <move-links> invalidiert die Revisions-Indizes der beteiligten Tabellen.
Migration
Die Migration Ticket_30031_unique_link_tables (tl-element) läuft automatisch:
- Die Bag-Tabellen werden angelegt.
- Die Links aller Bag-Referenzen werden aus den Standard-Tabellen in die Bag-Tabellen verschoben.
- In den Standard-Tabellen (inkl. der davon abgeleiteten Tabellen der Anwendung) werden Duplikate (gleiche source, metaAttribute, dest in überlappenden Revisionen) über die gesamte Historie bereinigt: Der erste Link bleibt, die weiteren werden gelöscht bzw. ihr Gültigkeitsbeginn wird gekürzt. Die Migration protokolliert jeden bereinigten Link.
- Der Index source dieser Tabellen wird durch den eindeutigen Index ersetzt.
Anwendungen mit eigenen, von hasWrapperAttValueBaseAssociation abgeleiteten Link-Tabellen erben den eindeutigen Index. Sind darin Bag-Referenzen (explizit per <storage table="..."/>) gespeichert, werden deren Links nicht bereinigt; die Migration meldet sie als Fehler, und das Anlegen des Index scheitert bei Duplikaten. Solche Referenzen müssen auf eine Bag-Tabelle (abgeleitet von hasWrapperAttValueBagBaseAssociation) umkonfiguriert und ihre Links mit <move-links> verschoben werden.
Java-API: ListStorage.listConfig(...), SetStorage.setConfig(...) und ImageGalleryStorage.imageGalleryConfig(...) haben einen zusätzlichen Parameter boolean bag (bisheriges Verhalten: false, für eine Bag-Referenz true).
Test
- TestLinkTableUniqueness: ein an der Speicherung vorbei angelegter doppelter Link scheitert beim Commit, in der Bag-Tabelle gelingt er; Tabellenwahl; Löschen und Wiedereinfügen in einem Commit; historische Referenz mit demselben Objekt in zwei Revisionen.
- TestUniqueLinkTablesMigration: das Migrationsskript auf einer Datenbank im alten Layout mit Bag-Links, Duplikaten (vollständig und teilweise überlappend) und historischen Werten.
- TestAlignIndexesProcessor, TestRemoveDuplicatesInLinkTables, TestMoveLinksProcessor: die Migrationsprozessoren.
- TestModelPatch: Modell-Patches, die bag bzw. deletionPolicy ändern, verschieben die Links.
- TestReferenceStorageChange: Änderung von bag, deletionPolicy, historyType, ordered an einer Referenz mit Werten im laufenden System, Komposition, Rollback.
- TestListStorage, TestSetStorage laufen unverändert.