Prérequis : Maitrise du Framework ABAP RESTful Application Programming Model.
Dans notre précédent article, nous avions créé un objet RAP en utilisant une API externe.
Pour rappel, nous utilisons l’entité PRODUCT mis a disposition par Northwind. Dans notre cas, nous pouvons récupérer les produits présents dans l’API Northwind en effectuant un GET. Notre objet RAP nous permet d’afficher les produits disponibles dans l’API Northwind au sein d’une application Fiori.
Nous allons désormais étendre cette API.
Pour cela nous allons :
- Ajouter un champs « Commentaire » au sein de l’objet RAP qu’il sera possible de modifier
- Créer une entité fille « Commandes » qui représentera les commandes associées a ces produits ( Partie II )
Tout le code contenant la Partie I et la Partie II est disponible sur notre Github.
L’ensemble des données issues de ces extensions seront sauvegardées au sein de BTP, ABAP Environment. Cela veut dire que les commentaires créés par les utilisateurs seront sauvegardes au sein de notre environnement, ainsi que les commandes. Lors de l’appel a l’API pour retrouver les produits, il y aura également une recherche au sein de notre base de données pour retrouver les commentaires saisis et les commandes créées par rapport a ces produits.
Cette technique convient également très bien aux OData de S/4 Hana ( side-by-side extensibility ). Étendre ces OData dans l’environnement SAP BTP, ABAP est une approche stratégique pour enrichir vos services OData existants sans modifier les systèmes backend ( Ici, S/4 Hana) sous-jacents.
Cette méthode permet une intégration fluide de nouveaux attributs de données, offrant ainsi la possibilité d’étendre les fonctionnalités sans impacter le cœur du backend, ce qui préserve la stabilité du système et garantit la conformité avec les processus existants.
1/ Créer une table de base de données
@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;
}
Le champs product_id permettra de faire le lien avec les données retournées de l’API.
Le champs comment_product stockera les commentaires sur le produit.
2/ Modifier la custom CDS
- Ajouter le champs « comment_product » dans la CDS.
- Ajouter les annotations UI qui permettrons de naviguer dans chaque produit et de les afficher dans un 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);
}
- Modifier la classe ztest_crt_proxy pour sélectionner le commentaire correspondant a la ligne du produit s’il existe
- Également, ajouter le traitement du filtrage, car lorsqu’on naviguera en cliquant sur un produit en particulier, nous avons besoin d’effectuer un appel api qui filtre sur ce produit en particulier.
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/ Créer un behavior definition
Comme nous sommes dans le cas d’une Custom CDS Entity, alors il faut gérer le behavior de manière « unmanaged ».
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;
}
Ici on active uniquement le « update » et seulement sur le champs « comment_product » ( les autres champs sont en Read only ) car c’est le seul champs qu’on veut autoriser modifier.
4/ Developper le Behavior Implementation
Le behavior implementation sera développé dans la classe zbp_test_cust_northwind
- On commence par développer la classe nécessaire au traitement de la phase d’intéraction.
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.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
La methode READ ( À implémenter – ici on ne s’en sert pas – , doit vous permettre de venir lire les informations sur les entités qui sont encore dans le buffer – dans la phase d’interaction – et pas encore sauvegardées – pas encore passées par la phase de sauvegarde – )
La methode UPDATE vient créer une entrées dans le buffer ( Ici la variable mt_buffer_upd_prod ) et sera sauvegardées plus tard dans la phase de sauvegarde.
On logue également les données updatées dans le CHANGING PARAMETER mapped.
- On implémente ensuite la classe nécessaire au traitement de la phase de sauvegarde.
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.
Dans la methode SAVE, on vient traiter les demandes d’UPDATE de notre entité ( donc l’ajout/modification d’un commentaire ).
C’est a ce moment qu’on vient mettre a jour la base de données.
5/ Exposer le business object RAP via une API
Créer le Service Definition et le Service Binding
On pourra ensuite utiliser cette API dans une application Fiori dont voici le résultat :
Note :
Dans notre cas, nous pouvons récupérer les produits présents dans l’API Northwind en effectuant un GET. Mais il n’est pas possible d’effectuer de POST pour créer de nouveaux produit, de PATCH pour mettre a jour un produit ou de DELETE pour supprimer un produit, mais cela pourrait être le cas pour d’autres API.
Si ces méthodes avaient été disponibles il aurait suffit de déclarer et d’implémenter les différentes méthodes nécessaires pour traiter les demandes de création/mise a jour/suppression et d’appeler l’API Northwind dans l’objet RAP avec des POST/PATCH/DELETE.
Conclusion
Nous avons vu comment étendre une API externe en y ajoutons un champs supplementaire qui sera sauvegarde au sein de notre environement.
Dans la Partie 2, nous complexifierons cet objet RAP en y ajoutons une toute nouvelle entité fille qui gerèra les commandes associées aux produits.