Nachdem wir uns im 1. Teil im Allgemeinen mit der SAP BOPF API und im speziellen mit der Implementierung der CRUDQ-Operationen Create, Read und Update beschäftigt haben, folgt nun der 2. Teil mit den restlichen Operationen Delete und Query. Weitere Themen bei der Implementierung gegen die SAP BOPF API sind Assoziationen, Actions (entspricht OData Function Imports) und Ausnahme- und Transaktionsbehandlung.
Das Coding aus dem 1. Teil hat bereits diese Funktionalitäten umfasst. Du kannst ihn hier herunterladen und dann einfach in Deinem System implementieren. Die Unit Test Klasse findest Du hier.
1. DELETE_HEADER
Die delete_header Methode ist einfach zu implementieren. Das geschieht wieder über die uns wohlbekannte Methode modify des Service Managers.
* <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_RL_EPM_SO_DAO->DELETE_HEADER * +-------------------------------------------------------------------------------------------------+ * | [--->] IV_KEY TYPE /BOBF/CONF_KEY * | [!CX!] CX_DEMO_DYN_T100 * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD delete_header. DATA lr_change TYPE REF TO /bobf/if_tra_change. DATA lr_message TYPE REF TO /bobf/if_frw_message. DATA lt_mod TYPE /bobf/t_frw_modification. FIELD-SYMBOLS <ls_mod> TYPE /bobf/s_frw_modification. APPEND INITIAL LINE TO lt_mod ASSIGNING <ls_mod>. <ls_mod>-node = /bobf/if_epm_sales_order_c=>sc_node-root. <ls_mod>-change_mode = /bobf/if_frw_c=>sc_modify_delete. <ls_mod>-key = iv_key. mr_service_mgr->modify( EXPORTING it_modification = lt_mod IMPORTING eo_change = lr_change eo_message = lr_message ). raise_exc_by_bopf_msg( lr_message ). ENDMETHOD.
In der Modification Struktur sind der Knotentyp, der Change_mode sowie der Datenbankschlüssel zu füllen. Ein gute Hilfe, wie die Modification Struktur abhängig vom Szenario zu füllen ist, liefert übrigens die Dokumentation zu /bobf/s_frw_modification.
2. QUERY_HEADER
Die Query_header Methode erwartet für jedes selektierbare Feld eine Range-Tabelle. Auf Seiten der BOPF API benutzen wir die query Methode des Service Managers. Die query Methode erwartet im Parameter it_selection_parameters die Selektionsparameter als eine einzige Range-Tabelle.
Über den Parameter is_query_options können Parameter für das Paging (top, skip, maxRows) sowie Sortieren übergeben werden. Wir halten es hier einfach und benutzen nur maxRows.
Wichtig zu wissen ist, dass die query Methode direkt das Ergebnis des Datenbank SELECT zurückgibt. Es wird also nicht geprüft, ob ein zurückgegebener Datensatz in der aktuellen Transaktion bereits geändert wurde. Dieses Verhalten ist identisch zu persistenten ABAP Klassen und stellt somit keine Überraschung dar.
* <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_RL_EPM_SO_DAO->QUERY_HEADER * +-------------------------------------------------------------------------------------------------+ * | [--->] IV_MAX_ROW TYPE I (default =0) * | [--->] IT_RNG_SO_ID TYPE SABP_T_RANGE_OPTIONS(optional) * | [--->] IT_RNG_SO_STATUS TYPE SABP_T_RANGE_OPTIONS(optional) * | [--->] IT_RNG_LCHG_DATE_TIME TYPE SABP_T_RANGE_OPTIONS(optional) * | [<---] ET_HEADER TYPE /BOBF/T_EPM_SO_ROOT * | [!CX!] CX_DEMO_DYN_T100 * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD QUERY_HEADER. DATA ls_query_options TYpe /bobf/s_frw_query_options. DATA lt_query_selparam TYPE /bobf/t_frw_query_selparam. field-SYMBOLS <ls_query_selparam> type /BOBF/S_FRW_QUERY_SELPARAM. DATA lr_message TYPE REF TO /bobf/if_frw_message. ls_query_options-maximum_rows = iv_max_row. * Spezifischen Range SalesOrderId in die allgemeine Range Tabelle lt_query_selparam übernehmen LOOP AT it_rng_so_id ASSIGNING FIELD-SYMBOL(<ls_rng_so_id>). APPEND INITIAL LINE TO lt_query_selparam ASSIGNING <ls_query_selparam>. MOVE-CORRESPONDING <ls_rng_so_id> TO <ls_query_selparam>. <ls_query_selparam>-attribute_name = /bobf/if_epm_sales_order_c=>sc_node_attribute-root-so_id. ENDLOOP. * Spezifischen Range SalesOrder Status in die allgemeine Range Tabelle lt_query_selparam übernehmen LOOP AT it_rng_so_status ASSIGNING FIELD-SYMBOL(<ls_rng_so_status>). APPEND INITIAL LINE TO lt_query_selparam ASSIGNING <ls_query_selparam>. MOVE-CORRESPONDING <ls_rng_so_status> TO <ls_query_selparam>. <ls_query_selparam>-attribute_name = /bobf/if_epm_sales_order_c=>sc_node_attribute-root-so_status. ENDLOOP. * Spezifischen Range SalesOrder Change DateTime in die allgemeine Range Tabelle lt_query_selparam übernehmen LOOP AT it_rng_lchg_date_time ASSIGNING FIELD-SYMBOL(<ls_rng_lchg_date_time>). APPEND INITIAL LINE TO lt_query_selparam ASSIGNING <ls_query_selparam>. MOVE-CORRESPONDING <ls_rng_lchg_date_time> TO <ls_query_selparam>. <ls_query_selparam>-attribute_name = /bobf/if_epm_sales_order_c=>sc_node_attribute-root-lchg_date_time. ENDLOOP. mr_service_mgr->query( EXPORTING iv_query_key = /bobf/if_epm_sales_order_c=>sc_query-root-select_by_elements it_selection_parameters = lt_query_selparam is_query_options = ls_query_options iv_fill_data = abap_true IMPORTING eo_message = lr_message et_data = et_header ). raise_exc_by_bopf_msg( lr_message ). ENDMETHOD.
3. QUERY_ITEM_BY_HEADER_KEY
Eine Assoziation verknüpft BOPF Knotenelemente und wird datenbankseitig durch einen Fremdschlüssel abgebildet. Wir wollen hier die auf der Root-Knoten Salesorder Header definierte Assoziation ITEM benutzen, um ausgehend von einem Header Key die zugehörigen Salesorder Items zu lesen.
Die Methode query_item_by_header_key erhält den Schlüssel eines Salesorder Header und liest dann über die Assoziation Root->Item die Salesorder Items. Typtechnisch werden die Items in einer interen Tabelle von Kombi-Strukturen zurückgegeben (Tabellentyp /BOBF/T_EPM_SO_ITEM).
* <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_RL_EPM_SO_DAO->QUERY_ITEM_BY_HEADER_KEY * +-------------------------------------------------------------------------------------------------+ * | [--->] IV_HEADER_KEY TYPE /BOBF/CONF_KEY * | [<---] ET_ITEM TYPE /BOBF/T_EPM_SO_ITEM * | [!CX!] CX_DEMO_DYN_T100 * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD query_item_by_header_key. DATA lr_message TYPE REF TO /bobf/if_frw_message. mr_service_mgr->retrieve_by_association( EXPORTING iv_node_key = /bobf/if_epm_sales_order_c=>sc_node-root "Source node it_key = VALUE #( ( key = iv_header_key ) ) iv_association = /bobf/if_epm_sales_order_c=>sc_association-root-item iv_fill_data = abap_true IMPORTING eo_message = lr_message et_data = et_item ). raise_exc_by_bopf_msg( lr_message ). ENDMETHOD.
4. Action CONFIRM_HEADER
Mit der Implementierung der CRUDQ-Operationen sind wir jetzt durch. Eine BOPF Action entspricht dem Funtion Import im OData Kontext. Wir wollen jetzt die Action CONFIRM definiert auf Header Ebene anbinden. Die Implementierung der Action in Klasse /BOBF/CL_EPM_SO_A_CHGE_STATUS ist schlicht: Das Datenbankfeld so_status wird ohne weitere Prüfung auf C gesetzt.
Die Action CONFIRM erwartet keine Import- und Exportparameter. Auf Seite der BOPF API rufen wir deshalb auf dem Service Manager die Methode do_action ohne weitere Parameter nur mit dem Schlüssel der Salesorder auf.
* <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_RL_EPM_SO_DAO->CONFIRM_HEADER * +-------------------------------------------------------------------------------------------------+ * | [--->] IV_KEY TYPE /BOBF/CONF_KEY * | [!CX!] CX_DEMO_DYN_T100 * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD confirm_header. DATA lr_change TYPE REF TO /bobf/if_tra_change. DATA lr_message TYPE REF TO /bobf/if_frw_message. mr_service_mgr->do_action( EXPORTING iv_act_key = /bobf/if_epm_sales_order_c=>sc_action-root-confirm it_key = VALUE #( ( key = iv_key ) ) * Action Confirm hat keine Import und Exportparameter * is_parameters = IMPORTING eo_change = lr_change eo_message = lr_message ). raise_exc_by_bopf_msg( lr_message ). ENDMETHOD.
5. Ausnahmebehandlung
Die Methoden der BOPF API deklarieren keine Ausnahmen. Die Ausnahmebehandlung muss auf Seiten des Aufrufers durch Auswertung der Exportparameter eo_change und eo_message erfolgen. Über eo_change (Interface /BOBF/IF_TRA_CHANGE) lässt sich feststellen, ob es fehlgeschlagene Änderungen gab.
Im Parameter eo_message wird ein (manchmal) Message Container mit Nachrichten zurückgegeben. Um nicht nach jedem BOPF API Aufruf die Ausnahmebehandlung erneut zu implementieren, habe ich die Methode RAISE_EXC_BY_BOPF_MSG implementiert. Der Aufrufer unserer DAO-Klasse soll im Fehlerfall eine klassenbasierte Ausnahme erhalten. Ich verwende hier die Demo Ausnahmeklasse CX_DEMO_DYN_T100, welche eine T100-Nachricht kapselt. In einem konkreten Anwendungsfall würde man eine Ausnahmeklasse analog zu CX_DEMO_DYN_T100 implementieren. Die Methode RAISE_EXC_BY_BOPF_MSG prüft, ob Fehlernachrichten vorliegen. Wenn ja, wird die 1. Fehlernachricht verwendet, um, die T100-Ausnahme zu erzeugen.
* <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Private Method ZCL_RL_EPM_SO_DAO->RAISE_EXC_BY_BOPF_MSG * +-------------------------------------------------------------------------------------------------+ * | [--->] IR_MESSAGE TYPE REF TO /BOBF/IF_FRW_MESSAGE * | [!CX!] CX_DEMO_DYN_T100 * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD RAISE_EXC_BY_BOPF_MSG. DATA lt_message TYPE /bobf/t_frw_message_k. DATA lv_exc_msg(200) TYPE c. IF ir_message IS BOUND AND ir_message->check( ) EQ abap_true. ir_message->get_messages( EXPORTING iv_severity = /bobf/cm_frw=>co_severity_error IMPORTING et_message = lt_message ). lv_exc_msg = lt_message[ 1 ]-message->get_text( ). * &1&2&3&4 MESSAGE e241(/BOBF/COM_GENERATOR) WITH lv_exc_msg+0(50) lv_exc_msg+50(50) lv_exc_msg+100(50) lv_exc_msg+150(50) INTO DATA(lv_sy_msg). raise_exc_by_sy( ). ENDIF. ENDMETHOD.
Für eine korrekte Ausnahmebehandlung reicht es nicht aus, nur auf die Parameter eo_change und eo_message zu schauen. Beispiele sind die read-Methoden READ_HEADER_BY_KEY und READ_HEADER_BY_SO_ID unserer DAO-Klasse, wo wir mittels der Service Manager Retrieve-Methode einen Datensatz lesen. Wird dieser nicht gefunden, wird keine Fehlernachricht gesetzt. Wir müssen deshalb hier prüfen, ob 1 Datensatz gefunden wurde.
mr_service_mgr->retrieve( EXPORTING iv_node_key = /bobf/if_epm_sales_order_c=>sc_node-root it_key = VALUE #( ( key = iv_key ) ) IMPORTING eo_message = lr_message et_data = lt_header ). raise_exc_by_bopf_msg( lr_message ). IF lines( lt_header ) = 0. raise_exc_by_msg( |Header { iv_key } nicht gefunden| ). ENDIF.
Für diesen Fall gibt es die Methode RAISE_EXC_BY_MSG. Sie erwartet einen String und wirft die T100-Ausnahme.
* <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Private Method ZCL_RL_EPM_SO_DAO->RAISE_EXC_BY_MSG * +-------------------------------------------------------------------------------------------------+ * | [--->] IV_MSG TYPE CLIKE * | [!CX!] CX_DEMO_DYN_T100 * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD raise_exc_by_msg. DATA lv_msg(200) TYPE c. lv_msg = iv_msg. * &1&2&3&4 MESSAGE e241(/bobf/com_generator) WITH lv_msg+0(50) lv_msg+50(50) lv_msg+100(50) lv_msg+150(50) INTO DATA(lv_sy_msg). raise_exc_by_sy( ). ENDMETHOD.
Als 3. Möglichkeit eine T100-Ausnahme zu werfen, habe ich die Methode RAISE_EXC_BY_SY implementiert. Die Methode nimmt die T100-Nachricht aus den SY-Feldern. Diese Form von Ausnahmebehandlung brauchen wir, wenn wir Funktionsbausteine mit klassischer Ausnahmebehandlung aufrufen. In unserem Beispiel ist der Funktionsbaustein NUMBER_GET_NEXT, mit dem wir eine neue Salesorder Id ziehen.
* <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Private Method ZCL_RL_EPM_SO_DAO->RAISE_EXC_BY_SY * +-------------------------------------------------------------------------------------------------+ * | [!CX!] CX_DEMO_DYN_T100 * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD raise_exc_by_sy. RAISE EXCEPTION TYPE cx_demo_dyn_t100 MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4. ENDMETHOD.
6. Transaktionsbehandlung
In der DAO-Klasse benutzen wir den Service Manager, um Datensätze zu lesen und zu schreiben. In der Praxis werde wir mehr als 1 DAO-Klasse haben, um z.B. die Zugriffe auf Items und Notes zu implementieren. Die Transaktionssteuerung sollte deshalb zentral außerhalb der DAO-Klassen erfolgen. In diesem Blog machen wir das in der setup-Methode der Unit-Test Klasse.
METHOD setup. CREATE OBJECT mr_epm_so_dao. mr_transaction_mgr = /bobf/cl_tra_trans_mgr_factory=>get_transaction_manager( ). * Änderungen von der letzten Testmethode zurücksetzen. Ansonsten scheitern die * Testmethoden, weil diese auf der gleichen Salesorder rumändern mr_transaction_mgr->cleanup( ). ENDMETHOD.
Über /bobf/cl_tra_trans_mgr_factory=>get_transaction_manager( ) holen wir uns den den BOPF Transaction Manager. Das Speichern triggern wir in der Testmethode über die Methode save. Beim Commit work führt das BOPF Framework den generischen Verbucherbaustein /BOBF/CL_DAC_UPDATE aus, der pro zu ändernder Tabelle einmal aufgerufen wird. Der folgende Screenshot zeigt das Speichern im Debugger für die Unit Testmethode READ_UPDATE_HEADER. Standardmäßig verbucht der Transaction Manager synchron als local update task. Dies kann über den save-Parameter IV_TRANSACTION_PATTERN auf asynchrone Verbuchung umgestellt werden.
7. SAP BOPF API Fazit
BOPF ist ein wichtiger Teil des ABAP Programming Model for SAP Fiori. BOPF steckt hinter den ObjectModel Annotations einer CDS View. Es gibt einer CDS View die Fähigkeit, nicht nur Daten zu lesen, sondern auch zu schreiben. Die in diesem Blog beschriebene BOPF API benötigt Du, wenn Du diese Geschäftsobjekte mittels Validations und Determinations erweiterst. Ein weiterer Anwendungsfall ist die Programmierung gegen die von SAP mit S/4HANA ausgelieferten Geschäftsobjekte.
Die BOPF API ist ein bißchen tricky in ihrer Benutzung. Mit diesem Blog hoffe ich, dass ein bisschen klarer gemacht zu haben. Aufsetzend auf den von SAP ausgelieferten EPM Geschäftsobjekten lässt sich schnell das Know-How in der BOPF API als auch in BOPF im allgemeinen aufbauen.
Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.