Extending an API via BTP, ABAP environment – PART II

This article is the last in a series about the Northwind API.

Prerequisites: Mastery of the ABAP Framework RESTful Application Programming Model.

As a reminder, the objective was as follows:

  1. Add a “Comment” field in the RAP object that can be modified (Part I)
  2. Créer une entité fille “Commandes” qui représentera les commandes associées a ces produits

In our last article, we added a field to the PRODUCT entity.

Here, we’re going to create a new entity, which will correspond to orders linked to products available in the Northwind API. We will then link these 2 entities to enable users to easily create orders associated with these products via a Fiori application.

All the code containing Part I and Part II is available on our Github.

1/ Create a database table and the associated CDS

@EndUserText.label : 'Order Northwind'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table ztest_order {

  key client     : abap.clnt not null;
  key product_id : abap.int4 not null;
  key order_id   : abap.int4 not null;
  quantity       : abap.int4;

}

The product_id field will link to data returned from the API.
The order_id and quantity fields will store data relating to orders (order_id will be an id automatically generated by the RAP Business Object).

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Order association product Northwind'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
    serviceQuality: #X,
    sizeCategory: #S,
    dataClass: #MIXED
}
define view entity ZTEST_CUST_NORTHWIND_ORDER
  as select from ztest_order
  association to parent ZTEST_CUST_NORTHWIND as _Product on $projection.product_id = _Product.product_id
{
      @EndUserText.label: 'Product ID'
      @UI.facet: [ {
        label: 'General Information',
        id: 'GeneralInfo',
        purpose: #STANDARD,
        position: 10 ,
        type: #IDENTIFICATION_REFERENCE
      } ]
      @UI.identification: [ {
        position: 10
      } ]
      @UI.lineItem: [ {
        position: 10
      } ]
      @UI.selectionField: [ {
        position: 10
      } ]
  key product_id,
      @EndUserText.label: 'Order ID'
      @UI.identification: [ {
        position: 20
      } ]
      @UI.lineItem: [ {
        position: 20
      } ]
      @UI.selectionField: [ {
        position: 20
      } ]
  key order_id,
      @EndUserText.label: 'Quantity'
      @UI.identification: [ {
        position: 30
      } ]
      @UI.lineItem: [ {
        position: 30
      } ]
      @UI.selectionField: [ {
        position: 30
      } ]
      quantity,
      _Product
}

As we want to link the orders to the associated products, we add a to parent association between the CDS that represents the orders (ZTEST_CUST_NORTHWIND_ORDER) and the one that represents the products (ZTEST_CUST_NORTHWIND).

What’s more, as this is a CDS entity and not a custom CDS entity, there’s no need to manually manage data retrieval. It’s all done by the framework.

2/ Modify the custom CDS product

Simply add the composition to ZTEST_CUST_NORTHWIND_ORDER

