Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
Andre_Fischer
Product and Topic Expert
Product and Topic Expert


  •  Update October 4h, 2019 :  Replaced if_a4c_rap_query_provider which has been deprecated by if_rap_query_provider


  •  Changed code such that it also runs on the trial version where no backend systems can be called via RFC. By setting the boolean lv_abap_trial to abap_true mock data will be used.

  • Added information that RAP and custom entities are only available on premise as of SAP S/4HANA 1909


Introduction


In this blog I want to show how to build and implement a custom entity ZCE_Product_via_RFC whose query is being implemented via a class zcl_cq_product_via_rfc that reads the data from a remote system via RFC using the function modules

  • BAPI_EPM_PRODUCT_GET_LIST

  • BAPI_EPM_PRODUCT_GET_DETAIL


The class only others one method select that is called for both queries and GET requests for single products.

Please note that RAP and custom entities are only available on premise as of SAP S/4HANA 1909.

Blog series


This blog is part of a blog series about developing a side-by-side Extension for on-premise SAP Systems in SAP Cloud Platform ABAP Environment using RFC communication

BAPI_EPM_PRODUCT_GET_LIST


FUNCTION bapi_epm_product_get_list
IMPORTING
VALUE(max_rows) TYPE bapi_epm_max_rows OPTIONAL
TABLES
headerdata LIKE bapi_epm_product_header OPTIONAL
selparamproductid LIKE bapi_epm_product_id_range OPTIONAL
selparamsuppliernames LIKE bapi_epm_supplier_name_range OPTIONAL
selparamcategories LIKE bapi_epm_product_categ_range OPTIONAL
return LIKE bapiret2 OPTIONAL.

BAPI_EPM_PRODUCT_GET_DETAIL


FUNCTION bapi_epm_product_get_detail
IMPORTING
VALUE(product_id) TYPE bapi_epm_product_id
EXPORTING
VALUE(headerdata) TYPE bapi_epm_product_header
TABLES
conversion_factors LIKE bapi_epm_product_conv_factors OPTIONAL
return LIKE bapiret2 OPTIONAL.

Please note that both function modules need the structure BAPI_EPM_PRODUCT_HEADER.

Step 1: Create a class


We start our implementation by creating a class zcl_cq_product_via_rfc that implements the method select of the interface if_rap_query_provider.

We will continue with the implementation later on.
CLASS zcl_cq_product_via_rfc DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .

PUBLIC SECTION.
INTERFACES if_rap_query_provider.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.



CLASS zcl_cq_product_via_rfc IMPLEMENTATION.
METHOD if_rap_query_provider~select.

ENDMETHOD.

ENDCLASS.

Step 2: Create custom entity


In ABAP in Eclipse we create a custom entity by creating a new Data Definition. In the wizard you can select the default template or start with the template for a custom entity with parameters.


In either case you have to change the coding such that it looks like follows. Please notice that the custom entity does not take any parameters and that we have added the root Statement.
@EndUserText.label: 'product demo data read via rfc from on prem'
@ObjectModel: {
query: {
implementedBy: 'ABAP:zcl_cq_product_via_rfc'
}
}

define root custom entity ZCE_Product_via_RFC
{
...

}

With the Annotation @ObjectModel.query.implementedBy we have to provide the name of the class that has been created in step 1 which implements the select method of the interface if_rap_query_provider.

The tricky part is now the creation of the DDL source code of our custom entity. Since there is no design time support available for this yet I have developed a class that takes the name of the structure BAPI_EPM_PRODUCT_HEADER that you can run via F9.

The source code of the report zcl_rfc_custom_entity_helper is available in my following blog How to generate the DDL source code for custom entities that are implemented by remote function call...

In this case where we want to create the custom entity in the SAP CP ABAP Environment System you have to run the class in the backend system where the RFC function module is being called since the structure BAPI_EPM_PRODUCT_HEADER is not available in the SAP CP ABAP Environment system.

In future versions of SAP S/4HANA it is planned to have the ABAP RESTful programming model available so that in this case the report can be run in the same system.

When running the class via F9 we get the following output in the console.



We can take the code and copy and paste it in the our custom entity.

In addition we will add some UI annotations.
@EndUserText.label: 'product demo data read via rfc from on prem'
@ObjectModel: {
query: {
implementedBy: 'ABAP:zcl_cq_product_via_rfc'
}
}

