BOPF Business Object Builder

SAP BOPF API Teil 1

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.

Business Object Builder ABAP

Als Entwicklungsumgebung für BOPF kann man alternativ auch Eclipse ABAP Development Tools (ADT) verwenden. Die ganzen BOB*-Transaktionen sind also mittelfristig obsolet.

Business-Object-Builder-Eclipse

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.

BOPF API Service Manager

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.

BOPF API Transaction Manager

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.

Nummernkreisobjekt /BOBF/EPM     Nummernkreisobjekt /BOBF/EPM - Intervall

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.

BOPF API Kombi Struktur

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.

BOPF API Datenbank Tabelle

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.

BOPF API Const Interface

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.

BOPF API Alternative Key

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.

BOPF Knotenattribute

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.

Ü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>

97 − = 90