This is a test message to test the length of the message box.
Login
ABAP Deep Dive Tabellenzugriff
Erstellt von Software-Heroes

ABAP Deep Dive - Tabellenzugriff (intern)

436

Schauen wir uns in diesem Artikel einmal den Tabellenzugriff auf interne Tabellen an und wie sie den READ TABLE ablösen.

Werbung

Seit nun einer ganzen Weile ist der neue Tabellenzugriff möglich, dabei werden Tabellen wie Arrays in anderen Sprachen gelesen und der Zugriff fällt damit viel kürzer aus. Bisher hat man vor allem über den READ TABLE auf Tabellen zugegriffen und Einträge ermittelt, dies ist nun alles über das neue Statement möglich.

 

Vorbereitung

Bevor wir mit den Beispielen anfangen, noch eine kurze Vorbereitung der Typen und Variablen, die wir in den weiteren Beispielen verwenden werden. wir definieren uns zwei Typen für die Verwendung und zwei Variablen die Daten enthalten:

TYPES:
  BEGIN OF ts_data,
    ident TYPE c LENGTH 3,
    text  TYPE string,
    date  TYPE d,
  END OF ts_data,
  tt_data TYPE SORTED TABLE OF ts_data
    WITH UNIQUE KEY ident
    WITH NON-UNIQUE SORTED KEY bydate COMPONENTS date,

  BEGIN OF ts_deep,
    deep_key TYPE c LENGTH 3,
    data     TYPE tt_data,
  END OF ts_deep,
  tt_deep TYPE STANDARD TABLE OF ts_deep WITH EMPTY KEY.

DATA:
  mt_data TYPE tt_data,
  mt_deep TYPE tt_deep.

 

Die Struktur TS_DATA enthält einige Beispieldaten und wird im Tabellentyp TT_DATA zur Verfügung gestellt. Es handelt sich dabei um eine sortierte Tabelle mit einem eindeutigen Schlüssel, sowie einem zweiten Schlüssel über das Datum. Hier ist zu beachten, dass jeder weitere Schlüssel auf einer internen Tabelle, ebenso Verwaltungskosten produziert, den Index auf dem aktuellen Stand zu halten. In einer zusätzlichen Methode initialisieren wir die Daten in den Variablen mit Beispieldaten:

mt_data = VALUE #(
  ( ident = 'A' text = 'Entry for A' date = '20220125' )
  ( ident = 'B' text = 'Entry for B' date = '20220225' )
  ( ident = 'C' text = 'Entry for C' date = '20220225' )
  ( ident = 'D' text = 'Entry for D' date = '20220125' )
  ( ident = 'K' text = 'Entry for K' date = '20220425' )
  ( ident = 'L' text = 'Entry for L' date = '20220325' )
  ( ident = 'N' text = 'Entry for N' date = '20220525' )
  ( ident = 'O' text = 'Entry for O' date = '20220425' )
).

mt_deep = VALUE #(
  ( deep_key = 'D1' data = mt_data )
  ( deep_key = 'D2' data = mt_data )
  ( deep_key = 'D3' data = mt_data )
).

 

Lesen über Index

Das einfachste Lesen geht über den Index, in den meisten Fällen möchte man die erste Zeile einer Tabelle lesen und verarbeiten. Dazu einfach hinter der Tabelle den Index in eckige Klammern schreiben, die Leerzeichen dazwischen nicht vergessen. Die Inline-Deklaration funktioniert hier, wie du es bereits gewohnt bist:

" Read table with index 1
DATA(ls_index) = mt_data[ 1 ].

" Read table with index 5
ls_index = mt_data[ 5 ].

 

Feld lesen

Du möchtest gar nicht die ganze Zeile haben, sondern nur ein Feld aus der Tabelle für eine bestimmte Zeile lesen? Kein Problem, einfach hinter der eckigen Klammer auf das Feld zugreifen, Eclipse unterstützt dabei mit dem Content Assist (STRG + SPACE). Funktioniert genauso für eine Feldzuweisung, wie für die Inline-Deklaration:

