NULL in CDS ABAP

NULL in CDS ABAP

Veröffentlicht am 8. März 2024 von

Jörg Brandeis

| ABAP | CDS |

Im SQL verhält sich NULL wie ein schwarzes Loch. Es hält sich nicht an die "normalen" Regeln der boolschen Logik und saugt ganze Ausdrücke auf. Und NULL kann auch bei der Ausführung von CDS ABAP entstehen. Aber ABAP kommt damit nicht zurecht und übersetzt diese in den Initialwert. Das ist zwar manchmal praktisch, aber dadurch verlieren die Entwickler das Gefühl für den NULL-Wert.

Was ist NULL?

"In SQL, null or NULL is a special marker used to indicate that a data value does not exist in the database. Introduced by the creator of the relational database model, E. F. Codd, SQL null serves to fulfil the requirement that all true relational database management systems (RDBMS) support a representation of "missing information and inapplicable information". "

Wikipedia über NULL

Also soll NULL nicht ein echter Wert sein, sondern das Fehlen eines Wertes anzeigen. Somit entspricht NULL in der Datenbank auch nicht dem Initialwert.

NULL frisst Ausdrücke auf

Wenn ein Ausdruck einen NULL Wert enthält, dann ist das Ergebnis ebenfalls NULL.

  {
    NetAmount * ( Vat / 100 + 1 ) as GrossAmount
  }

Wenn in dem Beispiel also AMOUNT oder VAT den Wert NULL hat, dann ist GrossAmount ebenfalls NULL. Gleiches gilt beispielsweise auch für

  • Verkettung von Zeichenketten
  • Aufruf von SQL-Funktionen
  • CASE Ausdrücke

Da die Konvertierung von NULL in INITIAL erst im ABAP passiert, ist die Logik von den Ausdrücken eine andere. Im folgenden Beispiel würde man vielleicht erwarten: Wenn der Text nicht gefunden wird, dann steht in dem Feld StatusText der Wert "Status:". Statt dessen wird der ganze Ausdruck NULL und damit im ABAP leer.

define view entity zbc_demo_null_in_expression
  as select from    zbc_tasks       as t
    left outer join zbc_status_text as st on  t.status    = st.status
                                          and st.language = $session.system_language
{
  t.task_key                                  as TaskKey,
  t.status                                    as Status,
  CONCAT_WITH_SPACE( 'Status: ', st.text, 1 ) as StatusText
}
TaskKeyStatusStatusText
INT-239NEWStatus: New
R3N-200XYZ
INT-472NEWStatus: New

NULL lässt sich (fast) nicht vergleichen

Der Vergleich mit NULL ergibt im SQL immer den logischen Wert UNKNOWN. Der ist also weder TRUE noch FALSE. An den folgenden Stellen geht es immer nur darum, dass ob ein Prädikat wahr ist, also den Wert TRUE liefert:

  • WHERE-Klausel
  • ON-Bedingung im join
  • CASE WHEN-Bedingung

Eine Unterscheidung zwischen UNKNOWN und FALSE findet nicht statt. Bei der Negierung dieser Werte wird das Problem deutlich:

  • NOT TRUE ergibt FALSE
  • NOT FALSE ergibt TRUE
  • NOT UNKNOWN ergibt UNKNOWN

Das folgende Beispiel zeigt das Dilemma. In einer Datenbanktabelle stehen die folgenden Daten:

IDName
1Peter
2Paul
3Petra
4Andrea
5NULL

Möchte ich alle Datensätze finden, deren Name mit P beginnt, dann liefert mir die folgende Abfrage das gewünschte Ergebnis:

SELECT * 
  FROM Tabelle
 WHERE Name LIKE 'P%'
IDName
1Peter
2Paul
3Petra

Wenn ich jetzt aber alle Datensätze suche, deren Name nicht mit P beginnt, dann liefert mir die folgende Abfrage nur den 4. Datensatz:

SELECT * 
  FROM Tabelle
 WHERE Name NOT LIKE 'P%' 
IDName
4Andrea

Der 5. Datensatz wird also von keiner der beiden Abfragen gefunden.

Das IS NULL-Prädikat

Leider funktioniert auch ein normaler Vergleich mit NULL nicht: NAME = NULL liefert immer UNKNOWN. Denn es spielt keine Rolle, ob NULL links oder rechts vom Vergleichsoperator steht. Das einzige Prädikat, dass einen NULL finden kann, heisst IS NULL. Und das lässt sich auch bei Bedarf mit NOT negieren.

SELECT * 
  FROM Tabelle
 WHERE Name NOT LIKE 'P%' 
    OR Name IS NULL
IDName
4Andrea
5NULL

Wo kommt NULL her?

In der Datenbank des ABAP Systems kommt normalerweise kein NULL vor. Die einzige mir bekannte Methode, um NULL in eine Spalte zu bekommen besteht darin, dass man nachträglich Spalten hinzufügt und dabei nicht das Flag für Initialwerte setzt. Aber wo kann das noch passieren?

OUTER JOIN

Bei einem OUTER JOIN werden immer dann NULL verwendet, wenn für eine Zeile kein Partner in der anderen Tabelle gefunden werden kann.

CASE ohne ELSE

Falls bei einem CASE-Ausdruck keine Bedingung wahr ist und kein Wert für ELSE definiert wurde, dann liefert der CASE Ausdruck NULL zurück.

Und wo ist das Problem mit ABAP?

Abgesehen von den besprochenen Punkten, die man vielleicht als ABAP Entwickler nicht unbedingt erwartet, gibt es ein ganz konkretes Problem bei der Umwandlung von NULL in INITIAL. Und das tritt beim Aggregieren auf. Wenn nach einer Spalte gruppiert werden soll, dann erzeugt die Datenbank für jeden unterschiedlichen Wert eine Zeile. Für die Datenbank sind NULL und INITIAL zwei unterschiedliche Werte. Die werden aber im ABAP auf den gleichen Wert gemappt.

define view entity zbc_demo_null_in_expression
  as select from    zbc_tasks       as t
    left outer join zbc_status_text as st on  t.status    = st.status
                                          and st.language = $session.system_language
{
  st.text as StatusText,
  count(*) as cnt
}
group by st.text

Ergibt:

StatusTextcnt
Neu31
In Bearbeitung7
12
34

Interpretation des Ergebnis:

  • In 31 Zeilen steht der Text "Neu"
  • In 7 Zeilen steht der Text "In Bearbeitung"
  • Bei 12 Zeilen wurde in der Texttabelle kein passende Zeile gefunden. Der Wert wird auf NULL gesetzt.
  • Bei 34 Zeilen wurde in der Texttabelle ein Eintrag gefunden. Der Wert des StatusText ist dort aber leer gewesen. Es kann aber auch anders herum sein, denn man sieht dem Initialwert ja nicht an, woraus er gebildet wurde.

Fazit

Die Logik vom SQL entspricht nicht der Erfahrungswelt von einem ABAP Entwickler. Aber im CDS ABAP hat man relativ selten mit Problemen mit NULL zu tun. Dann ist man aber lange am suchen, wenn man das Konzept von NULL nicht verstanden hat.