ABAP 7.5 Interne Tabellen.

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.

ABAP Interne Tabellen: Arbeitsbereiche (Work Areas)

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.

Die Lösung ab Release 7.4: Inline-Deklaration

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.

Noch effizienter: FIELD-SYMBOLs ohne vorherige Deklaration

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.

Gültigkeitsbereich von Workareas

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.

Lesen aus einer internen Tabelle

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 [].

Früher: READ TABLE – ausführlich, aber verständlich

So 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.

Heute: Direktzugriff mit eckigen Klammern

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.

Achtung: Ausnahme statt 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.

CORRESPONDING für normale interne Tabellen

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).

Warum 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.

Beispiel: Fahrzeugdaten intelligent übertragen

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:

  • Felder mit identischem Namen sollen automatisch übernommen werden.
  • Das Feld mileage soll nicht übernommen werden, obwohl es in beiden Strukturen existiert.
  • Das Feld max_speed aus der Quelltabelle soll in ein Feld namens top_speed in der Zieltabelle übernommen werden.

Vorgehensweise vor Release 7.4:

  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.

Lösung ab Release 7.4+ mit 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 ).

Was passiert hier?

  • Alle gleichnamigen Felder werden automatisch übernommen.
  • mileage wird explizit ausgeschlossen.
  • max_speed wird gezielt in top_speed gemappt.

Neue Funktionen für typische Aufgaben mit internen Tabellen

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.

Ein Beispiel dazu (vor 7.4), übertragen auf eine Autotabelle:

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.

Eleganter mit 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.

Existenz prüfen mit 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.

Gruppierung von internen Tabellen mit GROUP BY

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.

Ein Praxisbeispiel

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.

Was passiert hier genau?

  • Die interne Tabelle cars enthält mehrere Fahrzeuge mit Modellbezeichnung und Kilometerstand.
  • Wir gruppieren die Tabelle nach model und dem Rückgabewert der Methode is_ready_for_use, die überprüft, ob das Fahrzeug einsatzbereit ist.
  • Mit CHECK filtern wir nur Gruppen, deren Fahrzeuge dienstbereit sind.
  • Innerhalb jeder Gruppe erstellen wir eine Subset-Tabelle, um exemplarisch zu zeigen, wie man auf die gruppierten Einträge zugreifen kann.
  • Am Ende summieren wir die Kilometer und geben sie je Modell aus.

FILTER-Konstruktor

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.

FILTER mit Bedingung

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.

FILTER als Ersatz für FOR ALL ENTRIES (aber intern)

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.

Fazit

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.