Interne Tabellen sind das A und O der Programmierung in ABAP. Im Gegensatz zu vielen anderen Sprachen, die mit diversen Arrays oder Hash-Maps jonglieren müssen, bietet ABAP hier eine integrierte und leistungsstarke Lösung. Seit ABAP 7.5 wurden viele neue Funktionalitäten eingeführt, die den Umgang mit internen Tabellen noch effizienter und intuitiver gestalten. Viele dieser Neuerungen werden hier vorgestellt.
Es ist noch gar nicht so lange her, da galten Header Lines in internen Tabellen bei SAP als eine problematische Praxis. Der Grund: Sie führten dazu, dass zwei verschiedene Datenobjekte im Programm denselben Namen trugen eines für die Tabelle selbst, das andere für die aktuelle Zeile (Workarea). Ein klassisches Beispiel:
1 DATA: cars TYPE STANDARD TABLE OF zcar_data, 2 cars TYPE zcar_data. "Header line
Klar, das ist aus akademischer Sicht problematisch, aber in der Praxis waren viele ABAP-Entwickler mit dieser Konvention groß geworden. Das Loslassen fiel schwer zumal man beim Umstieg zusätzlich explizite Variablen für Workareas deklarieren musste:
1 DATA: car_record LIKE LINE OF cars.
Mit ABAP 7.4 kam dann die Erlösung in Form von Inline-Deklarationen.
Damit ist es möglich, Workareas direkt im LOOP oder bei einem READ zu deklarieren – ganz ohne zusätzliche DATA
-Zeile.
Beispiel: Lesen und Schleifen über eine Tabelle mit Autodaten
1 DATA: cars TYPE STANDARD TABLE OF zcar_data. 2 READ TABLE cars WITH KEY brand = 'BMW' INTO DATA(bmw_details). 3 LOOP AT cars INTO DATA(car_info). 4 " Verarbeitung von car_info 5 ENDLOOP.
Die Vorteile sind klar: Kein zusätzlicher Deklarationsaufwand, eindeutige Namen, saubere Trennung zwischen Tabelle und Workarea.
Wenn es nur um die Verarbeitung (auch mit Änderung) einzelner Zeilen geht, sind FIELD-SYMBOLs in der Regel performanter. Auch hier erlaubt ABAP 7.4 eine elegante Schreibweise ohne FIELD-SYMBOL-Deklaration im Vorfeld:
1 READ TABLE cars WITH KEY brand = 'BMW' ASSIGNING FIELD-SYMBOL(<bmw_details>). 2 LOOP AT cars ASSIGNING FIELD-SYMBOL(<car_row>). 3 " Verarbeitung von <car_row> 4 ENDLOOP.
Das spart nicht nur Zeilen, sondern entspricht auch modernen, lesbaren ABAP-Standards.
Ein häufiger Irrglaube: Workareas, die per DATA(...)
innerhalb eines Loops deklariert werden, verschwinden nach dem Loop automatisch.
Das stimmt nicht – die Variable bleibt technisch weiter verfügbar.
Allerdings sollte man sie außerhalb des Loops nicht mehr verwenden, da sie laut SAPs interner Coding-Guidelines außerhalb ihres Gültigkeitsbereichs liegt.
Seit ABAP 7.4+ gehört das klassische Schlüsselwort READ TABLE
der Vergangenheit an. Stattdessen wird eine moderne, kompakte Syntax verwendet mit eckigen Klammern []
.
READ TABLE
– ausführlich, aber verständlichSo sah das Auslesen einer Zeile aus einer internen Tabelle zum Beispiel aus:
1 DATA: car_brand TYPE zcar_brand VALUE 'TESLA', 2 cars_table TYPE STANDARD TABLE OF zcar_fleet, 3 car_entry LIKE LINE OF cars_table, 4 car_model TYPE REF TO zif_car_model. 5 READ TABLE cars_table INTO car_entry WITH KEY brand = car_brand. 6 car_model = zcl_car_model=>get_instance( car_entry-car_id ).
Diese Variante ist zwar gut lesbar, aber auch relativ langatmig – der eigentliche Zugriff auf die Tabelle geht zwischen Deklarationen und Zuweisungen beinahe unter.
Ab ABAP 7.4+ (insbesondere in Verbindung mit Inline-Deklarationen) ist der Zugriff deutlich kompakter:
1 DATA(car_model) = zcl_car_model=>get_instance( cars_table[ car_brand ]-car_id ).
Der Zugriff wird dadurch nicht nur kürzer, sondern auch eleganter. Der Wert wird direkt aus der Tabelle gelesen und sofort weiterverwendet.
SY-SUBRC = 4
Was passiert, wenn der gesuchte Eintrag nicht vorhanden ist? Früher blieb die Workarea leer, und man konnte SY-SUBRC
prüfen.
Heute: Es wird eine Ausnahme ausgelöst – was leicht zu unerwartetem Verhalten führen kann.
Glücklicherweise hat SAP mit ABAP 7.4 das Schlüsselwort OPTIONAL
eingeführt:
1 DATA(output1) = |Das Modell von { car_brand } ist { VALUE #( cars_table[ car_brand ]-car_id OPTIONAL ) }|.
Alternativ lässt sich mit DEFAULT
ein beliebiger Standardwert setzen:
1 DATA(output2) = |Das Modell von { car_brand } ist { VALUE #( cars_table[ car_brand ]-car_id DEFAULT 'UNKNOWN' ) }|.
Diese neuen Optionen machen den Code kompakter und sicherer. Aber Achtung: Zu viele ineinander verschachtelte Ausdrücke können die Lesbarkeit erschweren – ein häufiger Kritikpunkt bei übermäßigem Einsatz moderner Sprachmittel.
Jeder ABAP-Entwickler kennt und nutzt es: MOVE-CORRESPONDING
, die klassische Methode, um gleichnamige Felder zwischen zwei Strukturen oder internen Tabellen zu übertragen. Doch so nützlich dieser Befehl auch ist, er bringt gewisse Fallstricke mit sich, insbesondere wenn man versehentlich inkompatible Felder überträgt (z.B. Strings in numerische Felder).
MOVE-CORRESPONDING
problematisch sein kann?
In der SAP-Dokumentation wird häufig sogar davon abgeraten, MOVE-CORRESPONDING
zu verwenden, vor allem aus dem Grund, dass es potenziell zu Laufzeitfehlern führen kann, wenn etwa gleichnamige Felder unterschiedliche Datentypen haben.
Mit Release 7.4+ hat SAP jedoch ein neues Werkzeug eingeführt: den Constructor Operator CORRESPONDING
(ohne das Wort "MOVE"). Und dieser bringt Vorteile mit sich.
Stellen wir uns vor, wir haben zwei interne Tabellen: old_cars
und new_cars
. Beide basieren auf unterschiedlichen Strukturen, enthalten jedoch teilweise gleichnamige Felder.
Was wir erreichen wollen:
mileage
soll nicht übernommen werden, obwohl es in beiden Strukturen existiert.max_speed
aus der Quelltabelle soll in ein Feld namens top_speed
in der Zieltabelle übernommen werden.1 DATA: old_cars TYPE STANDARD TABLE OF zcar_old, 2 new_cars TYPE STANDARD TABLE OF zcar_new. 3 FIELD-SYMBOLS: <old_car> LIKE LINE OF old_cars, 4 <new_car> LIKE LINE OF new_cars. 5 LOOP AT old_cars ASSIGNING <old_car>. 6 APPEND INITIAL LINE TO new_cars ASSIGNING <new_car>. 7 MOVE-CORRESPONDING <old_car> TO <new_car>. 8 CLEAR <new_car>-mileage. 9 <new_car>-top_speed = <old_car>-max_speed. 10 ENDLOOP.
Wie man sieht: viel Boilerplate-Code, manuelle Feldzuweisungen und erhöhte Fehleranfälligkeit.
CORRESPONDING
Mit dem neuen Operator reduziert sich der Code drastisch – bei gleichzeitig erhöhter Lesbarkeit und Wartbarkeit:
1 new_cars = CORRESPONDING #( old_cars 2 MAPPING top_speed = max_speed 3 EXCEPT mileage ).
mileage
wird explizit ausgeschlossen.max_speed
wird gezielt in top_speed
gemappt.
Wenn man in ABAP mit internen Tabellen arbeitet, gibt es immer wieder Situationen, in denen man wissen muss, in welcher Zeile sich ein bestimmter Datensatz befindet.
Vor Release 7.4 war dies nur mit einem gewissen „Trick“ möglich: Man musste einen Hilfswert deklarieren, die Tabelle lesen nur um den Zeilenindex zu ermitteln und dann den Wert aus sy-tabix
übernehmen.
1 DATA: start_row TYPE sy-tabix, 2 cars TYPE STANDARD TABLE OF zcar_info, 3 license_plate TYPE zcar_license VALUE 'AB123CD'. 4 READ TABLE cars WITH KEY license_plate = license_plate TRANSPORTING NO FIELDS. 5 IF sy-subrc = 0. 6 start_row = sy-tabix. 7 ENDIF.
In diesem Muster findet man oft den Beginn einer Verarbeitung per LOOP AT cars FROM start_row
, z.B. bei verschachtelten Schleifen oder zielgerichtetem Zugriff.
LINE_INDEX
ab 7.4
Mit ABAP 7.4 wurde dies stark vereinfacht: Die neue eingebaute Funktion LINE_INDEX
liefert den Zeilenindex direkt, ohne dass man sy-subrc
oder sy-tabix
auswerten muss:
1 DATA(start_row) = line_index( cars[ license_plate = license_plate ] ). 2 LOOP AT cars FROM start_row ASSIGNING FIELD-SYMBOL(<car_details>). 3 " Verarbeitung der Fahrzeuge ab dieser Zeile 4 ENDLOOP.
Noch besser: Da LINE_INDEX
ein Ausdruck ist, lässt er sich direkt an Operandstellen verwenden – man braucht gar keine Hilfsvariable mehr:
1 LOOP AT cars FROM line_index( cars[ license_plate = license_plate ] ) 2 ASSIGNING FIELD-SYMBOL(<car_details>). 3 " Verarbeitung der Fahrzeuge ab dieser Zeile 4 ENDLOOP.
LINE_EXISTS
Ein weiteres Beispiel ist die Prüfung, ob ein bestimmter Datensatz bereits existiert. Vor 7.4 sah das z.B. so aus:
1 READ TABLE cars ASSIGNING <car_details> WITH KEY license_plate = license_plate. 2 IF sy-subrc EQ 0. 3 " Verarbeitung bei vorhandenen Eintrag 4 ENDIF.
Seit Release 7.4 gibt es auch hier eine eingebaute Funktion: LINE_EXISTS
. Damit wird das Ganze nicht nur kürzer, sondern auch aussagekräftiger und robuster:
1 IF line_exists( cars[ license_plate = license_plate ] ). 2 " Verarbeitung bei vorhandenen Eintrag 3 ENDIF.
Die meisten ABAP-Entwickler kennen den SQL-Befehl GROUP BY
, mit dem man in Datenbankabfragen ähnliche Datensätze zusammenfasst.
Aber wie sieht es aus, wenn man innerhalb von internen Tabellen gruppieren will? Früher mussten wir uns mit Konstrukten wie COLLECT
oder dem berüchtigten AT NEW
herumschlagen, letzteres war so unzuverlässig.
Mit ABAP 7.4+ kam endlich das GROUP BY
-Konstrukt wurde auf interne Tabellen ausgeweitet. Damit lassen sich Gruppierungen einfach, sauber und performant direkt beim Durchlaufen von Tabellen realisieren – ganz ohne verschachtelte Loops oder umständliche Hilfstabellen.
Szenario: Wir haben eine interne Tabelle mit Informationen über Fahrzeuge. Jedes Fahrzeug gehört zu einem bestimmten Modell und hat eine Anzahl von gefahrenen Kilometern. Wir möchten nun herausfinden, wie viele Gesamtkilometer pro Modell gefahren wurden – aber nur für Fahrzeuge, die als "dienstbereit" gelten.
Anstatt wie früher umständlich eine Zwischentabelle mit allen möglichen Kombinationen zu bauen, können wir mit GROUP BY
direkt gruppieren.
1 TYPES: tt_cars TYPE STANDARD TABLE OF zcar_data WITH DEFAULT KEY. 2 DATA: car_sub_set TYPE tt_cars, 3 total_km TYPE i. 4 DATA(cars) = VALUE tt_cars( 5 ( car_id = '1' model = 'A100' km_driven = 12000 ) 6 ( car_id = '2' model = 'A100' km_driven = 15000 ) 7 ( car_id = '3' model = 'B200' km_driven = 18000 ) 8 ( car_id = '4' model = 'A100' km_driven = 9000 ) 9 ( car_id = '5' model = 'B200' km_driven = 20000 ) 10 ). 11 LOOP AT cars ASSIGNING FIELD-SYMBOL(<car>) 12 GROUP BY ( 13 model = <car>-model 14 is_available = zcl_fleet=>is_ready_for_use( <car>-car_id ) 15 ) ASSIGNING FIELD-SYMBOL(<car_group>). 16 CHECK <car_group>-is_available = abap_true. 17 CLEAR car_sub_set. 18 LOOP AT GROUP <car_group> ASSIGNING FIELD-SYMBOL(<available_car>). 19 car_sub_set = VALUE #( BASE car_sub_set ( <available_car> ) ). 20 ENDLOOP. 21 CLEAR total_km. 22 LOOP AT car_sub_set ASSIGNING FIELD-SYMBOL(<record>). 23 total_km = total_km + <record>-km_driven. 24 ENDLOOP. 25 WRITE: / 'Dienstbereite Fahrzeuge des Modells', <car_group>-model, 26 'haben insgesamt', total_km, 'gefahrene Kilometer'. 27 ENDLOOP.
cars
enthält mehrere Fahrzeuge mit Modellbezeichnung und Kilometerstand.model
und dem Rückgabewert der Methode is_ready_for_use
, die überprüft, ob das Fahrzeug einsatzbereit ist.CHECK
filtern wir nur Gruppen, deren Fahrzeuge dienstbereit sind.Mit ABAP 7.4 (ab SP08) wurde eine elegante Möglichkeit eingeführt, Teilbereiche einer internen Tabelle basierend auf einer Bedingung zu extrahieren – ganz ohne manuelles Loopen. Die Lösung heißt: FILTER-Konstruktoroperator.
Statt also wie früher mit einer Schleife selektiv Datensätze zu prüfen und in eine neue Tabelle zu übertragen, erledigt man dies nun in einer einzigen, ausdrucksstarken Zeile. Doch wie immer steckt der Teufel im Detail – insbesondere was Sortierung und Schlüssel angeht.
Stell dir vor, du hast eine interne Tabelle mit einer Flotte von Fahrzeugen, jedes davon mit einem bestimmten Kilometerstand. Nun möchtest du nur die Fahrzeuge herausfiltern, die unterdurchschnittlich viele Kilometer gefahren sind – zum Beispiel alle unter 75.000 km.
Vor ABAP 7.4 hätte der Code so ausgesehen:
1 DATA: 2 all_cars TYPE SORTED TABLE OF zcar_data WITH NON-UNIQUE KEY car_id 3 WITH NON-UNIQUE SORTED KEY mileage COMPONENTS km_driven, 4 low_mileage_cars TYPE STANDARD TABLE OF zcar_data, 5 single_low_mileage_car LIKE LINE OF low_mileage_cars. 6 LOOP AT all_cars ASSIGNING FIELD-SYMBOL(<car_record>) WHERE km_driven < 75000. 7 CLEAR single_low_mileage_car. 8 MOVE-CORRESPONDING <car_record> TO single_low_mileage_car. 9 APPEND single_low_mileage_car TO low_mileage_cars. 10 ENDLOOP.
Ab ABAP 7.4 geht es so elegant wie folgt:
1 DATA(low_mileage_cars) = FILTER #( all_cars USING KEY mileage WHERE km_driven < CONV #( 75000 ) ).
Wichtig: Der FILTER
-Konstruktor funktioniert nur auf SORTED oder HASHED Tabellen – also solchen, die mindestens einen passenden Schlüssel besitzen.
In unserem Beispiel besitzt all_cars
einen sortierten Schlüssel auf km_driven
, was die Voraussetzung erfüllt.
Ein weiteres Einsatzszenario für FILTER
ist die Selektion aus einer internen Tabelle basierend auf einer anderen –
ganz ähnlich dem bekannten FOR ALL ENTRIES
bei Datenbankzugriffen.
Angenommen, du hast bereits alle Lieferungen (car_deliveries
) aus der Datenbank gelesen und möchtest daraus nur die Lieferungen zu den Fahrzeugen extrahieren,
die sich aktuell in der internen Tabelle all_cars
befinden.
Früher – bei Datenbankzugriff:
1 SELECT * FROM zcar_deliveries 2 INTO CORRESPONDING FIELDS OF TABLE car_deliveries 3 FOR ALL ENTRIES IN all_cars 4 WHERE car_id = all_cars-car_id.
Jetzt – vollständig intern mit FILTER
:
1 DATA(deliveries_for_our_cars) = FILTER #( car_deliveries IN all_cars WHERE car_id = car_id ).
Auch hier gilt: Beide Tabellen müssen einen passenden sortierten oder gehashten Schlüssel haben –
in diesem Fall typischerweise auf car_id
. Hier gehe ich auch davon aus dass, car_deliveries wurde schon aus dem Datenbank gelesen.
Die kontinuierliche Weiterentwicklung von ABAP, insbesondere seit Version 7.4, hat die Arbeit mit internen Tabellen erheblich modernisiert. Von der intelligenten Handhabung von Arbeitsbereichen über prägnante Lesefunktionen bis hin zu leistungsstarken Aggregations- und Filteroperatoren die neuen Funktionen ermöglichen es Entwicklern, effizienteren, lesbareren und robusteren Code zu schreiben. Obwohl einige Neuerungen eine Umstellung erfordern und anfangs komplex wirken mögen, bieten sie leistungsstarke Werkzeuge für die Datenverarbeitung in ABAP-Anwendungen.