Die Sprache ABAP hat sich seit NetWeaver ABAP 7.40 enorm weiterentwickelt. Viele neue Sprachkonstrukte sind hinzugekommen. Vor allem natürlich viele neue Ausdrücke. Damit kann man sauberen Code schreiben und sich viele Hilfsvariablen und Deklarationen sparen. Die neuen Sprachelemente sollten genutzt werden, um den Code robuster und lesbarer zu machen. Eine wesentlich schnellere Ausführung ist jedoch nicht zu erwarten.
Dieses Plakat zeigt die wichtigsten und nützlichsten Neuerungen. Ausgelassen wurden die neue ABAP-SQL-Syntax und die ABAP Built-In Functions.
Mit Inline-Deklarationen kann eine Variable dort definiert werden, wo sie erstmals verwendet wird – also bei ihrer ersten Wertzuweisung. An vielen Stellen im ABAP-Code ergibt sich der Datentyp einer Variablen aus dem Kontext.
Mit DATA(<Name>) kann ein Datenobjekt dort definiert werden, wo es benötigt wird.
LOOP AT ResultTab INTO DATA(ResultLine).
...
ENDLOOP.
DATA ResultLine LIKE LINE OF ResultTab.
LOOP AT ResultTab INTO ResultLine.
...
ENDLOOP.
Die Vorteile der Inline-Deklaration sind neben dem kompakteren Code auch die Flexibilität. Wenn sich im Beispiel die Feldliste ändert, wird der Datentyp von RESULT automatisch angepasst. Gerade bei JOIN Operationen hat man oft auch keine passende Strukturdefinition.
SELECT *
FROM zbc_users
INTO TABLE @DATA(Result).
DATA Result TYPE TABLE OF zbc_users.
SELECT *
FROM zbc_users
INTO TABLE Result.
Auch Feldsymbole können mit Inline-Deklarationen einem existierenden Speicherbereich zugewiesen werden. Entweder mit einem ASSIGN oder mit den entsprechenden Anweisungen für den Zugriff auf interne Tabellen wie zum Beispiel
LOOP AT ... ASSIGNING FIELD-SYMBOL(<fs-name>).
...
ENDLOOP." oder
READ TABLE ... ASSIGNING FIELD-SYMBOL(<fs-name>) ...
Sehr häufig wird Datenobjekte nur einmal ein Wert zugewiesen, der sich im folgenden Code nicht ändern soll. In diesen Fällen kann statt mit DATA auch mit FINAL deklariert werden. Damit kann man sich darauf verlassen, dass immer dieser Wert zugewiesen ist und nicht versehentlich etwas anderes eingetragen wird.
FINAL(Tomorrow) = CONV dats( sy-datum + 1 ).
&&Zwei Zeichenketten können mit dem Operator && verkettet werden. Dies ist wesentlich eleganter als mit CONCATENATE und funktioniert auch an Operandenpositionen ohne Hilfsvariable.
out->write( `Es ist das Jahr `
&& sy-datum(4) ).
DATA tmp TYPE c LENGTH 100.
CONCATENATE `Es ist das Jahr `
sy-datum(4) INTO tmp.
out->write( tmp ).
Beim Zusammensetzen von Zeichenketten ist es oft notwendig, 'Anhängen' zu verwenden. Zum Beispiel bei der Generierung von HTML.
html &&= `<b>Hallo</b>`
CONCATENATE html `<b>Hallo</b>`
INTO html.
String-Templates sind Ausdrücke, die eine Zeichenkette erzeugen. Sie bestehen aus Literalen, die eingebettete Ausdrücke, Formatierungen und Steuerzeichen enthalten können. Ein String-Template beginnt und endet mit einem senkrechten Strich |. Alles dazwischen ist konstanter Text (Literal), außer er ist ein von geschweiften Klammern { ... } umgebener, eingebetteter Ausdruck.
Die Zeichen {, }, | und \ müssen im Literal durch einen Backslash \ escaped werden.
out->write( |Heute: {
sy-datum DATE = USER }| ).
DATA tmp TYPE string.
DATA datum TYPE c LENGTH 10.
WRITE sy-datum TO datum.
CONCATENATE `Heute: ` datum INTO tmp.
out->write( tmp ).
In geschweiften Klammern { ... } können Ausdrücke direkt in das String-Template eingebettet werden. Damit kann z.B. eine funktionale Methode aufgerufen oder eine Berechnung durchgeführt werden. Schöne Beispiele dafür sind bei den Konstruktor-Ausdrücken COND und SWITCH.
Die Darstellung der Ausdrücke kann über die Formatierungsoptionen angepasst werden. Die wichtigsten sind
WIDTH = <Länge> - Breite in ZeichenALIGN = <Ausrichtung> - Entweder LEFT, RIGHT oder CENTERDATE = USER - Formatierung des Datums gemäß Benutzerstammsatz, siehe obenTIME = USER - Dasselbe für die ZeitALPHA = IN/OUT - AlphakonvertierungDie Steuerzeichen
\n - Zeilenvorschub/Line Feed\r - Wagenrücklauf/Return und\t - Tabulatorkönnen direkt in String Templates verwendet werden. Damit können also ohne Weiteres auch mehrzeilige Texte ohne die Verwendung von CL_ABAP_CHAR_UTILS=>CR_LF erzeugt werden.
out->write( |Hallo\r\nWelt| ).
Konstruktor-Ausdrücke erzeugen ein neues (Daten-) Objekt eines bestimmten (Daten-) Typs. Dazu werden sogenannte Konstruktor-Operatoren verwendet. Diese decken eine Vielzahl sehr unterschiedlicher Anwendungsfälle ab. Gemeinsam haben sie jedoch die folgende Syntax:
<Konstruktor-Operator> <Datentyp>( <Parameter> ).
Ein einfaches Beispiel: Mit dem Operator VALUE eine interne Tabelle mit festem Inhalt erzeugen.
TYPES tt_Country TYPE STANDARD TABLE OF I_Country WITH DEFAULT KEY.
DATA Country TYPE tt_Country.
Country = VALUE tt_Country( ( Country = 'DE' IsEuropeanUnionMember = 'X' )
( Country = 'US' IsEuropeanUnionMember = ' ' ) ).
#Je nach Situation wird der passende Datentyp angegeben, z.B. ein Datenelement, ein Tabellentyp, ein Strukturtyp oder ein Klassenname.
Wenn der Datentyp aus dem Kontext heraus abgeleitet werden kann, dann muss er nicht mehr angegeben werden. Stattdessen wird das # Zeichen verwendet. Das ist z.B. bei einer Zuweisung der Fall.
Einige Operatoren können sehr komplexe Logik ausführen. Dazu können Konstruktor-Ausdrücke auch ineinander verschachtelt werden. Der Code wird dabei schnell unübersichtlich. Es empfiehlt sich daher, nur einfache Logik in Konstruktor-Ausdrücken zu verwenden. Bei komplexer Logik ist klassisches ABAP oft besser lesbar. In den ADTs gibt es keine Möglichkeit, Konstruktor-Ausdrücke zu debuggen.