" Read text field
DATA(ld_text) =  mt_data[ 2 ]-text.

" Read date field
DATA(ld_date) =  mt_data[ 5 ]-date.

 

Lesen über Schlüssel

Bisher haben wir die Zeilen vor allem über den Index gelesen, doch für SORTED und HASHED Tabellen funktioniert dies teilweise nur bedingt. Um über einen Schlüssel zu lesen, kannst du anstatt dem Index den Schlüssel in der eckigen Klammer angeben. Dazu einfach Feldname und Vergleich eintragen, bei mehreren Schlüsselfeldern einfach die Felder und die Abfrage auflisten. Möchtest du über einen weiteren Schlüssel lesen, dann musst du den Schlüssel angeben, nur das Feld anzugeben funktioniert nicht. Hier gibt es eine Kurzform und eine Langform:

" Read for primary key
DATA(ls_key) = mt_data[ ident = 'B' ].

" Read for secondary key date 
ls_date = mt_data[ KEY bydate COMPONENTS date = '20220325' ].

" Read for secondary key date (short version)
DATA(ls_date) = mt_data[ KEY bydate date = '20220525' ].

 

Kein Eintrag

Wenn es für deine Abfrage keinen Eintrag gibt, dann wirft das System eine behandelbare Ausnahme die du per TRY/CATCH abfangen kannst. Wenn du die Ausnahme nirgendwo abfängst, kommt es zu einem Abbruch der Verarbeitung:

" Index not found
TRY.
    DATA(ls_index) =  mt_data[ 10 ].
  CATCH cx_sy_itab_line_not_found.
ENDTRY.

" Key not found
TRY.
    DATA(ls_key) =  mt_data[ ident = 'X' ].
  CATCH cx_sy_itab_line_not_found.
ENDTRY.

 

Zusatzfunktionen

Mit den neuen Tabellenzugriffen gibt es auch einige neue Funktionen, um mit den Tabellen zu arbeiten. LINE_EXISTS prüft die nachfolgende Abfrage, ob diese ein Ergebnis zurückgibt. Wenn du diese verwendest, musst du keine Ausnahme abfangen. Dies funktioniert soweit für den Index und den Schlüssel. Mit LINE_INDEX erhältst du den Index für eine Abfrage auf der Tabelle (per Schlüssel). Ist die Zeile mit dem Schlüssel nicht vorhanden, wird der Rückgabewert gesetzt, es muss keine Ausnahme abgefangen werden:

" Index not found
IF NOT line_exists( mt_data[ 10 ] ).
ENDIF.

" Key not found
IF NOT line_exists( mt_data[ ident = 'X' ] ).
ENDIF.

" Index for key
DATA(ld_tabix) = line_index( mt_data[ ident = 'D' ] ).

 

Index finden

In der Vergangenheit hat man oft einen READ genutzt, um die Einfügeposition zu ermitteln, um einen Eintrag in der Tabelle anzulegen (BINARY SEARCH). Dieses Statement sah in den meisten Fällen so aus:

" Fill sy-tabix with position
READ TABLE mt_data TRANSPORTING NO FIELDS
  WITH TABLE KEY ident = 'E'.

 

Hierfür gibt es keine neue Version, aber die ist auch nicht mehr nötig. Mittlerweile solltest du mit Tabellen arbeiten, die einen spezifizierten Schlüssel haben. STANDARD Tabellen ohne Schlüssel sollten nicht mehr so oft verwendet werden. Verwende stattdessen eine sortierte Tabelle oder einen Schlüssel und suche nach dem Eintrag, ist dieser nicht vorhanden, fügst du einfach einen neuen per INSERT ein und der Schlüssel macht den Rest:

IF line_index( mt_data[ ident = 'E' ] ) = 0.
  " Insert new entry
ENDIF.

 

Tabelle ändern

Wie ändert man eigentlich einzelne Tabelleneinträge in einer Tabelle? Hierfür stehen zwei Varianten zur Verfügung, einmal über eine Referenz und eine direkte Anpassung des Eintrags in der Tabelle. Wenn du eine ganze Zeile anpassen möchtest, dann kannst du eine Zeile lesen und dir die Referenz zurückgeben lassen. Wenn du dann die Felder der Referenz änderst, ändert sich auch der Eintrag in der Tabelle. In der zweiten Variante kannst du auch einfach direkt den Inhalt eines Feldes ändern, der Lesezugriff gibt dir die Möglichkeit:

" Change values in line
DATA(lr_data) = REF #( mt_data[ 2 ] ).
lr_data->date = '20230101'.

" Change field of line
mt_data[ ident = 'N' ]-date = '20230202'.

 

Der Inhalt in den beiden Zeilen der Tabelle hat sich nach der Durchführung geändert, nie waren Änderungen an Tabelleninhalten so einfach:

 

Performance

Bei all der Leichtigkeit der Zugriff, solltest du allerdings auch die Performance im Auge behalten. Allzu schnell kann eine Abfrage schlecht implementiert werden, vor allem wenn man einzelne Felder lesen möchte. Im folgenden Beispiel werden die Felder einzeln zugewiesen:

" 3 read requests
DATA(ld_key) = mt_data[ ident = 'N' ]-ident.
DATA(ld_longtext) = mt_data[ ident = 'N' ]-text.
DATA(ld_date) = mt_data[ ident = 'N' ]-date.

 

Das ergibt allerdings auch 3 Lesezugriffe auf die Daten und auch der Zugriff muss 3-mal implementiert werden. Wenn dies nur einmal durchgeführt wird, ist das in Ordnung. Bei mehr Zugriffen sollte die ganze Zeile gelesen werden und einer Variable zugewiesen werden, über diese lassen sich dann die Felder besser versorgen und man spart sich eine Mehrfachimplementierung:

" 1 read request
DATA(ls_data) = mt_data[ ident = 'N' ].
ld_key = ls_data-ident.
ld_longtext = ls_data-text.
ld_date = ls_data-date.

 

Datensätze filtern

Im nächsten Schritt benötigen wir nur eine Teilmenge aus der Tabelle, um damit eine weitere Verarbeitung zu beginnen. Klassisch könnte man das über einen LOOP abbilden und eine WHERE Bedingung nutzten, um die Datensätze in die neue Tabelle zu übernehmen. Dafür muss allerdings die Tabelle definiert werden. Beim LOOP geben wir den sekundären Schlüssel an, damit die Performance höher ist:

DATA:
  lt_filtered_loop TYPE tt_data,
  ld_filter_by     TYPE d VALUE '20220225'.

" Filter with loop
LOOP AT mt_data INTO DATA(ls_data) USING KEY bydate WHERE date = ld_filter_by.
  INSERT ls_data INTO TABLE lt_filtered_loop.
ENDLOOP.

 

Die kürzere Version funktioniert auch über das neue FILTER Statement, Voraussetzung ist ein Schlüssel auf der Tabelle, für den der Filter angewandt werden soll. Filter kann sich den Zieldatentyp aus der übergebenen Tabelle ableiten, deshalb können wir # verwenden. Wenn wir nicht über den primären Schlüssel gehen, geben wir noch den entsprechenden Schlüssel an (wie oben):

" New filter statement
DATA(lt_filtered) = FILTER #( mt_data USING KEY bydate WHERE date = ld_filter_by ).

 

Verschachtelter Zugriff

Wie sieht es eigentlich mit tiefen Strukturen aus? Um sich in der Vergangenheit durch solche Strukturen zu lesen, waren viele READs nötig, heute können die Zugriffe in einem Schritt durchgeführt werden. Da wir auf dem Ergebnis der Abfrage weiterarbeiten können, könnte ein Zugriff wie folgt aussehen:

" Nested access to data
DATA(ld_nested_field) = mt_deep[ deep_key = 'D2' ]-data[ 1 ]-text.

 

Im ersten Schritt lesen wird aus der Tabelle MT_DEEP mit einem Schlüssel und referenzieren auf das Feld DATA, welche eine Tabelle ist. Hier lesen wir den ersten Eintrag und lassen uns den Text dazu zurückgeben, welchen wir per Inline-Deklaration in einer Variable ablegen.

 

VALUE

Beim Thema VALUE Statement gibt es ebenso für die neuen Tabellenzugriffe einige Erleichterungen. Wir haben dir oben beschrieben, wie du nicht vorhandene Zeilen abfangen kannst. Diese Prüfungen helfen auf solche SItutationen zu reagieren, doch wie sieht es damit aus, wenn du eine einfache IF-THEN-ELSE Logik benötigst? Hier mal ein Beispiel, wie du das mit dem Value abbilden kannst:

" When not found, return empty line
DATA(ls_optional) = VALUE #( mt_data[ ident = 'E' ] OPTIONAL ).

" When not found, return the first line 
DATA(ls_default) = VALUE #( mt_data[ 15 ] DEFAULT mt_data[ 1 ] ).

" When not found, return custom line
DATA(ls_default_line) = VALUE #( mt_data[ 15 ] DEFAULT VALUE #( ident = 'Z' text = 'Default is Z' ) ).

 

Hier gibt es für das VALUE Statement noch zwei Erweiterungen:

  • OPTIONAL - Schlägt der Zugriff auf die Tabelle fehl, dann muss keine Ausnahme abgefangen werden und das Ergebnis ist Optional. Bei keinem Treffer wird eine initiale Zeile zurückgeliefert.
  • DEFAULT - Schlägt der erste Zugriff fehl, dann wird eine definierte Zeile zurückgegeben oder wie im Beispiel oben, kann auch die erste Zeile der Tabelle gelesen werden. Schlägt allerdings auch dieser Lesezugriff fehl, muss die Ausnahme abgefangen werden.

 

Komplettes Beispiel

Die gezeigten Beispiele sind teilweise recht umfangreich, hier noch einmal die gesamte ausführbare Klasse mit allen Beispielen:

CLASS zcl_bs_demo_table_deep DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES if_oo_adt_classrun.

  PROTECTED SECTION.
  PRIVATE SECTION.
    TYPES:
      BEGIN OF ts_data,
        ident TYPE c LENGTH 3,
        text  TYPE string,
        date  TYPE d,
      END OF ts_data,
      tt_data TYPE SORTED TABLE OF ts_data
        WITH UNIQUE KEY ident
        WITH NON-UNIQUE SORTED KEY bydate COMPONENTS date,

      BEGIN OF ts_deep,
        deep_key TYPE c LENGTH 3,
        data     TYPE tt_data,
      END OF ts_deep,
      tt_deep TYPE STANDARD TABLE OF ts_deep WITH EMPTY KEY.

    DATA:
      mt_data TYPE tt_data,
      mt_deep TYPE tt_deep.

    METHODS:
      prepare_data,
      spacer
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      read_by_index
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      read_only_field
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      line_not_found
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      read_by_key
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      itab_checks
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      change_itab_line
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      performance_hint
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      filter_values
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      nested_call
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      find_insert_index
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out,
      optimized_value
        IMPORTING
          io_out TYPE REF TO if_oo_adt_classrun_out.
ENDCLASS.


CLASS zcl_bs_demo_table_deep IMPLEMENTATION.
  METHOD if_oo_adt_classrun~main.
    prepare_data( ).
    read_by_index( out ).
    spacer( out ).
    read_only_field( out ).
    spacer( out ).
    read_by_key( out ).
    spacer( out ).
    line_not_found( out ).
    spacer( out ).
    itab_checks( out ).
    spacer( out ).
    find_insert_index( out ).
    spacer( out ).
    change_itab_line( out ).
    spacer( out ).
    performance_hint( out ).
    spacer( out ).
    filter_values( out ).
    spacer( out ).
    nested_call( out ).
    spacer( out ).
    optimized_value( out ).
  ENDMETHOD.


  METHOD prepare_data.
    mt_data = VALUE #(
      ( ident = 'A' text = 'Entry for A' date = '20220125' )
      ( ident = 'B' text = 'Entry for B' date = '20220225' )
      ( ident = 'C' text = 'Entry for C' date = '20220225' )
      ( ident = 'D' text = 'Entry for D' date = '20220125' )
      ( ident = 'K' text = 'Entry for K' date = '20220425' )
      ( ident = 'L' text = 'Entry for L' date = '20220325' )
      ( ident = 'N' text = 'Entry for N' date = '20220525' )
      ( ident = 'O' text = 'Entry for O' date = '20220425' )
    ).

    mt_deep = VALUE #(
      ( deep_key = 'D1' data = mt_data )
      ( deep_key = 'D2' data = mt_data )
      ( deep_key = 'D3' data = mt_data )
    ).
  ENDMETHOD.


  METHOD spacer.
    io_out->write( '---' ).
  ENDMETHOD.


  METHOD read_by_index.
    DATA(ls_index) = mt_data[ 1 ].
    io_out->write( |Read by index 1: { ls_index-ident }| ).

    ls_index = mt_data[ 5 ].
    io_out->write( |Read by index 5: { ls_index-ident }| ).
  ENDMETHOD.


  METHOD read_only_field.
    DATA(ld_text) =  mt_data[ 2 ]-text.
    io_out->write( |Read by index 2 only text: { ld_text }| ).

    DATA(ld_date) =  mt_data[ 5 ]-date.
    io_out->write( |Read by index 5 only date: { ld_date }| ).
  ENDMETHOD.


  METHOD read_by_key.
    DATA(ls_key) = mt_data[ ident = 'B' ].
    io_out->write( |Read by key B: { ls_key-text }| ).

    DATA(ls_date) = mt_data[ KEY bydate date = '20220525' ].
    io_out->write( |Read by sec-key date: { ls_date-text }| ).

    ls_date = mt_data[ KEY bydate COMPONENTS date = '20220325' ].
    io_out->write( |Read by sec-key date with components: { ls_date-text }| ).
  ENDMETHOD.


  METHOD line_not_found.
    TRY.
        DATA(ls_index) =  mt_data[ 10 ].
      CATCH cx_sy_itab_line_not_found.
        io_out->write( |Index 10 not found| ).
    ENDTRY.

    TRY.
        DATA(ls_key) =  mt_data[ ident = 'X' ].
      CATCH cx_sy_itab_line_not_found.
        io_out->write( |Key X not found| ).
    ENDTRY.
  ENDMETHOD.


  METHOD itab_checks.
    IF NOT line_exists( mt_data[ 10 ] ).
      io_out->write( |Index 10 not found| ).
    ENDIF.

    IF NOT line_exists( mt_data[ ident = 'X' ] ).
      io_out->write( |Key X not found| ).
    ENDIF.

    DATA(ld_tabix) = line_index( mt_data[ ident = 'D' ] ).
    io_out->write( |Line-Index for D is: { ld_tabix }| ).
  ENDMETHOD.


  METHOD find_insert_index.
    READ TABLE mt_data TRANSPORTING NO FIELDS
      WITH TABLE KEY ident = 'E'.
    io_out->write( |New entry at position: { sy-tabix }| ).

    IF line_index( mt_data[ ident = 'E' ] ) = 0.
      io_out->write( 'Insert new entry' ).
    ENDIF.
  ENDMETHOD.


  METHOD change_itab_line.
    DATA(lr_data) = REF #( mt_data[ 2 ] ).
    lr_data->date = '20230101'.

    mt_data[ ident = 'N' ]-date = '20230202'.

    io_out->write( |Changed table:| ).
    io_out->write( mt_data ).

    prepare_data( ).
  ENDMETHOD.


  METHOD filter_values.
    DATA:
      lt_filtered_loop TYPE tt_data,
      ld_filter_by     TYPE d VALUE '20220225'.

    LOOP AT mt_data INTO DATA(ls_data) USING KEY bydate WHERE date = ld_filter_by.
      INSERT ls_data INTO TABLE lt_filtered_loop.
    ENDLOOP.
    io_out->write( |Data filtered with loop:| ).
    io_out->write( lt_filtered_loop ).

    DATA(lt_filtered) = FILTER #( mt_data USING KEY bydate WHERE date = ld_filter_by ).
    io_out->write( |Data filtered with FILTER:| ).
    io_out->write( lt_filtered ).
  ENDMETHOD.


  METHOD nested_call.
    DATA(ld_nested_field) = mt_deep[ deep_key = 'D2' ]-data[ 1 ]-text.
    io_out->write( |Nested text from D2/1: { ld_nested_field }| ).
  ENDMETHOD.


  METHOD performance_hint.
    DATA(ld_key) = mt_data[ ident = 'N' ]-ident.
    DATA(ld_longtext) = mt_data[ ident = 'N' ]-text.
    DATA(ld_date) = mt_data[ ident = 'N' ]-date.

    DATA(ls_data) = mt_data[ ident = 'N' ].
    ld_key = ls_data-ident.
    ld_longtext = ls_data-text.
    ld_date = ls_data-date.

    io_out->write( 'For performance check' ).
  ENDMETHOD.


  METHOD optimized_value.
    DATA(ls_optional) = VALUE #( mt_data[ ident = 'E' ] OPTIONAL ).
    io_out->write( |Value of E with Optional:| ).
    io_out->write( ls_optional ).

    DATA(ls_default) = VALUE #( mt_data[ 15 ] DEFAULT mt_data[ 1 ] ).
    io_out->write( |Value of 15 with Default 1:| ).
    io_out->write( ls_default ).

    DATA(ls_default_line) = VALUE #( mt_data[ 15 ] DEFAULT VALUE #( ident = 'Z' text = 'Default is Z' ) ).
    io_out->write( |Value of 15 with Default custom:| ).
    io_out->write( ls_default_line ).
  ENDMETHOD.
