Prerequisite: proficiency in the ABAP Framework RESTful Application Programming Model.
In our previous article, we created a RAP object using an external API.
As a reminder, we’re using the PRODUCT entity provided by Northwind. In our case, we can retrieve the products present in the Northwind API by performing a GET. Our RAP object allows us to display the products available in the Northwind API within a Fiori application.
We’re now going to extend this API.
To do this, we’ll :
- Add a “Comment” field within the RAP object, which can be modified.
- Create an “Orders” child entity to represent the orders associated with these products ( Part II )
All the code containing Part I and Part II is available on our Github.
All data from these extensions will be saved within BTP, ABAP Environment. This means that comments created by users will be saved within our environment, as will orders. When calling the API to retrieve products, there will also be a search within our database for comments entered and orders created in relation to these products.
This technique also works well with S/4 Hana OData (side-by-side extensibility). Extending these OData into the SAP BTP environment, ABAP is a strategic approach to enriching your existing OData services without modifying the underlying backend systems (here, S/4 Hana).
This method enables the seamless integration of new data attributes, making it possible to extend functionality without impacting the core backend, thus preserving system stability and ensuring compliance with existing processes.
1/ Creating a database table
@EndUserText.label : 'Test product extension'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table ztest_prod {
key client : abap.clnt not null;
key product_id : abap.int4 not null;
comment_product : abap.char(100) not null;
}
The product_id field links to the data returned by the API.
The comment_product field will store comments on the product.
2/ Modifying the custom CDS
- Add the “comment_product” field to the CDS.
- Add UI annotations that will allow you to navigate through each product and display it in an Object Page.
@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 }
]
@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);
}
- Modify the ztest_crt_proxy class to select the comment corresponding to the product line if it exists
- Also, add filtering processing, because when we navigate by clicking on a particular product, we need to make an api call that filters on that particular product.
CLASS ztest_crt_proxy IMPLEMENTATION.
METHOD if_rap_query_provider~select.
DATA(top) = io_request->get_paging( )->get_page_size( ).
DATA(skip) = io_request->get_paging( )->get_offset( ).
DATA(requested_fields) = io_request->get_requested_elements( ).
DATA(sort_order) = io_request->get_sort_elements( ).
data(filter) = io_request->get_filter( ).
DATA:
ls_entity_key TYPE ztest_northwind=>tys_alphabetical_list_of_produ,
ls_business_data TYPE ztest_northwind=>tys_alphabetical_list_of_produ,
lt_business_data TYPE TABLE OF ztest_northwind=>tys_alphabetical_list_of_produ,
lo_http_client TYPE REF TO if_web_http_client,
lo_client_proxy TYPE REF TO /iwbep/if_cp_client_proxy.
TRY.
" Create http client
DATA(lo_destination) = cl_http_destination_provider=>create_by_url( 'https://services.odata.org' ).
lo_http_client = cl_web_http_client_manager=>create_by_http_destination( lo_destination ).
lo_client_proxy = /iwbep/cl_cp_factory_remote=>create_v2_remote_proxy(
EXPORTING
is_proxy_model_key = VALUE #( repository_id = 'DEFAULT'
proxy_model_id = 'ZTEST_NORTHWIND'
proxy_model_version = '0001' )
io_http_client = lo_http_client
iv_relative_service_root = '/v2/northwind/northwind.svc' ).
ASSERT lo_http_client IS BOUND.
DATA: lo_read_list_request TYPE REF TO /iwbep/if_cp_request_read_list,
lo_entity_list_resource TYPE REF TO /iwbep/if_cp_resource_list,
lo_read_list_response TYPE REF TO /iwbep/if_cp_response_read_lst.
lo_entity_list_resource = lo_client_proxy->create_resource_for_entity_set( 'PRODUCTS' ).
lo_read_list_request = lo_entity_list_resource->create_request_for_read( ).
lo_read_list_response = lo_read_list_request->execute( ).
lo_read_list_response->get_business_data( IMPORTING et_business_data = lt_business_data ).
CATCH /iwbep/cx_cp_remote INTO DATA(lx_remote).
CATCH /iwbep/cx_gateway INTO DATA(lx_gateway).
CATCH cx_web_http_client_error INTO DATA(lx_web_http_client_error).
RAISE SHORTDUMP lx_web_http_client_error.
ENDTRY.
TYPES : tty_products TYPE STANDARD TABLE OF ztest_cust_northwind WITH EMPTY KEY.
DATA(lt_data) = VALUE tty_products(
FOR ls_data IN lt_business_data
( product_id = ls_data-product_id
category_id = ls_data-category_id
product_name = ls_data-product_name
quantity_per_unit = ls_data-quantity_per_unit
supplier_id = ls_data-supplier_id
unit_price = ls_data-unit_price
units_on_order = ls_data-units_on_order
comment_product = me->get_comment( ls_data-product_id ) )
).
data(lt_range) = filter->get_as_ranges( ).
if lt_range IS NOT INITIAL.
data(lv_product_id) = lt_range[ 1 ]-range[ 1 ]-low.
DELETE lt_data WHERE product_id <> lv_product_id.
ENDIF.
io_response->set_total_number_of_records( lines( lt_data ) ).
io_response->set_data( lt_data ).
ENDMETHOD.
METHOD get_comment.
SELECT SINGLE comment_product FROM ztest_prod WHERE product_id = @id INTO @DATA(lv_comment).
rv_comments = lv_comment.
ENDMETHOD.
ENDCLASS.
3/ Creating a behavior definition
Since we’re dealing with a Custom CDS Entity, we need to manage behavior in an “unmanaged” way.
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;
}
Here, we only activate the “update” function, and only on the “comment_product” field (the other fields are read-only), as this is the only field we want to modify.
4/ Implementing developer behavior
The behavior implementation will be developed in the zbp_test_cust_northwind class.
- We start by developing the class needed to handle the interaction phase.
CLASS lhc_ZTEST_CUST_NORTHWIND DEFINITION INHERITING FROM cl_abap_behavior_handler.
PUBLIC SECTION.
TYPES : BEGIN OF ty_buffer_prod,
pid TYPE abp_behv_pid,
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_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 update FOR MODIFY
IMPORTING entities FOR UPDATE ztest_cust_northwind.
ENDCLASS.
CLASS lhc_ZTEST_CUST_NORTHWIND IMPLEMENTATION.
METHOD read.
* To implement
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_order.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
The READ method ( To be implemented – not used here – , allows you to read information on entities that are still in the buffer – in the interaction phase – and not yet saved – not yet passed through the save phase – )
The UPDATE method creates an entry in the buffer (here, the mt_buffer_upd_prod variable), which will be saved later in the save phase.
We also add the updated data to the CHANGING PARAMETER mapped so that it is correctly returned in the API response and can be displayed in the Fiori application.
- We then implement the class needed to process the save phase.
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_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.
* To implement
ENDMETHOD.
ENDCLASS.
The SAVE method handles UPDATE requests for our entity (i.e. adding/modifying a comment).
This is when we update the database.
5/ Exposing the RAP business object via an API
Creating Service Definition and Service Binding
This API can then be used in a Fiori application, the result of which is shown below:
Note:
In our case, we can retrieve the products present in the Northwind API by performing a GET. However, it is not possible to perform a POST to create a new product, a PATCH to update a product or a DELETE to delete a product, although this could be the case for other APIs.
If these methods had been available, it would have been sufficient to declare and implement the various methods needed to handle creation/update/deletion requests and call the Northwind API in the RAP object with POST/PATCH/DELETE.
Conclusion
We’ve seen how to extend an external API by adding an additional field that will be saved within our environment.
In Part 2, we’ll make this RAP object more complex by adding a brand-new child entity to manage the orders associated with the products.