Das Business Object Processing Framework (BOPF) ist ein Teil des ABAP Programming Model for SAP Fiori. BOPF steckt hinter den ObjectModel Annotations einer CDS View. BOPF gibt einer CDS View die Fähigkeit, nicht nur Daten zu lesen, sondern auch zu schreiben. In diesem Blog beschäftigen wir uns mit der SAP BOPF API. Wir implementieren CRUDQ Operationen auf dem ausgelieferten Demo Geschäftsobjekt /BOBF/EPM_SALES_ORDER und nutzen dabei die SAP BOPF API. In der SAP Doku ist das nur sehr theoretisch beschrieben. Deshalb dieser Blog, um das mal dem konkreten Beispiel EPM Salesorder auszuprobieren.
Die Relevanz von BOPF wird auch durch die 500 implementierten BOPF-Geschäftsobjekte in einem S/4HANA-System (siehe Transaktion BOBX) unterstrichen. In einem ECC6 EHP8 gibt es nur knapp 60 BOPF-Geschäftsobjekte.
Den Code kannst Du hier herunterladen und dann einfach in Deinem System implementieren. Die Unit Test Klasse findest Du hier.
1. SAP BOPF Demo Geschäftsobjekt /BOBF/EPM_SALES_ORDER
Die folgende Abbildung zeigt das Demo Geschäftsobjekt /BOBF/EPM_SALES_ORDER (Transaktion BOBX). Es besteht aus dem Root Knoten Salesorder Header (Datenbanktabelle /BOPF/D_SO_ROOT) und den Knoten für Items und Notes. Das einem Item zugeordnete Produkt ist im BOPF-Geschäftsobjekt /BOBF/EPM_PRODUCT implementiert. In diesem Blog arbeiten wir im nur mit dem Header.
Als Entwicklungsumgebung für BOPF kann man alternativ auch Eclipse ABAP Development Tools (ADT) verwenden. Die ganzen BOB*-Transaktionen sind also mittelfristig obsolet.
2. SAP BOPF API
Die BOPF API ist ziemlich schmal. Für die Implementierung der CRUDQ-Methoden benutzen wir den Service Manager /BOBF/IF_TRA_SERVICE_MANAGER. Wir werden hierfür die Methoden RETRIEVE, MODIFY und QUERY benutzen.
Für die Transaktionssteuerung nutzen wir den Transaction Manager /BOBF/IF_TRA_TRANSACTION_MGR. In unserem sehr einfachen Fall brauchen wir nur die Methoden SAVE und CLEANUP.
Wir werden noch sehen, dass die Programmierung gegen die BOPF API ein bisschen fummelig ist. Aus diesem Grund kapseln wir die Logik in eine Data Access Object Klasse. Die DAO Klasse stellt die CRUDQ-Methoden bereit und bietet dem Aufrufer eine vereinfachte API. Als Aufrufer implementieren wir hier eine Unit Test Klasse. In den folgenden Abschnitten gehen wir die Implementierung der CRUDQ-Methoden im Detail durch.
3. CREATE_HEADER
Die Methode create_header bekommt eine Struktur und speichert diese über die Service Manager Methode MODIFY.
* <SIGNATURE>---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_RL_EPM_SO_DAO->CREATE_HEADER * +-------------------------------------------------------------------------------------------------+ * | [--->] IS_HEADER TYPE /BOBF/S_EPM_SO_ROOT * | [<---] EV_KEY TYPE /BOBF/CONF_KEY * | [<---] EV_SO_ID TYPE /BOBF/EPM_SO_ID * | [!CX!] CX_DEMO_DYN_T100 * +--------------------------------------------------------------------------------------</SIGNATURE> METHOD create_header. DATA lt_mod TYPE /bobf/t_frw_modification. FIELD-SYMBOLS <ls_mod> TYPE /bobf/s_frw_modification. DATA lr_change TYPE REF TO /bobf/if_tra_change. DATA lr_message TYPE REF TO /bobf/if_frw_message. DATA lr_so_root TYPE REF TO /bobf/s_epm_so_root. DATA ls_so_root TYPE /bobf/s_epm_so_root. * Key erzeugen MOVE-CORRESPONDING is_header TO ls_so_root. ev_key = ls_so_root-key = /bobf/cl_frw_factory=>get_new_key( ). * Fachliche SalesorderId erzeugen * Siehe Transaktion SNUM. Dort ein Intervall anlegen CALL FUNCTION 'NUMBER_GET_NEXT' EXPORTING nr_range_nr = '01' object = '/BOBF/EPM' IMPORTING number = ev_so_id EXCEPTIONS interval_not_found = 1 number_range_not_intern = 2 object_not_found = 3 quantity_is_0 = 4 quantity_is_not_1 = 5 interval_overflow = 6 buffer_overflow = 7 OTHERS = 8. IF sy-subrc <> 0. raise_exc_by_sy( ). ENDIF. ls_so_root-so_id = ev_so_id. GET REFERENCE OF ls_so_root INTO lr_so_root. 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_create. <ls_mod>-key = ls_so_root-key. <ls_mod>-data = lr_so_root. 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.
Die Methode zieht aus dem Nummernkreisobjekt /BOBF/EPM eine Nummer, welche als Salesorder Id verwendet wird. Damit das funktioniert, leg bitte in Transaktion SNUM ein Intervall an.
Zurück zum Code: Als Typisierung der Struktur wird die sogenannte Kombi-Struktur verwendet. Dies ist eine von BOPF generierte Struktur, welche die persistenten Attribute mit den transienten Attributen zusammenfasst. In unserem Fall haben wir nur persistente Attribute.
Felder wie Zeitpunkt und Benutzer der letzten Änderung lassen sich durch das BOPF-Framework automatisch füllen. Hierfür ist die Determination ADMINISTRATIVE_DATA definiert. Die Implementierung der Determination wird in Klasse /BOBF/CL_LIB_D_ADMIN_DATA_TSM geliefert.
Die MODIFY-Methode des Service Managers kann ziemlich viel:
- Create, Update und Delete
- Bearbeitung eines einzelnen Satz oder Massenverarbeitung
- Bearbeiten eines Felds einer Struktur oder für den ganzen Datensatz
Aus diesem Grund braucht die Modify-Methode eine genaue Spezifikation, was zu tun ist. Hier kommt die Modification Struktur /BOBF/S_FRW_MODIFICATION ins Spiel (Programmvariable <ls_mod>). In das Feld node wird der Knotentyp geschrieben. In key kommt der Datenbankschlüssel des Datensatzes rein. Das ist immer eine Guid. Jede durch BOPF gemanagte Tabelle ist datenbankseitig mit dem Guid-Feld DB_KEY als Primärschlüssel angelegt. Die folgende Abbildung zeigt die Datenbanktabelle /BOBF/D_SO_ROOT, in welcher ein Salesorder Header gespeichert wird.
4. READ_HEADER_BY_KEY
Das Lesen eines Datensatzes zu einem Knoten mit dem Primärschlüssel ist einfach. Die zentrale Service Manager Methode retrieve benötigt den node_key und in einer Tabelle die Liste der zu lesenden Datensätze. Hier ist gut zu sehen, dass mittels der Methode retrieve auch eine Tabelle von Headern gelesen werden kann.
* ---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_RL_EPM_SO_DAO->READ_HEADER_BY_KEY * +-------------------------------------------------------------------------------------------------+ * | [--->] IV_KEY TYPE /BOBF/CONF_KEY * | [<---] ES_HEADER TYPE /BOBF/S_EPM_SO_ROOT * | [!CX!] CX_DEMO_DYN_T100 * +-------------------------------------------------------------------------------------- METHOD read_header_by_key. DATA lr_message TYPE REF TO /bobf/if_frw_message. DATA lt_header TYPE /bobf/t_epm_so_root. 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. es_header = lt_header[ 1 ]. ENDMETHOD.
Der Parameter iv_node_key ist ein bißchen irreführend. Es ist eigentlich der Knotentyp. Der Salesorder Header ist hier das Root Geschäftsobjekt. Wir müssen also die Konstante /bobf/if_epm_sales_order_c=>sc_node-root verwenden.
Für jedes BOPF Geschäftsobjekt existiert ein generiertes Konstanten Interface, welches hier /bobf/if_epm_sales_order_c heißt. Dieses Konstanteninterface definiert diverse Konstanten: Actions, Assoziationen, Knotentypen, Actions usw. Hinter einem Großteil der hier definierten Konstanten stecken Guids. Dieses Interface wird uns bei der Programmierung gegen die BOPF API ständig begleiten.
5. READ_HEADER_BY_SO_ID
Guids sind für den Anwender schwer lesbar. Um dem Anwender einen lesbaren Schlüssel zu bieten, bietet BOPF den Alternative Key an. Der Alternative Key am Salesorder Knoten ist das Feld SO_ID, welches wir beim CREATE_HEADER aus dem Nummernkreis gefüllt hatten.
Um einen Datensatz mittels Alternative Key zu lesen, müssen wir zunächst diesen Key in die Guid umwandeln, welche als Datenbank Key benutzt wird. Das geschieht über mr_service_mgr->convert_altern_key. Danach können wir via Methode retrieve den Datensatz lesen. Auch hier ist wieder gut die Nutzung des Konstanten Interface /bobf/if_epm_sales_order_c zu sehen.
* ---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_RL_EPM_SO_DAO->READ_HEADER_BY_SO_ID * +-------------------------------------------------------------------------------------------------+ * | [--->] IV_SO_ID TYPE /BOBF/EPM_SO_ID * | [<---] ES_HEADER TYPE /BOBF/S_EPM_SO_ROOT * | [!CX!] CX_DEMO_DYN_T100 * +-------------------------------------------------------------------------------------- METHOD read_header_by_so_id. DATA lr_message TYPE REF TO /bobf/if_frw_message. DATA lt_header TYPE /bobf/t_epm_so_root. DATA lt_alt_key_so_id TYPE /bobf/t_epm_k_sales_order_id. DATA lt_key TYPE /bobf/t_frw_key. lt_alt_key_so_id = VALUE #( ( iv_so_id ) ). mr_service_mgr->convert_altern_key( EXPORTING iv_node_key = /bobf/if_epm_sales_order_c=>sc_node-root iv_altkey_key = /bobf/if_epm_sales_order_c=>sc_alternative_key-root-sales_order_id it_key = lt_alt_key_so_id iv_check_existence = abap_true IMPORTING et_key = lt_key eo_message = lr_message ). raise_exc_by_bopf_msg( lr_message ). IF lt_key[ 1 ]-key IS INITIAL. raise_exc_by_msg( |Header { iv_so_id } nicht gefunden| ). ENDIF. mr_service_mgr->retrieve( EXPORTING iv_node_key = /bobf/if_epm_sales_order_c=>sc_node-root it_key = lt_key IMPORTING eo_message = lr_message et_data = lt_header ). raise_exc_by_bopf_msg( lr_message ). es_header = lt_header[ 1 ]. ENDMETHOD.
6. Update_header
Die update_header Methode erhält wie die Create Methode die Kombi-Struktur. Als kleine Sonderlocke bauen wir in diese Methode ein, dass der Schlüssel als Gui oder alternativ über den Alternative Key so_id übergeben werden kann. Wir setzen hierbei auf die bereits implementierte Methode read_header_by_so_id auf. Der Code ist ziemlich ähnlich zum Create. Wir müssen also die Modification Struktur /BOBF/S_FRW_MODIFICATION füllen.
* ---------------------------------------------------------------------------------------+ * | Instance Public Method ZCL_RL_EPM_SO_DAO->UPDATE_HEADER * +-------------------------------------------------------------------------------------------------+ * | [--->] IS_HEADER TYPE /BOBF/S_EPM_SO_ROOT * | [!CX!] CX_DEMO_DYN_T100 * +-------------------------------------------------------------------------------------- METHOD update_header. DATA lt_mod TYPE /bobf/t_frw_modification. FIELD-SYMBOLS <ls_mod> TYPE /bobf/s_frw_modification. DATA lr_change TYPE REF TO /bobf/if_tra_change. DATA lr_message TYPE REF TO /bobf/if_frw_message. DATA ls_so_root TYPE /bobf/s_epm_so_root. DATA ls_so_root_pers TYPE /bobf/s_epm_so_root. * Wenn technischer Schlüssel leer, versuch per SO_ID zu lesen IF is_header-key IS INITIAL. CALL METHOD read_header_by_so_id( EXPORTING iv_so_id = is_header-so_id IMPORTING es_header = ls_so_root_pers ). MOVE-CORRESPONDING is_header TO ls_so_root. ls_so_root-key = ls_so_root_pers-key. ls_so_root-root_key = ls_so_root_pers-root_key. ELSE. MOVE-CORRESPONDING is_header TO ls_so_root. ENDIF. 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_update. <ls_mod>-key = ls_so_root-key. * Nur das Feld net_amount updaten. Das geht nicht, weil das Feld net_amount schreibgeschützt ist * <ls_mod>-changed_fields = value #( ( /bobf/if_epm_sales_order_c=>sc_node_attribute-root-net_amount ) ). GET REFERENCE OF ls_so_root INTO <ls_mod>-data. 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.
Über die interne Tabelle changed_fields in der Modification Struktur können wir der BOPF API mitteilen, dass wir nicht alle Attribute updaten wollen, sondern nur bestimmte Attribute. Dabei muss man beachten, dass man nicht jedes Feld updaten kann. Bei der Definition des Geschäftsobjekts kann man die Attributeigenschaften definieren. Hier siehst Du, dass z.B. das Attribut NET_AMOUNT schreibgeschützt ist. Wenn Du im Code den auskommentierten Code für die changed_fields einkommentierst, wird die BOPF API deshalb einen Fehler werden. Lässt Du den Parameter changed_fields weg, werden alle Attribute ohne Schreibschutz aktualisiert.
7. Das war der 1. Teil
Das war der 1. Teil zum Thema BOPF API. Wir haben gesehen, dass BOPF ein mächtiges Framework zur Implementierung von Geschäftsobjekten ist. Die BOPF API ist auf Grund ihrer Generizität (generische Parameter, wenige API-Methoden) bei der Benutzung zunächst nicht ganz einfach. Ich hoffe aber, dass ich mit mit diesem Blogbeitrag ein gute Grundlage hierfür gelegt habe.
Im 2. Teil, der bald folgt, beschäftigen wir uns mit den restlichen CRUDQ-Funktionalitäten wie Delete und Query, Assoziationen, Actions ( entspricht OData Function Imports), Ausnahme- und Transaktionsbehandlung .
Hast du noch Fragen?
Nutze gerne unsere Kommentarfunktion!
Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.