Maßgeschneiderte Schulungen und Beratung von Buchautoren und SAP Champions! Auf unserer Website www.brandeis.de finden Sie unser Angebot.
(C) Brandeis Consulting GmbH
VALUE und NEWDie beiden Operatoren erzeugen Daten. Mit VALUE erhält man die Daten direkt, mit NEW die Referenz darauf. Wenn keine Parameter übergeben werden, erzeugen beide Operatoren leere Datenobjekte.
Wenn Strukturen erzeugt werden, kann man die einzelnen Komponenten als Parameter mitgeben. Nicht zugewiesene Komponenten werden mit ihrem Initialwert belegt.
DATA(Country) = VALUE I_Country( Country = 'DE' IsEuropeanUnionMember = 'X' ).
Die einzelnen Zeilen einer internen Tabelle werden wiederum in runde Klammern eingeschlossen.
DATA DateRange TYPE RANGE OF dats.
DateRange = VALUE #( ( sign = 'I' option = 'EQ' low = '20221031' )
( sign = 'I' option = 'EQ' low = '20220406' ) ).
Gemeinsame Bestandteile der einzelnen Zeilen können auch vor den runden Klammern definiert werden.
DateRange = VALUE #( sign = 'I' option = 'EQ' ( low = '20221031' )
( low = '20230406' ) ).
BASE gibt einen Initialwert vorBei Strukturen kann vor der ersten Komponente mit BASE eine kompatible Struktur angegeben werden, die die nicht zugewiesenen Komponenten mit Werten füllt.
Bei internen Tabellen kann mit BASE eine interne Tabelle mitgegeben werden, die dann durch die folgenden Zeilen ergänzt wird. Das entspricht also einem APPEND.
NewDateRange = VALUE #( BASE DateRange
( sign = 'I' option = 'EQ' low = '20230101' ) ).
Im VALUE-Operator sind auch FOR-Schleifen möglich. Diese sind beim REDUCE-Operator beschrieben.
NEWMit NEW können auch Instanzen von Klassen erzeugt werden.
NEW <Klassenname>( <Konstruktorparameter> )
CONV und EXACTDieser Operator wird verwendet, um Datentypen zu konvertieren. Dies ist z.B. bei Methodenparametern ein häufiges Problem: Der Inhalt einer Variablen passt, der Datentyp aber nicht.
my_method( Start = CONV #( sy-datum ) ).
"oder
my_method( Start = EXACT #( sy-datum ) ).
DATA DateText TYPE char8.
DateText = sy-datum.
my_method( Start = DateText )
Der Unterschied zwischen EXACT und CONV ist erst im Falle von Datenverlust erkennbar. Wenn durch die Typkonvertierung beispielsweise der Inhalt einer Zeichenkette abgeschnitten wird, dann führt CONV zur Laufzeit zu einer Ausnahme vom Typ CX_SY_MOVE_CAST_ERROR, die abgefangen werden kann.
CASTIn klassischem ABAP erfolgt der Upcast durch Zuweisung mit =, der Downcast mit dem Cast-Operator ?=. Dafür wird immer eine Hilfsvariable vom passenden Typ benötigt. Mit dem CAST-Operator kann diese in vielen Fällen entfallen.
DATA User TYPE zbc_user.
DATA(UserComponents) = cast cl_abap_structdescr(
cl_abap_typedescr=>describe_by_data(
User ) )->get_components( ).
DATA User TYPE zbc_users.
DATA StructDescr TYPE REF TO cl_abap_structdescr.
DATA(TypeDescr) = cl_abap_typedescr=>describe_by_data( User ).
StructDescr ?= TypeDescr.
DATA(UserComponents) = StructDescr->get_components( ).
COND und SWITCHDiese Konstruktor-Ausdrücke entsprechen in SQL dem CASE Ausdruck. Einfache Fallunterscheidungen in Abhängigkeit von einem Feld werden mit SWITCH implementiert.
out->write(
|Hallo { SWITCH #( User-Gender
WHEN 'F' THEN 'Frau'
WHEN 'M' THEN 'Herr'
ELSE '' )
} { User-Lastname } | ).
DATA Salutation TYPE string.
CASE User-Gender.
WHEN 'F'. Salutation = 'Frau'.
WHEN 'M'. Salutation = 'Herr'.
WHEN OTHERS. Salutation = ''.
ENDCASE.
out->write( |Hallo { Salutation
} { User-Lastname }| ).
Komplexere Unterscheidungen mit beliebigen Bedingungen erfolgen mit COND.
out->write( |Der Status ist { COND #( WHEN Priority > 3
AND DueDate <= SY-DATUM THEN 'Critical'
WHEN Priority > 2 THEN 'Medium'
ELSE 'Low' ) }| ).
REDUCE einen Wert aus einer internen Tabelle berechnenDieser Operator berechnet durch Schleifen über interne Tabellen einen einzelnen Ergebniswert. Auf komplexe Varianten wurde bewusst verzichtet, da dieser Operator schnell die Lesbarkeit einschränkt.
out->write( REDUCE string( INIT res TYPE string
sep TYPE string
FOR User IN Users
NEXT
res &&= sep && User-Firstname
sep = ', ' ) ).
FILTER-Operator erzeugenMit dem FILTER-Operator können interne Tabellen auf Basis einer anderen internen Tabelle durch Filterung erzeugt werden. Entweder über eine einfache WHERE-Klausel oder anhand einer anderen Tabelle. Grundsätzlich müssen Tabellentyp und -Schlüssel für die Filterung optimiert sein, sonst gibt es Syntaxfehler.
FILTER mit WHERE-KlauselDATA lt_data TYPE SORTED TABLE OF I_CountryText WITH UNIQUE KEY LANGUAGE COUNTRY.
SELECT * FROM i_countrytext INTO TABLE @lt_data.
out->WRITE( FILTER #( lt_data WHERE LANGUAGE = 'D' ) ).
FILTER mit IN ... WHEREHier wird nach einer anderen internen Tabelle gefiltert. Dies entspricht einem INNER JOIN in SQL.
DATA lt_data TYPE SORTED TABLE OF I_CountryText WITH UNIQUE KEY COUNTRY.
DATA(lt_filter) = VALUE tt_demo( ( COUNTRY = 'DE' )
( COUNTRY = 'US' ) ).
SELECT * FROM i_countrytext WHERE LANGUAGE = 'D' INTO TABLE @lt_data.
out->WRITE( FILTER #( lt_data IN lt_filter WHERE COUNTRY = COUNTRY ) ).
CORRESPONDINGDieser Operator erinnert an die Anweisung MOVE-CORRESPONDING. Er kann sowohl für Strukturen als auch für interne Tabellen verwendet werden.
Mit dem CORRESPONDING-Operator kann man aus einem strukturierten Datenobjekt, also einer internen Tabelle oder einer Struktur, ein anderes Datenobjekt erzeugen und dabei die Werte gleichlautender Felder übernehmen. Das ähnelt der Anweisung MOVE-CORRESPONDING.
CORRESPONDINGDie Daten werden in gleichlautende Felder kopiert. Felder, die in der Quelle nicht vorhanden sind, bleiben im Ziel leer.
TasksSmall = corresponding #( TasksOriginal ).
MAPPING und EXCEPTWenn die Komponenten nicht exakt den gleichen Namen haben und trotzdem aufeinander kopiert werden oder wenn einzelne Komponenten eben nicht kopiert werden sollen, so kann dies explizit mit den Zusätzen MAPPING und EXCEPT angegeben werden.
TaskSmall = corresponding #( TaskOriginal
MAPPING id = task_id
name = summary
EXCEPT assignee ).
Mit BASE werden Initialwerte vorgegebenen, siehe VALUE-Operator. Damit werden bei Strukturen die Werte vorgefüllt oder bei internen Tabellen zusätzliche Zeilen an den Anfgang gesetzt.
CORRESPONDING mit Lookup TabelleDiese Variante führt ein Lookup auf eine andere interne Tabelle durch. Das entspricht einem LEFT OUTER JOIN mit einer :1 Kardinalität. Wie beim FILTER-Operator funktioniert dies nur dann, wenn der Tabellentyp und die Schlüsseldefinition zu der Join-Bedingung in der USING-Klausel passen.
DATA Lookup TYPE HASHED TABLE OF I_CountryText WITH UNIQUE KEY Country.
DATA(Original) = VALUE tt_demo( ( Country = 'DE' )
( Country = 'US' ) ).
SELECT * FROM I_CountryText WHERE LANGUAGE = 'D' INTO TABLE @Lookup.
DATA(Result) = CORRESPONDING tt_demo( Original FROM Lookup
USING Country = Country
MAPPING country_text = CountryName ).
Hinter dem sperrigen Begriff Prädikativer Methodenaufruf verbirgt sich ein einfaches Konzept, das in (fast) allen anderen Programmiersprachen Standard ist: Ein Methodenaufruf als Prädikat, z.B. direkt in einer IF Anweisung. Da in ABAP die Werte TRUE und FALSE nicht eindeutig festgelegt sind, gilt die folgende Definition:
Wenn der Rückgabewert einer funktionalen Methode (d.h. mit RETURNING ) initial ist, ist das Prädikat logisch FALSE, ansonsten TRUE. Es entspricht also IF Methode( ) IS NOT INITIAL.
IF isRelevant( ).
...
ENDIF.
IF isRelevant( ) EQ abap_true.
...
ENDIF.
Auch wenn der Name etwas anderes vermuten lässt, so liefern uns Tabellenausdrücke Zeilen einer Tabelle. In SQL würde man sie daher Zeilenausdrücke nennen.
Sie sind keine Kopien der Zeile, sondern die Zeile in der Tabelle. Deshalb verändern Schreiboperationen auch die Tabelle. Sie ersetzen also READ ... INTO und READ ... ASSIGNING Anweisungen.
Aufbau von Tabellenausdrücken: <Tabelle>[ <Zeilenspezifikation> ]
Die Tabelle kann dabei eine beliebige interne Tabelle sein. Die Zeilenspezifikation steht in eckigen Klammern und kann auf verschiedene Arten erfolgen: Über die Angabe der Zeilennummer bei Standardtabellen, eines freien Schlüssels oder des Tabellenschlüssels.
* Kopie der ersten Zeile
DATA(row) = Users[ 1 ].
* Ändern des Vornamens 1. Zeile
Users[ 1 ]-firstname = 'Edgar'.
* Ändern von Peters Nachname
Users[ firstname = 'Peter'
]-lastname = 'Pan'.
DATA row LIKE LINE OF Users.
READ TABLE Users INTO row INDEX 1.
FIELD-SYMBOLS <row> LIKE LINE OF Users.
READ TABLE Users ASSIGNING <row> INDEX 1.
<row>-firstname = 'Edgar'.
FIELD-SYMBOLS <row> LIKE LINE OF Users.
READ TABLE Users ASSIGNING <row>
WITH firstname = 'Peter'.
<row>-lastname = 'Pan'.
Ein Zugriff auf nicht existierende Zeilen erzeugt eine Ausnahme CX_SY_ITAB_LINE_NOT_FOUND.
* Ändern von Peters Nachname
TRY.
Users[ firstname = 'Peter'
]-lastname ='Pan'.
CATCH CX_SY_ITAB_LINE_NOT_FOUND.
ENDTRY.
* Ändern von Peters Nachname
FIELD-SYMBOLS <row> LIKE LINE OF Users.
READ TABLE Users ASSIGNING <row>
WITH firstname = 'Peter'.
IF sy-subrc = 0.
<row>-lastname = 'Pan'.
ENDIF.
Bei lesendem Zugriff kann alternativ der VALUE-Operator verwendet werden, um
einen Defaultwert zu erzeugen oder der Zugriff kann als OPTIONAL markiert sein.
...VALUE #( <Tabelle>[<Zeilenspezifikation>]
DEFAULT <Alternativer Wert> | OPTIONAL )
Die drei Funktionen line_exists( <Tabellenausdruck> ), lines( <InterneTabelle> ) sowie line_index( <Tabellenausdruck> ) erleichtern die Arbeit mit internen Tabellen erheblich und sparen jeweils eine Hilfsvariable ein. Sie können direkt als Ausdruck verwendet werden.
"Existiert ein User mit Vornamen Heidi?
IF line_exists(
Users[ Firstname = 'Heidi' ] ).
...
"Gibt es mehr als 10 Zeilen in Users?
IF lines( Users ) > 10.
...
"Welchen Index hat die Zeile mit Heidi?
DATA(HeidiIndex) = line_index(
Users[ Firstname = 'Heidi' ] ).
"Existiert ein User mit Vornamen Heidi?
READ TABLE Users TRANSPORTING NO FIELDS
WITH KEY Firstname = 'Heidi'.
IF sy-subrc = 0.
...
"Gibt es mehr als 10 Zeilen in Users?
DESCRIBE TABLE Users LINES DATA(LineCount).
IF LineCount > 10.
...
"Welchen Index hat die Zeile mit Heidi?
READ TABLE Users TRANSPORTING NO FIELDS
WITH KEY Firstname = 'Heidi'.
DATA(HeidiIndex) = sy-tabix.
Die klassische Gruppenstufenverarbeitung mit AT ... ist abhängig von der Sortierung der Daten und der Reihenfolge der Spalten. Beides muss exakt passen. Damit ist sie fehleranfällig und manchmal kaum nutzbar. Die neue Gruppenstufenverarbeitung löst dieses Problem durch verschachtelte LOOP-Schleifen.
LOOP AT PlantMats
INTO DATA(Grouping)
GROUP BY ( Plant = Grouping-Plant )
INTO DATA(Grp).
" A
LOOP AT GROUP Grp
INTO DATA(PlantMat).
" B
ENDLOOP.
" C
ENDLOOP.
LOOP AT PlantMats
INTO DATA(PlantMat).
AT NEW Plant.
" A
ENDAT.
" B
AT END OF Plant.
" C
ENDAT.
ENDLOOP.