@EndUserText.label: 'test custom CDS Northwind'
@ObjectModel.query.implementedBy: 'ABAP:ZTEST_CRT_PROXY'
@UI.headerInfo: { typeName: 'Product', typeNamePlural: 'Products', title: { type: #STANDARD, value: 'product_id' }, description.value : 'product_name' }
define root custom entity ZTEST_CUST_NORTHWIND 
{ 
@UI.facet: [
{ id: 'Product', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE, position: 10 },
{ id: 'Order', purpose: #STANDARD, type: #LINEITEM_REFERENCE, label: 'Orders', position: 20, targetElement: 'orders'  }
]
@UI.lineItem: [{ position: 10 }]
@UI.identification: [{ position: 10 }]
@EndUserText.label: 'Product ID'
  key   product_id : int4;
@UI.lineItem: [{ position: 20 }]
@UI.identification: [{ position: 20 }]
@EndUserText.label: 'Product Name'
        product_name : abap.char( 100 );
@UI.lineItem: [{ position: 30 }]  
@UI.identification: [{ position: 30 }]      
@EndUserText.label: 'Supplier ID'
        supplier_id : int4;
@UI.lineItem: [{ position: 40 }]
@UI.identification: [{ position: 40 }]
@EndUserText.label: 'Category ID'
        category_id : int4;
@UI.lineItem: [{ position: 50 }]
@UI.identification: [{ position: 50 }]
@EndUserText.label: 'Quantity Per Unit'
        quantity_per_unit : abap.char( 100 );
@UI.lineItem: [{ position: 60 }]
@UI.identification: [{ position: 60 }]
@EndUserText.label: 'Unit Price'
        unit_price : abap.dec( 16, 0 );
@UI.lineItem: [{ position: 70}]
@UI.identification: [{ position: 70 }]
@EndUserText.label: 'Units In Stock'
        units_in_stock : int2;
@UI.lineItem: [{ position: 80, label: 'Units On Order' }]
@UI.identification: [{ position: 80 }]
@EndUserText.label: 'Units On Order'
        units_on_order : int2; 
@UI.lineItem: [{ position: 90, label: 'Comment' }]
@UI.identification: [{ position: 90 }]
@EndUserText.label: 'Comment'
        comment_product : abap.char(100);               
        orders : composition [0..*] of ZTEST_CUST_NORTHWIND_ORDER;
}

3/ Modifying Behavior Definition

unmanaged implementation in class zbp_test_cust_northwind unique;
//strict ( 2 ); //Uncomment this line in order to enable strict mode 2. The strict mode has two variants (strict(1), strict(2)) and is prerequisite to be future proof regarding syntax and to be able to release your BO.

define behavior for ZTEST_CUST_NORTHWIND //alias <alias_name>
late numbering
//lock master
//authorization master ( instance )
//etag master <field_name>
{
  //create;
  update;
  //delete;
  field ( readonly ) product_id, category_id, product_name, quantity_per_unit, unit_price, units_in_stock, units_on_order, supplier_id;
  association orders { create; }
}

define behavior for ZTEST_CUST_NORTHWIND_ORDER //alias <alias_name>
//late numbering
//lock dependent by _Product
//authorization dependent by _Product
//etag master <field_name>
{
//  create;
  update;
  delete;
  field ( readonly ) product_id, order_id;
  association _Product;

  mapping for ztest_order
  {
    product_id = product_id;
    order_id = order_id;
    quantity = quantity;
  }
}

We add the information linked to the ORDER entity.

As we want to be able to create, modify or delete an order, we add CREATE-BY-ASSOCIATION (here, we only want to authorize the creation of an order from an article – you need to access the article to create the order – we could have added CREATE if we wanted to authorize the creation of an order without starting from an article, here it’s just a choice to show how we can interact between the 2 entities), UPDATE, DELETE.

Finally, we note that late numbering is enabled, allowing us to manage the automatic generation of the ORDER id (order_id) in the “ADJUST_NUMBER” method.

4/ Modifying Behavior Implementation

We will modify the behavior implementation developed in the zbp_test_cust_northwind class.

CLASS lhc_ZTEST_CUST_NORTHWIND DEFINITION INHERITING FROM cl_abap_behavior_handler.

  PUBLIC SECTION.

    TYPES : BEGIN OF ty_buffer,
              pid        TYPE abp_behv_pid,
              product_ID TYPE ztest_cust_northwind_order-product_ID,
              order_id   TYPE ztest_cust_northwind_order-order_id,
              quantity   TYPE ztest_cust_northwind_order-quantity,
            END OF ty_buffer,
            BEGIN OF ty_buffer_prod,
              product_ID      TYPE ztest_cust_northwind-product_ID,
              comment_product TYPE ztest_cust_northwind-comment_product,
            END OF ty_buffer_prod.
    CLASS-DATA : mt_buffer_create TYPE STANDARD TABLE OF ty_buffer WITH EMPTY KEY.
    CLASS-DATA : mt_buffer_update TYPE STANDARD TABLE OF ty_buffer WITH EMPTY KEY.
    CLASS-DATA : mt_buffer_delete TYPE STANDARD TABLE OF ty_buffer WITH EMPTY KEY.
    CLASS-DATA : mt_buffer_upd_prod TYPE STANDARD TABLE OF ty_buffer_prod WITH EMPTY KEY.
  PRIVATE SECTION.

    METHODS read FOR READ
      IMPORTING keys FOR READ ztest_cust_northwind RESULT result.

    METHODS rba_Orders FOR READ
      IMPORTING keys_rba FOR READ ztest_cust_northwind\Orders FULL result_requested RESULT result LINK association_links.

    METHODS cba_Orders FOR MODIFY
      IMPORTING entities_cba FOR CREATE ztest_cust_northwind\Orders.
    METHODS update FOR MODIFY
      IMPORTING entities FOR UPDATE ztest_cust_northwind.

ENDCLASS.

CLASS lhc_ZTEST_CUST_NORTHWIND IMPLEMENTATION.

  METHOD read.

* To implement

  ENDMETHOD.

  METHOD rba_Orders.

* To implement

  ENDMETHOD.

  METHOD cba_Orders.
    DATA n TYPE i VALUE 1.

    LOOP AT entities_cba INTO DATA(ls_entity).
      LOOP AT ls_entity-%target INTO DATA(ls_target).

        APPEND VALUE #(
         pid = n
         product_id = ls_target-product_id
         quantity = ls_target-quantity
        ) TO lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_create.

        APPEND VALUE #(
        %cid = ls_target-%cid
        %pid = n
        product_id = ls_target-product_id ) TO mapped-ztest_cust_northwind_order.
        n = n + 1.

      ENDLOOP.
    ENDLOOP.

  ENDMETHOD.

  METHOD update.
    LOOP AT entities INTO DATA(ls_entity).
      APPEND VALUE #(
       product_id = ls_entity-product_id
       comment_product = ls_entity-comment_product
      ) TO lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_upd_prod.

      APPEND VALUE #(
      product_id = ls_entity-product_id ) TO mapped-ztest_cust_northwind.

    ENDLOOP.
  ENDMETHOD.

