Performanzoptimierung des ABAP OData $Expand

Ein häufig anzutreffendes Performanzproblem bei der klassischen OData Entwicklung in ABAP ohne Nutzung von SADL ist das Lesen von Entitäten mitsamt assozierter Entitäten mittels $expand.

Ein Beispiel ist eine SAPUI5 Worklist App, welche zu den angezeigten SalesOrders weitere Informationen aus dem zugehörigen Business Partner und den SalesOrderItems anzeigt. Das Performanzproblem kann man gut an dem bekannten GWSAMPLE_BASIC oData Service beobachten, welcher ansonsten für viele Features der oData Entwicklung Lösungsansätze liefert.

Performanzuntersuchung des GWSAMPLE_BASIC-Service

Lass uns einmal die Performanz vom GWSAMPLE_BASIC-Service beim Expand untersuchen. Das Lesen von 50 SalesOrders mitsamt BusinessPartner und SalesOrderItems mit der Url /sap/opu/odata/IWBEP/GWSAMPLE_BASIC/SalesOrderSet?$top=50&$expand=ToBusinessPartner,ToLineItems&sap-ds-debug=true braucht auf meinem (zugegeben nicht besonders schnellen) SAP Gateway System rund 867ms.

GWSAMPLE_BASIC Performanz beim Expand

Die Laufzeit wird dominiert durch die Methode /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_EXPANDED_ENTITYSET in der DPC_EXT-Klasse /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT. Die  DPC_EXT-Klasse erbt diese Methode aus dem OData Framework. Diese geerbte Methode geht beim Expand wie folgt vor

  1. Lesen der SalesOrders mittels Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, SALESORDERSET_GET_ENTITYSET
  2. Lesen der SalesOrderItems zu jeder SalesOrder ( Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, SALESORDERLINEIT_GET_ENTITYSET )
  3. Lesen des BusinessPartner zu jeder SalesOrder (Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, BUSINESSPARTNERS_GET_ENTITY )

Das führt beim Lesen von 50 SalesOrders also in Summe zu 101 Datenbankzugriffen. Im Debugger kann man das gut nachvollziehen.

Debuggen des OData Expand

Jeder dieser SQL SELECTs ist nicht teuer. Die Summe alle SQL SELECTs ist jedoch teuer. Ein SQL Trace via Transaktion ST05 zeigt dies.

SQL Trace OData Expand

Lösungsansatz Reimplementierung von /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_EXPANDED_ENTITYSET

Der Lösungsansatz ist die Reduzierung der Datenbankzugriffe von 101 auf 3, indem die assozierten Entitäten in einem Rutsch für alle SalesOrders gelesen werden. Im Folgenden implementieren wir das einmal durch und führen die Messung dann nochmal durch.

Dafür kopiere den Service GWSAMPLE_BASIC mitsamt seiner Implementierung. Wie das geht, steht in diesem Blogbeitrag. Im nächsten Schritt implementieren wir die Methode /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_EXPANDED_ENTITYSET in der DPC_EXT-Klasse.

/IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_EXPANDED_ENTITYSET redefinieren

Die Methode werden bei jedem Expand auf einem EntitySet aufgerufen. Wir müssen also hier prüfen, ob der Expand auf dem SalesOrderSet aufgerufen wird. Wenn ja, lesen wir die SalesOrders und nutzen dabei die vorhandene Implementierung in Methode salesorderset_get_entityset. Danach verzweigen in die Methode exp_entset_salesorder. Wenn der Expand auf einemEntitySet ungleich SalesOrder aufgerufen wird, delegieren wir an die Standardlogik des OData Frameworks,

