enhancement
major
minor
defect
critical
major
minor
major
#30167
Datenbank-Defaults für Spalten entfernen: Pflichtspalten per „anlegen, füllen, Pflicht setzen“ hinzufügen
Beobachtung
Das Schema einer Wissensbasis (*Meta.xml, AttributeConfig, DBColumn) kennt keinen Datenbank-Default; Tabellen werden immer ohne DEFAULT angelegt. Datenbank-Defaults entstehen nur als Nebenprodukt von Migrationen:
- AddPrimitiveMOAttributeProcessor legt eine neue Pflichtspalte mit einem typabhängigen Default an (0`, `false, leerer String, aktuelles Datum, ...), damit vorhandene Zeilen einen Wert erhalten.
- AddMOReferenceProcessor legt die Spalten einer neuen Referenz mit Defaults an ("dummyTypeValue", Null-ID, NULL_REPLACEMENT).
- AddDatabaseColumnProcessor kann über default-value einen beliebigen Default setzen.
Die Defaults bleiben danach dauerhaft an der Spalte, obwohl die Wissensbasis sie nicht braucht (neue Zeilen werden immer mit allen Spalten geschrieben). Eine so migrierte Datenbank unterscheidet sich dadurch von einer neu aufgesetzten.
Die Unterstützung von Defaults in der SQL-Erzeugung ist zudem in mehreren Dialekten fehlerhaft:
- H2 und DB2: Für eine nicht verpflichtende Spalte mit Default entsteht ... DEFAULT 'x' DEFAULT NULL (zwei DEFAULT-Klauseln, Syntaxfehler), da nullSpec() DEFAULT NULL liefert. Betrifft addColumn(..) und Spaltenänderungen, z. B. AddDatabaseColumnProcessor mit mandatory="false" und default-value.
- PostgreSQL: Bei einer Typänderung wird der Default an die TYPE-Klausel gehängt (ALTER COLUMN c TYPE ... DEFAULT 'x'), was ungültig ist.
- MS SQL Server: Ein DEFAULT in ALTER COLUMN ist nicht zulässig (Defaults sind dort eigene Constraints).
- MySQL: MODIFY / CHANGE verwirft einen vorhandenen Default, wenn er nicht erneut angegeben wird; Spaltenänderungen (AlterColumnProcessor, RecollateStringColumnsProcessor) entfernen damit migrierte Defaults, während sie auf anderen Datenbanken erhalten bleiben.
Vorschlag
Datenbank-Defaults werden vollständig entfernt:
- Eine Pflichtspalte wird in einer Migration wie in DBSchemaUtils hinzugefügt: Spalte ohne Pflichtangabe anlegen, vorhandene Zeilen per UPDATE mit dem Füllwert belegen, danach per modifyColumnMandatory(..) zur Pflichtspalte machen. Umzustellen sind AddPrimitiveMOAttributeProcessor und AddMOReferenceProcessor.
- AddDatabaseColumnProcessor: default-value wird zum Wert, mit dem vorhandene Zeilen gefüllt werden; es entsteht kein Datenbank-Default mehr.
- Der Default-Wert entfällt aus SQLAddColumn / SQLModifyColumn (getDefaultValue(), setDefaultValue(..)), aus SQLFactory.addColumn(..) (und den modifyColumn..(..)-Methoden, soweit dort noch vorhanden), aus SQLCopy und aus den Dialekten (appendDefaultValue(..), Parameter defaultValue der append..-Methoden in DBHelper und seinen Unterklassen).
Bereits in Datenbanken vorhandene Defaults bleiben unberührt; sie stören nicht.
Tests: Hinzufügen einer Pflichtspalte (primitiv und Referenz) zu einer gefüllten Tabelle auf allen Datenbanken (TestAlterTable bzw. Tests der Migrationsprozessoren), Lauf mit ONLY_DEFAULT_DB = false.
Lösung
- DBHelper und die Dialekte (H2Helper, MSSQLHelper, OracleHelper, PostgreSQLHelper) erzeugen keine DEFAULT-Klausel mehr. appendDefaultValue(..) und die Variante von appendDBType(..) mit Default-Wert sind entfernt, ebenso der Parameter defaultValue von appendChangeColumnName(..), appendChangeColumnType(..) und appendChangeMandatory(..).
- Der Default-Wert ist aus SQLAddColumn, SQLModifyColumn, SQLFactory.addColumn(..) (beide Varianten), SQLCopy und AbstractStatementBuilder entfernt.
- AddMOAttributeProcessor.createColumns(..) legt neue Spalten nullbar an, füllt die vorhandenen Zeilen in einem einzigen UPDATE und setzt danach die Pflichtangabe. AddPrimitiveMOAttributeProcessor und AddMOReferenceProcessor nutzen das; die Spalten einer Referenz werden damit in einem Tabellendurchlauf gefüllt.
- AddDatabaseColumnProcessor: default-value füllt nur die vorhandenen Zeilen und heißt in der Oberfläche jetzt „Füllwert“ („Fill value“); der Konfigurationsname bleibt.
- Neuer Test TestAddColumnProcessors (alle Datenbanken): fügt einer gefüllten Tabelle ein Pflichtattribut, eine Pflicht-Referenz und eine Datenbankspalte mit Füllwert hinzu und prüft, dass alle Zeilen gefüllt sind, die Spalten NOT NULL sind und kein Datenbank-Default bleibt.
- Laufzeit (gemessen auf PostgreSQL 15): Das Füllen kostet rund 12 s je 1 Mio. Zeilen der Tabelle; Tabelle und Indizes wachsen bis zum nächsten VACUUM auf etwa die doppelte Größe. Das Anlegen mit Default war dagegen eine reine Metadaten-Änderung.
Migration
- SQLFactory.addColumn(..) hat keinen Parameter defaultValue mehr; SQLAddColumn.getDefaultValue() / setDefaultValue(..) entfallen. Eigene Migrationsprozessoren, die eine Pflichtspalte mit Default anlegen, legen die Spalte ohne Pflichtangabe an, füllen sie per UPDATE und setzen danach die Pflichtangabe mit SQLFactory.modifyColumnMandatory(..).
- Eigene SQL-Dialekte (Unterklassen von DBHelper): Bei appendChangeColumnName(..), appendChangeColumnType(..) und appendChangeMandatory(..) entfällt der letzte Parameter Object defaultValue; appendDefaultValue(..) und appendDBType(Appendable, DBType, String, long, int, boolean, boolean, Object) gibt es nicht mehr (stattdessen die Variante ohne Default-Wert).
- AddDatabaseColumnProcessor: default-value legt keinen Datenbank-Default mehr an, sondern füllt nur die vorhandenen Zeilen. Wer einen dauerhaften Datenbank-Default braucht, setzt ihn mit einem eigenen sql-Schritt.
- Migrationen, die einer großen Tabelle eine Pflichtspalte hinzufügen, dauern länger (siehe Lösung) und brauchen während der Migration etwa den doppelten Platz für die Tabelle.