ENDCLASS.

CLASS lhc_ZTEST_CUST_NORTHWIND_ORDER DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.

    DATA : lt_buffer TYPE TABLE OF ztest_order.

    METHODS update FOR MODIFY
      IMPORTING entities FOR UPDATE ztest_cust_northwind_order.

    METHODS delete FOR MODIFY
      IMPORTING keys FOR DELETE ztest_cust_northwind_order.

    METHODS read FOR READ
      IMPORTING keys FOR READ ztest_cust_northwind_order RESULT result.

    METHODS rba_Product FOR READ
      IMPORTING keys_rba FOR READ ztest_cust_northwind_order\_Product FULL result_requested RESULT result LINK association_links.


ENDCLASS.

CLASS lhc_ZTEST_CUST_NORTHWIND_ORDER IMPLEMENTATION.

  METHOD update.
    LOOP AT entities INTO DATA(ls_entity).
      APPEND VALUE #(
       product_id = ls_entity-product_id
       order_id = ls_entity-order_id
       quantity = ls_entity-quantity
      ) TO lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_update.

      APPEND VALUE #(
      product_id = ls_entity-product_id
      order_id = ls_entity-order_id ) TO mapped-ztest_cust_northwind_order.
    ENDLOOP.
  ENDMETHOD.

  METHOD delete.
    LOOP AT keys INTO DATA(ls_key).
      APPEND VALUE #(
       product_id = ls_key-product_id
       order_id = ls_key-order_id
      ) TO lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_delete.

      APPEND VALUE #(
      product_id = ls_key-product_id
      order_id = ls_key-order_id ) TO mapped-ztest_cust_northwind_order.
    ENDLOOP.
  ENDMETHOD.


  METHOD read.

    DATA : orders TYPE TABLE OF ztest_order.

    LOOP AT keys INTO DATA(ls_key).

      SELECT SINGLE * FROM ztest_order
      WHERE product_id = @ls_key-product_id AND order_id = @ls_key-order_id
      INTO @DATA(ls_result).

      IF sy-subrc <> 0.
        READ TABLE lt_buffer INTO ls_result WITH KEY product_id = ls_key-product_id order_id = ls_key-order_id.
      ENDIF.

      IF ls_result IS NOT INITIAL.
        APPEND CORRESPONDING #( ls_result MAPPING TO ENTITY ) TO result.
      ENDIF.

    ENDLOOP.

    IF result IS INITIAL.
      APPEND VALUE #(
      product_id = ls_key-product_id
      order_id = ls_key-order_id
      %fail-cause = if_abap_behv=>cause-not_found ) TO failed-ztest_cust_northwind_order.


    ENDIF.

  ENDMETHOD.

  METHOD rba_Product.

  ENDMETHOD.

ENDCLASS.