METHOD /iwbep/if_mgw_appl_srv_runtime~get_expanded_entityset.

    DATA lt_ent_salesorder TYPE zcl_zrlgwsample_basic_mpc=>tt_salesorder.


    IF iv_entity_name = zcl_zrlgwsample_basic_mpc=>gc_salesorder AND
       iv_entity_set_name = |{ zcl_zrlgwsample_basic_mpc=>gc_salesorder }Set| AND
       iv_source_name = zcl_zrlgwsample_basic_mpc=>gc_salesorder.

      salesorderset_get_entityset(
        EXPORTING
          iv_entity_name           = iv_entity_name
          iv_entity_set_name       = iv_entity_set_name
          iv_source_name           = iv_source_name
          it_filter_select_options = it_filter_select_options
          is_paging                = is_paging
          it_key_tab               = it_key_tab
          it_navigation_path       = it_navigation_path
          it_order                 = it_order
          iv_filter_string         = iv_filter_string
          iv_search_string         = iv_search_string
          io_tech_request_context = io_tech_request_context
        IMPORTING
          et_entityset             = lt_ent_salesorder
          es_response_context      = es_response_context  ).

      exp_entset_salesorder(
        EXPORTING
          it_ent_salesorder = lt_ent_salesorder
          ir_expand = io_expand
        IMPORTING
          et_expanded_tech_clauses = et_expanded_tech_clauses
          er_entityset = er_entityset )  .

    ELSE.
      CALL METHOD super->/iwbep/if_mgw_appl_srv_runtime~get_expanded_entityset
        EXPORTING
          iv_entity_name           = iv_entity_name
          iv_entity_set_name       = iv_entity_set_name
          iv_source_name           = iv_source_name
          it_filter_select_options = it_filter_select_options
          it_order                 = it_order
          is_paging                = is_paging
          it_navigation_path       = it_navigation_path
          it_key_tab               = it_key_tab
          iv_filter_string         = iv_filter_string
          iv_search_string         = iv_search_string
          io_expand                = io_expand
          io_tech_request_context  = io_tech_request_context
        IMPORTING
          er_entityset             = er_entityset
          et_expanded_clauses      = et_expanded_clauses
          et_expanded_tech_clauses = et_expanded_tech_clauses
          es_response_context      = es_response_context.
    ENDIF.

  ENDMETHOD.

Implementierung der Expand Logik

Die Methode exp_entset_salesorder sammelt die Daten für den Expand bereitet diese auf. Das Coding ist ein wenig länglich, weil die vorhandene Implementierung in den Methoden bp_get_entityset und soli_get_entityset noch ein paar Tabellen mehr selektiert. Die Logik führt folgende Schritte durch

  1. Ermitteln, wohin expandiert werden soll
  2. Expand TOBUSINESSPARTNER: Extraktion der BusinessPartnerIds aus den SalesOrders und Selektion der BusinessPartner
  3. Expand TOLINEITEMS: Extraktion der SalesOrderIds aus den SalesOrders und Selektion der SalesOrderItems
  4. Abmischen der selektieren Daten in die Expand-Datenstruktur
* ---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZRLGWSAMPLE_BASIC_DPC_EXT->EXP_ENTSET_SALESORDER
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_ENT_SALESORDER              TYPE        ZCL_ZRLGWSAMPLE_BASIC_MPC=>TT_SALESORDER
* | [--->] IR_EXPAND                      TYPE REF TO /IWBEP/IF_MGW_ODATA_EXPAND
* | [<---] ER_ENTITYSET                   TYPE REF TO DATA
* | [<---] ET_EXPANDED_TECH_CLAUSES       TYPE        STRING_TABLE
* +--------------------------------------------------------------------------------------
  METHOD exp_entset_salesorder.
    TYPES:
      BEGIN OF ts_exp_salesorder.
        INCLUDE TYPE zcl_zrlgwsample_basic_mpc=>ts_salesorder.
    TYPES:
      tolineitems       TYPE STANDARD TABLE OF zcl_zrlgwsample_basic_mpc=>ts_salesorderlineitem WITH DEFAULT KEY,
      tobusinesspartner TYPE zcl_zrlgwsample_basic_mpc=>ts_businesspartner,
      END OF ts_exp_salesorder.
    TYPES:
      BEGIN OF ts_soli_helper,
        soli_guid          TYPE snwd_node_key,
        note_guid          TYPE snwd_node_key,
        note_orig_language TYPE spras.
        INCLUDE TYPE zcl_zrlgwsample_basic_mpc=>ts_salesorderlineitem.
    TYPES END OF ts_soli_helper .

    TYPES:
      BEGIN OF ts_bpa_helper.
        INCLUDE TYPE zcl_zrlgwsample_basic_mpc=>ts_businesspartner.
        INCLUDE TYPE /iwbep/s_gws_basic_address.
    TYPES END OF ts_bpa_helper.

    DATA lt_child TYPE /iwbep/if_mgw_odata_expand=>ty_t_node_children.
    DATA ls_child TYPE /iwbep/if_mgw_odata_expand=>ty_s_node_child.
    DATA lt_rng_bpa TYPE /iwbep/t_cod_select_options.
    DATA lt_rng_so_id TYPE /iwbep/t_cod_select_options.
    DATA ls_exp_salesorder TYPE ts_exp_salesorder.
    DATA lt_exp_salesorder TYPE table of ts_exp_salesorder.
    DATA ls_ent_businesspartner TYPE zcl_zrlgwsample_basic_mpc=>ts_businesspartner.
    DATA lt_ent_businesspartner TYPE zcl_zrlgwsample_basic_mpc=>tt_businesspartner.
    DATA lt_ent_salesorderlineitem TYPE zcl_zrlgwsample_basic_mpc=>tt_salesorderlineitem.
    DATA ls_ent_salesorderlineitem TYPE zcl_zrlgwsample_basic_mpc=>ts_salesorderlineitem.
    DATA lt_soli_helper TYPE TABLE OF ts_soli_helper.
    DATA lt_note_texts TYPE STANDARD TABLE OF snwd_texts.
    FIELD-SYMBOLS  TYPE snwd_texts.
    DATA lt_sosl TYPE STANDARD TABLE OF snwd_so_sl WITH NON-UNIQUE SORTED KEY p_key COMPONENTS parent_key.
    DATA lt_bpa_helper TYPE TABLE OF ts_bpa_helper.

    lt_child = ir_expand->get_children( ).

*   Ermitteln, wohin expandiert werden soll
    LOOP AT lt_child INTO ls_child.
      CASE ls_child-tech_nav_prop_name.

*       Expand zum BusinessPartner
        WHEN 'TOBUSINESSPARTNER'.
          APPEND ls_child-tech_nav_prop_name TO et_expanded_tech_clauses.

*         Extraktion der BusinessPartnerIds
          lt_rng_bpa = conv_itab_to_range(
            it_itab = it_ent_salesorder
            iv_fieldname = 'BUYER_ID' ).

          SORT lt_rng_bpa BY low.
          DELETE ADJACENT DUPLICATES FROM lt_rng_bpa COMPARING low.

*         Code adaptiert aus Methode bp_get_entityset: Selektion der BusinessPartner
          SELECT
            b~bp_id
            b~bp_role
            b~company_name
            b~web_address
            b~email_address
            b~phone_number
            b~fax_number
            b~legal_form
            b~currency_code
            b~created_at
            b~changed_at
            a~city
            a~postal_code
            a~street
            a~building
            a~country
            a~address_type
            INTO CORRESPONDING FIELDS OF TABLE lt_bpa_helper
            FROM ( snwd_bpa AS b
                   INNER JOIN snwd_ad AS a ON a~node_key = b~address_guid )
            WHERE bp_id IN lt_rng_bpa.

          SORT lt_ent_businesspartner BY bp_id.

*       Expand zu den SalesOrderItems
        WHEN 'TOLINEITEMS'.
          APPEND ls_child-tech_nav_prop_name TO et_expanded_tech_clauses.

*         Extraktion der SalesOrderIds
          lt_rng_so_id = conv_itab_to_range(
            it_itab = it_ent_salesorder
            iv_fieldname = 'SO_ID' ).

*         Code adaptiert aus Methode SOLI_GET_ENTITYSET: Selektion SalesOrderItems
          SELECT
            s~so_id
            i~node_key AS soli_guid
            i~so_item_pos
            i~note_guid
            i~currency_code
            i~gross_amount
            i~net_amount
            i~tax_amount
            p~product_id
            tk~original_langu AS note_orig_language
            INTO CORRESPONDING FIELDS OF TABLE lt_soli_helper
            FROM ( ( ( snwd_so_i AS i
                       INNER JOIN snwd_so AS s ON s~node_key = i~parent_key )
                       INNER JOIN snwd_pd AS p ON p~node_key = i~product_guid )
                       LEFT OUTER JOIN snwd_text_key AS tk ON tk~node_key = i~note_guid )
            WHERE so_id IN lt_rng_so_id.

          SORT lt_soli_helper BY so_id.

*         Weitere Infos dazu selektieren: Texte und Schedule Lines
          IF lines( lt_soli_helper ) > 0.
            SELECT * FROM snwd_texts INTO TABLE lt_note_texts
              FOR ALL ENTRIES IN lt_soli_helper
                WHERE parent_key EQ lt_soli_helper-note_guid.

            SELECT * FROM snwd_so_sl INTO TABLE lt_sosl
              FOR ALL ENTRIES IN lt_soli_helper
                WHERE parent_key EQ lt_soli_helper-soli_guid.

            SORT lt_note_texts BY parent_key language.
            SORT lt_sosl BY parent_key delivery_date.

            LOOP AT lt_soli_helper ASSIGNING FIELD-SYMBOL().
              CLEAR ls_ent_salesorderlineitem.
              MOVE-CORRESPONDING  TO ls_ent_salesorderlineitem.

*             Text ermitteln
              UNASSIGN .
              READ TABLE lt_note_texts ASSIGNING  WITH KEY
                  parent_key = -note_guid
                  language   = sy-langu BINARY SEARCH.
              IF sy-subrc NE 0.
                READ TABLE lt_note_texts ASSIGNING  WITH KEY
                  parent_key = -note_guid
                  language   = -note_orig_language BINARY SEARCH.
              ENDIF.

              IF  IS ASSIGNED.
                ls_ent_salesorderlineitem-note          = -text.
                ls_ent_salesorderlineitem-note_language = -language.
              ENDIF.

*             Ermittlung schedule line
              LOOP AT lt_sosl ASSIGNING FIELD-SYMBOL() USING KEY p_key WHERE parent_key = -soli_guid.
                IF -delivery_date IS INITIAL.
                  ls_ent_salesorderlineitem-delivery_date = -delivery_date.
                ENDIF.
                ls_ent_salesorderlineitem-quantity      = ls_ent_salesorderlineitem-quantity + -quantity.
                ls_ent_salesorderlineitem-quantity_unit = -quantity_unit.
              ENDLOOP.

              APPEND ls_ent_salesorderlineitem TO lt_ent_salesorderlineitem.

            ENDLOOP.
          ENDIF.
      ENDCASE.
    ENDLOOP.

*   Expanded Struktur aufbauen
    LOOP AT it_ent_salesorder ASSIGNING FIELD-SYMBOL().
      CLEAR ls_exp_salesorder.

      MOVE-CORRESPONDING  TO ls_exp_salesorder.

*     Business Partner dazumischen
      IF lines( lt_bpa_helper ) > 0.
        READ TABLE lt_bpa_helper ASSIGNING FIELD-SYMBOL()
          WITH KEY  bp_id = -buyer_id BINARY SEARCH.
        IF sy-subrc = 0.
          MOVE-CORRESPONDING  TO ls_exp_salesorder-tobusinesspartner.
          MOVE-CORRESPONDING  TO ls_exp_salesorder-tobusinesspartner-address.
        ENDIF.
      ENDIF.

*     SalesorderItems dazumischen
      IF lines( lt_ent_salesorderlineitem ) > 0.
        READ TABLE lt_ent_salesorderlineitem WITH KEY so_id = -so_id
          BINARY SEARCH TRANSPORTING NO FIELDS.
        IF sy-subrc = 0.
          LOOP AT lt_ent_salesorderlineitem ASSIGNING FIELD-SYMBOL() FROM sy-tabix.
            IF -so_id <> -so_id.
              EXIT.
            ENDIF.

            APPEND  TO ls_exp_salesorder-tolineitems.
          ENDLOOP.
        ENDIF.
      ENDIF.

      APPEND ls_exp_salesorder TO lt_exp_salesorder.
    ENDLOOP.

    copy_data_to_ref(
         EXPORTING
           is_data = lt_exp_salesorder
         CHANGING
           cr_data = er_entityset ).

  ENDMETHOD.

* ---------------------------------------------------------------------------------------+
* | Instance Private Method ZCL_ZRLGWSAMPLE_BASIC_DPC_EXT->CONV_ITAB_TO_RANGE
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_ITAB                        TYPE        ANY TABLE
* | [--->] IV_FIELDNAME                   TYPE        FIELDNAME(optional)
* | [<-()] RT_RNG                         TYPE        /IWBEP/T_COD_SELECT_OPTIONS
* +--------------------------------------------------------------------------------------
  METHOD conv_itab_to_range.
    DATA ls_rng TYPE /iwbep/s_cod_select_option.
    FIELD-SYMBOLS  TYPE any.
    FIELD-SYMBOLS  TYPE any.

    ls_rng-sign = 'I'.
    ls_rng-option = 'EQ'.

    LOOP AT it_itab ASSIGNING .
      IF iv_fieldname IS INITIAL.
        ASSIGN  TO .
      ELSE.
        ASSIGN COMPONENT iv_fieldname OF STRUCTURE  TO .
      ENDIF.

      IF  IS NOT INITIAL.
        ls_rng-low = .
        APPEND ls_rng TO rt_rng.
      ENDIF.
    ENDLOOP.

  ENDMETHOD.

Aufbau der Expand-Datenstruktur

Das Coding ist straight forward. Interessant ist noch, wie die Expand-Datenstruktur aussehen muss, damit das OData unsere expandierten Entities auch verarbeiten kann. Die Methode exp_entset_salesorder liefert eine Tabelle mit dieser Datenstruktur zurück. Die Expand-Datenstruktur ist im Code als lokaler Typ ts_exp_salesorder definiert. Man sieht hier gut, das der Gateway Service Builder bei der Generierung für jede Entity einen zugehörigen Typ in der MPC-Klasse erzeugt.

TYPES:
      BEGIN OF ts_exp_salesorder.
        INCLUDE TYPE zcl_zrlgwsample_basic_mpc=>ts_salesorder.
    TYPES:
      tolineitems       TYPE STANDARD TABLE OF zcl_zrlgwsample_basic_mpc=>ts_salesorderlineitem WITH DEFAULT KEY,
      tobusinesspartner TYPE zcl_zrlgwsample_basic_mpc=>ts_businesspartner,
      END OF ts_exp_salesorder.

Die Datenstruktur ist tief. Sie hat neben der Feldern aus der SalesOrder die Felder tolineitems und tobusinesspartner, welche durch eine Struktur bzw. Tabelle typisiert sind. Diese Feldnamen korrespondieren zu den Navigationseigenschaften im Service Builder.

OData Expand Datenstruktur

Performanzmessung der neuen Logik durchführen

Lass uns mal schauen, was die neue Logik auf der Performanzseite bringt. Hierfür starte den Gateway Client und messe die Url /sap/opu/odata/SAP/ZRLGWSAMPLE_BASIC_SRV/SalesOrderSet?$top=50&$expand=ToBusinessPartner,ToLineItems&sap-ds-debug=true durch.

Die Laufzeit ist von 867ms auf 427ms gesunken. Das ist eine Halbierung! In der Realität wird das noch drastischer sein, weil in den SalesOrder Tabellen des EPM-Modell (Tabellen SNWD_*) nur wenige Tausend Datensätze enthalten sind.

Performanz nach der Expand-Optimierung

Hast du noch Fragen zu OData Performanz oder zu anderen Themen?

Nutze gerne unsere Kommentarfunktion oder schreib mir direkt eine eMail

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

 

Über den Autor

Rüdiger Lühr

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

68 + = 78