define root custom entity ZCE_Product_via_RFC
{
@UI.facet : [
{
id : 'Product',
purpose: #STANDARD,
type : #IDENTIFICATION_REFERENCE,
label : 'Product',
position : 10 }
]
// DDL source code for custom entity for BAPI_EPM_PRODUCT_HEADER
// generated on: 20190214 at:142338
@UI : {
lineItem : [{position: 10, importance: #HIGH}],
identification: [{position: 10}],
selectionField: [{position: 10}]
}
key ProductId : abap.char( 10 );
TypeCode : abap.char( 2 );
@UI : {
lineItem : [{position: 20, importance: #HIGH}],
identification: [{position: 20}],
selectionField: [{position: 20}]
}
Category : abap.char( 40 );
@UI : {
lineItem : [{position: 30, importance: #HIGH}],
identification: [{position: 30}]
}
Name : abap.char( 255 );
@UI : {
identification: [{position: 40}]
}
Description : abap.char( 255 );
SupplierId : abap.char( 10 );
SupplierName : abap.char( 80 );
TaxTarifCode : abap.int1;
@Semantics.unitOfMeasure: true
MeasureUnit : abap.unit( 3 );
@Semantics.quantity.unitOfMeasure: 'WeightUnit'
WeightMeasure : abap.quan( 13, 3 );
@Semantics.unitOfMeasure: true
WeightUnit : abap.unit( 3 );
@UI : {
lineItem : [{position: 50, importance: #HIGH}],
identification: [{position: 50}]
}
Price : abap.dec( 23, 4 );
@Semantics.currencyCode: true
CurrencyCode : abap.cuky( 5 );
@Semantics.quantity.unitOfMeasure: 'DimUnit'
Width : abap.quan( 13, 3 );
@Semantics.quantity.unitOfMeasure: 'DimUnit'
Depth : abap.quan( 13, 3 );
@Semantics.quantity.unitOfMeasure: 'DimUnit'
Height : abap.quan( 13, 3 );
@Semantics.unitOfMeasure: true
DimUnit : abap.unit( 3 );
ProductPicUrl : abap.char( 255 );
}

 

Step 3: Implement query


When implementing the query you have to know that the implementation class only offers one method which is called select.

The code first checks if data is being requested or not.
io_request->is_data_requested( ).

Since the same method is used for queries and single selects we have to find out whether a single select has been performed, that means whether a call like the following has been sent by the OData client:

/…/Products('HT-1000')

In this case the table lt_filter_cond will only contain one entry for the key field PRODUCTID. This is checked via the method is_key_filter( ).

Since the structure BAPIRET2 is not yet on the whitelist of released structures I have used the same report mentioned above to generate a type definition ty_bapiret2.

It is important to note that you must return the number of entries found, also if a single request is returned because otherwise no data will be shown in the object page. This is done by the statement:
io_response->set_total_number_of_records( lines( lt_product ) ).

This number must also not exceed the number of entries that have been requested by the client or the number that has been enforced by the fhe framework if no $top and $skip has been used.

The data both for the single read as well as for queries is returned as a internal table lt_return to the framework.
io_response->set_data( lt_product ).

 
CLASS zcl_cq_product_via_rfc DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .

PUBLIC SECTION.
INTERFACES if_rap_query_provider.
PROTECTED SECTION.
PRIVATE SECTION.

METHODS is_key_filter
IMPORTING it_filter_cond TYPE if_rap_query_filter=>tt_name_range_pairs
RETURNING VALUE(rv_is_key_filter) TYPE abap_bool.

METHODS get_orderby_clause
IMPORTING it_sort_elements TYPE if_rap_query_request=>tt_sort_elements
RETURNING VALUE(rv_orderby_string) TYPE string.

ENDCLASS.



CLASS zcl_cq_product_via_rfc IMPLEMENTATION.
METHOD if_rap_query_provider~select.



"variables needed to call BAPI's
DATA lt_product TYPE STANDARD TABLE OF zce_product_via_rfc.
DATA lt_result TYPE STANDARD TABLE OF zce_product_via_rfc.
DATA ls_product TYPE zce_product_via_rfc.

"key for BAPI_GET_DETAIL
TYPES : BEGIN OF product_rfc_key_type,
productid TYPE zce_product_via_rfc-productid,
END OF product_rfc_key_type.
DATA ls_product_rfc_key TYPE product_rfc_key_type.

"select options
DATA lt_filter_ranges_productid TYPE RANGE OF zce_product_via_rfc-productid.
DATA ls_filter_ranges_productid LIKE LINE OF lt_filter_ranges_productid.
DATA lt_filter_ranges_supplier TYPE RANGE OF zce_product_via_rfc-suppliername.
DATA ls_filter_ranges_supplier LIKE LINE OF lt_filter_ranges_supplier.
DATA lt_filter_ranges_category TYPE RANGE OF zce_product_via_rfc-category.
DATA ls_filter_ranges_category LIKE LINE OF lt_filter_ranges_category.

"######################### ABAP source code ################################
" ABAP source code for type definition for BAPIRET2
" generated on: 20190301 at: 165321 in: UIA
TYPES : BEGIN OF ty_bapiret2,
type TYPE c LENGTH 1,
id TYPE c LENGTH 20,
number TYPE n LENGTH 3,
message TYPE c LENGTH 220,
logno TYPE c LENGTH 20,
logmsgno TYPE n LENGTH 6,
messagev1 TYPE c LENGTH 50,
messagev2 TYPE c LENGTH 50,
messagev3 TYPE c LENGTH 50,
messagev4 TYPE c LENGTH 50,
parameter TYPE c LENGTH 32,
row TYPE i,
field TYPE c LENGTH 30,
system TYPE c LENGTH 10,
END OF ty_bapiret2.

"DATA lt_return TYPE STANDARD TABLE OF bapiret2.
DATA lt_return TYPE STANDARD TABLE OF ty_bapiret2.
"variables generic for implementation of custom entity
DATA lv_details_read TYPE abap_bool.
"DATA ls_sel_opt TYPE /iwbep/s_cod_select_option.

* ensure: in case of a single record is requested (e.g. data for a detail page),
* only one record is returned and SET_TOTAL_NUMBER_OF_RECORDS = 1
DATA lv_orderby_string TYPE string.
DATA lv_select_string TYPE string.
"In the trial version we cannot call RFC function module in backend systems
DATA(lv_abap_trial) = abap_true.

IF lv_abap_trial = abap_false.

TRY.
DATA(lo_rfc_dest) = cl_rfc_destination_provider=>create_by_cloud_destination(
i_name = |S4H_ON_PREM_RFC|
i_service_instance_name = |OutboundCommunication| ).
DATA(lv_rfc_dest_name) = lo_rfc_dest->get_destination_name( ).

CATCH cx_rfc_dest_provider_error INTO DATA(lx_dest).

ENDTRY.

ENDIF.

TRY.

IF io_request->is_data_requested( ).

TRY.
"get and add filter
DATA(lt_filter_cond) = io_request->get_filter( )->get_as_ranges( ). " get_filter_conditions( ).

CATCH cx_rap_query_filter_no_range INTO DATA(lx_no_sel_option).

"@todo :
" raise an exception that the filter that has been provided
" cannot be converted into select options
" here we just continue

ENDTRY.

DATA(lv_top) = io_request->get_paging( )->get_page_size( ).
DATA(lv_skip) = io_request->get_paging( )->get_offset( ).
DATA(lt_fields) = io_request->get_requested_elements( ).
DATA(lt_sort) = io_request->get_sort_elements( ).



" $orderby was called
IF lt_sort IS NOT INITIAL.
CLEAR lv_orderby_string.
LOOP AT lt_sort INTO DATA(ls_sort).
IF ls_sort-descending = abap_true.
CONCATENATE lv_orderby_string ls_sort-element_name 'DESCENDING' INTO lv_orderby_string SEPARATED BY space.
ELSE.
CONCATENATE lv_orderby_string ls_sort-element_name 'ASCENDING' INTO lv_orderby_string SEPARATED BY space.
ENDIF.
ENDLOOP.
ELSE.
" lv_orderby_string must not be empty.
lv_orderby_string = 'PRODUCTID'.
ENDIF.

" $select handling
IF lt_fields IS NOT INITIAL.
CONCATENATE LINES OF lt_fields INTO lv_select_string SEPARATED BY ','.
ELSE.
"check coding. If no columns are specified via $select retrieve all columns from the model instead?
lv_select_string = '*'.
ENDIF.

"check if filter condition is for a single read
lv_details_read = is_key_filter( lt_filter_cond ).

"single read
IF lv_details_read = abap_true.

READ TABLE lt_filter_cond WITH KEY name = 'PRODUCTID' INTO DATA(ls_productid_filter_key).
IF sy-subrc = 0 AND lines( ls_productid_filter_key-range ) = 1.
READ TABLE ls_productid_filter_key-range INTO DATA(ls_id_option) INDEX 1.
IF sy-subrc = 0 AND ls_id_option-sign = 'I' AND ls_id_option-option = 'EQ' AND ls_id_option-low IS NOT INITIAL.
"read details for single record in list
ls_product_rfc_key-productid = ls_id_option-low.

IF lv_abap_trial = abap_true.

"fill structure with test data
ls_product = VALUE #( productid = ls_product_rfc_key-productid name = 'Notebook' ).

ELSE.

CALL FUNCTION 'BAPI_EPM_PRODUCT_GET_DETAIL'
DESTINATION lv_rfc_dest_name
EXPORTING
product_id = ls_product_rfc_key
IMPORTING
headerdata = ls_product
TABLES
return = lt_return.


ENDIF.

APPEND ls_product TO lt_product.

ENDIF.
ENDIF.

"the request is a GET_LIST request and a filter has been provided
ELSE .

"-get filter for ProductID
READ TABLE lt_filter_cond WITH KEY name = 'PRODUCTID' INTO DATA(ls_productid_cond).
IF sy-subrc EQ 0.
LOOP AT ls_productid_cond-range INTO DATA(ls_sel_opt_productid).
MOVE-CORRESPONDING ls_sel_opt_productid TO ls_filter_ranges_productid.
INSERT ls_filter_ranges_productid INTO TABLE lt_filter_ranges_productid.
ENDLOOP.
ENDIF.

"-get filter for SUPPLIERNAME
READ TABLE lt_filter_cond WITH KEY name = 'SUPPLIERNAME' INTO DATA(ls_suppliername_cond).
IF sy-subrc EQ 0.
LOOP AT ls_suppliername_cond-range INTO DATA(ls_sel_opt_suppliername).
MOVE-CORRESPONDING ls_sel_opt_suppliername TO ls_filter_ranges_supplier.
INSERT ls_filter_ranges_supplier INTO TABLE lt_filter_ranges_supplier.
ENDLOOP.
ENDIF.

"-get filter for CATEGORY
READ TABLE lt_filter_cond WITH KEY name = 'CATEGORY' INTO DATA(ls_category_cond).
IF sy-subrc EQ 0.
LOOP AT ls_category_cond-range INTO DATA(ls_sel_opt_category).
MOVE-CORRESPONDING ls_sel_opt_category TO ls_filter_ranges_category.
INSERT ls_filter_ranges_category INTO TABLE lt_filter_ranges_category.
ENDLOOP.
ENDIF.

IF lv_abap_trial = abap_true.

"fill table with demo data
lt_product = VALUE #( ( productid = 'HT-1000' name = 'Notebook' )
( productid = 'HT-1001' name = 'Aotebook' )
( productid = 'HT-1002' name = 'Notebook' )
( productid = 'HT-1003' name = 'Notebook' )
( productid = 'HT-1004' name = 'Notebook' )
( productid = 'HT-1005' name = 'Notebook' )
).
ELSE.

CALL FUNCTION 'BAPI_EPM_PRODUCT_GET_LIST'
DESTINATION lv_rfc_dest_name
* EXPORTING
* max_rows =
TABLES
headerdata = lt_product
selparamproductid = lt_filter_ranges_productid
selparamsuppliernames = lt_filter_ranges_supplier
selparamcategories = lt_filter_ranges_category
return = lt_return.

ENDIF.



ENDIF.

"Apply all query options to filter so that also filter options are supported that
"are not available as filter parameters for the RFC function modules being used
"Also ensure that not more elements are returned than have been
"requested by the framework

IF lv_details_read = abap_false.

DATA(dyn_clause) = io_request->get_filter( )->get_as_sql_string( ).

SELECT (lv_select_string) FROM @lt_product AS products
WHERE (dyn_clause)
ORDER BY (lv_orderby_string)
INTO CORRESPONDING FIELDS OF TABLE @lt_result
UP TO @lv_top ROWS
OFFSET @lv_skip .

IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lt_product ).
ENDIF.
io_response->set_data( lt_result ).

ELSE.

io_response->set_total_number_of_records( lines( lt_product ) ).
io_response->set_data( lt_product ).

ENDIF.




ELSE.
"no data has been requested
ENDIF.

"error handling
CATCH cx_rap_query_provider INTO DATA(lx_exc).


ENDTRY.

ENDMETHOD.

METHOD is_key_filter.

"check if the request is a single read
READ TABLE it_filter_cond WITH KEY name = 'PRODUCTID' INTO DATA(ls_productid_filter_key).
IF sy-subrc = 0 AND lines( ls_productid_filter_key-range ) = 1.
READ TABLE ls_productid_filter_key-range INTO DATA(ls_id_option) INDEX 1.
IF sy-subrc = 0 AND ls_id_option-sign = 'I' AND ls_id_option-option = 'EQ' AND ls_id_option-low IS NOT INITIAL.
"read details for single record in list
rv_is_key_filter = abap_true.
ENDIF.
ENDIF.

ENDMETHOD.

METHOD get_orderby_clause.

ENDMETHOD.

ENDCLASS.

 

Step 4: Create Service Definition and Service Binding


We can now create a Service definition
@EndUserText.label: 'Read product demo data via RFC'
define service ZSD_PRODUCT_VIA_RFC {
expose ZCE_Product_via_RFC;
}

 

and a service binding.

 



 

Result


Using the preview functionality



we can see that the app supports filtering as indicated by the @UI annotations.



 
57 Comments
Frank1
Participant
0 Kudos
Hi Michaela,

Thanks a lot for your reply. A completely new RAP data model has good performance, but this cannot solve your original issues "click the download button in the Fiori app, then all corresponding backend data is downloaded into excel, only the displayed records in the Fiori UI are downloaded", right? How to fulfill this customer's requirements? Thank you.
Micha_Reisner
Explorer
Hi Frank,

I think there's a misunderstanding. The problem with the Excel download isn't that "only the displayed records are downloaded." Actually, it doesn't download the displayed records. Instead, it initiates a new backend call to retrieve the complete data before the download. However, there's a caveat: this backend request occurs in packages of 200 records per call. Consequently, the Fiori frontend triggers the oData service for the first 200 records, then for the next 200, and so forth until it receives the entire data set.

The challenge lies in the fact that this mechanism requires backend support for "paging." While this is automatically managed in the case of the CDS view, it becomes an issue when using an ABAP function module. In such cases, the logic within the function module should implement data selection while respecting parameters like $top and $skip. Unfortunately, existing ABAP selections typically don't adhere to this approach.

Regards,

Michaela
Frank1
Participant
Hi Michaela,

Thank you for your explanation.

If the Fiori App is developed with Fiori Element such as list report pattern with OData V4 or V2, then in the app list page, there is "Export Table" button, all the data from the backend can be downloaded into Excel with the export table button,  ONLY ONE BATCH call without first 200 records, and then for next 200, as this is out of box functionality from Fiori Element. From F12, we can see one batch call with get request to retrieve all the backend data with &$skip=0&$top=total number of database as URL get request parameter. So I do not get your point regarding the below reply.

 

However, there's a caveat: this backend request occurs in packages of 200 records per call. Consequently, the Fiori frontend triggers the oData service for the first 200 records, then for the next 200, and so forth until it receives the entire data set.


export table



F12


 
Micha_Reisner
Explorer
0 Kudos

Dear Frank
That's very interesting.
In our case, it behaves as I described above.
An example:
Total of 1024 records.
The Excel download executes 6 batch calls to select these 1024:
1. first 200 records
2. second 200 records


3. next 200 records
4. next 200 records
5. next 200 records
6. last 24 records

 

The behaviour of your app - selecting all at once - was what I was trying to achieve and what my original question was aimed at. But I don't know how to influence the skip/top parameters from the frontend in this way.

 

Any ideas?

 

Frank1
Participant
0 Kudos
Dear Michaela,

Thank you for your detail screenshot, I also tried another Fiori application which its OData V2 exposed by SEGW - SAP Gateway Service Builder, the download button work the same as you said.  And if tried with Fiori app which based on Fiori Element list report pattern with RAP OData V4, the download button work as my last reply.


Best regards,

Frank

top and skip

mpredolim
Participant
0 Kudos

Hi all,
In my current ABAP RAP development, there is only one field that i can´t get from CDS views ( Long Text ), only using API from SAP API HUB (/A_Product('{Product}')/to_ProductBasicText).

I´ve created an ABAP Class in order to read the API payload but now i need know how do i call this custom ABAP class and combine the value from API with current CDS fields please. It is possible?

Thanks in advance,
Mauricio
ABAP RESTful Application Programming Model 
SAP S/4HANA Cloud Public Edition 

0012anirban
Product and Topic Expert
Product and Topic Expert
0 Kudos

@Andre_Fischer : Really amazing blog, thanks for explaining all the technical details.