CLASS lsc_ZTEST_CUST_NORTHWIND DEFINITION INHERITING FROM cl_abap_behavior_saver.
  PROTECTED SECTION.

    METHODS finalize REDEFINITION.

    METHODS check_before_save REDEFINITION.

    METHODS save REDEFINITION.

    METHODS cleanup REDEFINITION.

    METHODS cleanup_finalize REDEFINITION.
    METHODS adjust_numbers REDEFINITION.

ENDCLASS.

CLASS lsc_ZTEST_CUST_NORTHWIND IMPLEMENTATION.

  METHOD finalize.

* To implement

  ENDMETHOD.

  METHOD check_before_save.

* To implement

  ENDMETHOD.

  METHOD save.

    LOOP AT lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_create INTO DATA(ls_buffer_create).
      DATA(ls_data) = VALUE ztest_order( product_id = ls_buffer_create-product_id order_id = ls_buffer_create-order_id quantity = ls_buffer_create-quantity ).
      MODIFY ztest_order FROM @ls_data.
    ENDLOOP.

    LOOP AT lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_update INTO DATA(ls_buffer_update).
      ls_data = VALUE ztest_order( product_id = ls_buffer_update-product_id order_id = ls_buffer_update-order_id quantity = ls_buffer_update-quantity ).
      MODIFY ztest_order FROM @ls_data.
    ENDLOOP.

    LOOP AT lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_delete INTO DATA(ls_buffer_delete).
      DATA(ls_key) = VALUE ztest_order( product_id = ls_buffer_delete-product_id order_id = ls_buffer_delete-order_id ).
      DELETE ztest_order FROM @ls_key.
    ENDLOOP.

    LOOP AT lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_upd_prod INTO DATA(ls_buffer_upd_prod).
      DATA(ls_data_prod) = VALUE ztest_prod( product_id = ls_buffer_upd_prod-product_id comment_product =  ls_buffer_upd_prod-comment_product ).
      MODIFY ztest_prod FROM @ls_data_prod.
    ENDLOOP.

  ENDMETHOD.

  METHOD cleanup.

* To implement

  ENDMETHOD.

  METHOD cleanup_finalize.

* To implement

  ENDMETHOD.

  METHOD adjust_numbers.

    LOOP AT lhc_ZTEST_CUST_NORTHWIND=>mt_buffer_create ASSIGNING FIELD-SYMBOL(<ls_buffer>).

      SELECT MAX( order_id )
        FROM ztest_cust_northwind_order
        WHERE product_id = @<ls_buffer>-product_id
        INTO @DATA(lv_orderid).

      lv_orderid = lv_orderid + 1.

      APPEND VALUE #(
      %pid = <ls_buffer>-pid
      %tmp = VALUE #( product_id = <ls_buffer>-product_id )
      product_id = <ls_buffer>-product_id
      order_id = lv_orderid ) TO mapped-ztest_cust_northwind_order.

      <ls_buffer>-order_id = lv_orderid.

    ENDLOOP.

  ENDMETHOD.

ENDCLASS.

1: Implement UPDATE and DELETE methods

We create entries in the buffer (here, the mt_buffer_update and mt_buffer_delete variables), which will be saved later in the save phase.

Modified instances are also logged in the MAPPED parameter.

2: Implement the CREATE-BY-ASSOCITION method (cba_Orders)

Here, entries are created in the buffer ( mt_buffer_create ) and saved later in the save phase.

We also log the instances created in parameters MAPPED

%CID will return the data created in the API response

%PID is necessary because there are currently no unique transactional keys, as the order_id field will be determined later in the ADJUST_NUMBER method.

More on implicit parameters here

  • %CID will return the data created in the API response (linking the data entered to the generated %PID).
  • %PID is required because there are currently no unique transactional keys, as the order_id field will be determined later in the ADJUST_NUMBER method.
  • Plus d’infos sur les implicit parameters ici

3: Implement the ADJUST_NUMBER method

We read the rows created and assign a unique id to each order_id linked to a product.

We then MAPPE this new ID by linking it to the %PID previously created with the new order_id in the MAPPED parameter.

4 : Update the SAVE method

The SAVE method handles CREATE-BY-ASSOCIATION/UPDATE/DELETE requests from our entity (i.e. adding/modifying/deleting an order).

This is when we update the database.

5/ Exposing the RAP business object via an API

Create the Service Definition and Service Binding

This API can then be used in a Fiori application, the result of which is shown below: