enhancement
major
minor
major
minor
minor
#29883
A boolean attribute without a value reads as false on a persistent object, as null on a transient one
An optional tl.core:Boolean attribute that was never set reads back differently, depending on the object's kind:
- **Persistent object** (created in a transaction and committed): tValueByName returns false.
- **Transient object** of the same type (TransientObjectFactory): tValueByName returns null.
Code that compares a stored object with a transient one, e.g. to decide whether imported data differ from what exists, sees a difference where there is none. Found in E-Home (haumacher/ehome #34): the ETS import builds the definition of an already known mask version transiently and compares it with the stored one. Every unset flag counted as a change. The workaround is to treat null and false as equal for booleans in the comparison (ObjectMerge).
Expected: both kinds of object read an unset boolean attribute the same way. Either both return null (no value), or both apply the same default.
Test: a type with an optional boolean attribute. Create one persistent and one transient instance without setting it. Both read the same value.
Analyse
Der Datentyp tl.core:Boolean ist an das BooleanMapping gebunden, das beim Lesen null zu false normalisiert. Ein persistentes Objekt liest und schreibt jeden primitiven Wert über das StorageMapping seines Datentyps (getStorageObject beim Schreiben, getBusinessObject beim Lesen, siehe PrimitiveStorage / PrimitiveColumnStorage). Ein transientes Objekt (TransientTLObjectImpl) speichert den übergebenen Wert unverändert und liefert für ein nie gesetztes Attribut TLModelUtil.getEmptyValue(part), also null für einwertige Attribute. Der null-Fall ist nur ein Symptom: auch ein Integer, der in ein tl.core:Long-Attribut geschrieben wird, kommt aus einem persistenten Objekt als Long, aus einem transienten als Integer zurück.
Lösung
Transiente Objekte wenden das StorageMapping des primitiven Datentyps genauso an wie persistente:
- Beim Schreiben (TransientTLObjectImpl.tUpdate) wird der Wert eines primitiv typisierten Attributs über StorageMapping.normalizeValue (Hin- und Rückabbildung über die Speicherdarstellung) normalisiert, bei mehrwertigen Attributen elementweise. Nicht kompatible Werte (StorageMapping.isCompatible) werden wie beim persistenten checkSetValue mit einer IllegalArgumentException abgewiesen. Die Logik liegt als Hilfsmethode TLModelUtil.normalizeValue(part, value) neben getEmptyValue.
- TLModelUtil.getEmptyValue(part) liefert für ein einwertiges primitives Attribut den Geschäftswert des Mappings für null, also den Wert, den ein persistentes Objekt aus einer leeren Spalte liest (false für tl.core:Boolean, null für tl.core:Tristate und alle anderen Datentypen). Mehrwertige Attribute und Datentypen ohne Mapping sind unverändert. Die weiteren Aufrufer von getEmptyValue (Lesen bei verweigertem Zugriff in CopyOperationImpl und im TL-Script-Attributzugriff AccessLike) verhalten sich damit ebenfalls wie ein nicht gesetztes Attribut.
Test TestPrimitiveStorage (com.top_logic.element): je ein persistentes und ein transientes Objekt; ein nicht gesetztes tl.core:Boolean liest auf beiden false, ein nicht gesetztes tl.core:Tristate auf beiden null, ein in ein tl.core:Long-Attribut geschriebener Integer kommt auf beiden als Long zurück (einwertig und als Listenelement), ein inkompatibler Wert wird auf beiden abgewiesen.