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.

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
- Lesen der SalesOrders mittels Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, SALESORDERSET_GET_ENTITYSET
- Lesen der SalesOrderItems zu jeder SalesOrder ( Methode /IWBEP/CL_GWSAMPLE_BAS_DPC_EXT, SALESORDERLINEIT_GET_ENTITYSET )
- 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.

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

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.

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
- Ermitteln, wohin expandiert werden soll
- Expand TOBUSINESSPARTNER: Extraktion der BusinessPartnerIds aus den SalesOrders und Selektion der BusinessPartner
- Expand TOLINEITEMS: Extraktion der SalesOrderIds aus den SalesOrders und Selektion der SalesOrderItems
- 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.

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.

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.