ENDCLASS.

 

Dabei werden bei Ausführung die folgenden Daten in die Konsole geschrieben:

 

Fazit

Das neue Statement ist sehr mächtig, sollte allerdings auch an einigen Stellen mit Bedacht eingesetzt werden. Unser Tipp ist weiterhin auf die mittlerweile "alten" Statements zu verzichten und nur noch die Neuen einzusetzen.

 

Quelle:
SAP Dokumentation - Table expressions
SAP Dokumentation - FILTER
SAP Dokumentation - LINE_INDEX


Enthaltene Themen:
Deep DiveArrayREAD TABLEModernes ABAP
Kommentare (0)



Und weiter ...

Bist du zufrieden mit dem Inhalt des Artikels? Wir posten jeden Freitag neuen Content im Bereich ABAP und unregelmäßig in allen anderen Bereichen. Schaue bei unseren Tools und Apps vorbei, diese stellen wir kostenlos zur Verfügung.


ABAP Deep Dive - FOR (Schleifen)

Kategorie - ABAP

Schauen wir uns einmal die FOR Schleife etwas näher an. Wie funktioniert sie? Was muss ich beachten und was kann ich damit tun?

14.04.2023

ABAP Deep Dive - VALUE

Kategorie - ABAP

In diesem Artikel wollen wir uns noch einmal das Value Statement in allen Ausprägungen anschauen und wie du es in deiner täglichen Arbeit nutzen kannst.

11.11.2022

ABAP Deep Dive - CORRESPONDING

Kategorie - ABAP

In diesem Artikel einmal etwas mehr über das neue Corresponding Statement und wie man es im Detail einsetzen kann. Dabei schauen wir einmal auf die zusätzlichen Features.

16.09.2022

ABAP - Type Casting

Kategorie - ABAP

Wie kommst du eigentlich an den ursprünglichen Typ einer Klasse bzw. Instanz, wenn diese in einer generischen Tabelle übergeben wird? In diesem Artikel prüfen wir die Möglichkeiten.

16.04.2024

ABAP - RETURN value

Kategorie - ABAP

Nach all den Jahren ist nun endlich der "echte" Return in ABAP angekommen, in diesem Artikel zeigen wir dir, wie der funktioniert und was er kann.

13.02.2024