RAP - Custom Entity
In this article we look at a special form of entities in the RAP model and how we can use them.
Table of contents
In this article we will take a closer look at the custom entity, how the data is retrieved and what we can use these entities for. We will also go into the differences to the classic entity and examine them in more detail.
Introduction
The custom entity differs from a CDS entity in terms of data acquisition. Here you have the option of implementing ABAP code using a query class and determining and transferring the data there. In the figure below you can see the classic model of how the data comes from a table on the left-hand side. On the right side the custom entity with a query class and data acquisition via a consumption model:
The data can be derived locally or loaded via an on-premise interface. Since you are working in ABAP here, you have a free choice of methodology.
Custom Entity
In the first step, let's create a new entity. The goal is to provide the data from on-premises that we have linked in this article. To do this, we define a new CDS entity in our demo package without a corresponding reference object.
After confirming the transport, we can still select the appropriate template, but here there are only "Extend custom entity" and "Custom entity with parameters", so we choose the second type:
In the next step, we can then specify the CDS view and adopt the elements from the abstract entity "ZBS_RAP_COMPANYNAMES". The entity was automatically generated using the Service Consumption Model. We do not need the "_VC" elements (Value Control). Then we have to add an annotation for the query class, the view now looks like this:
@EndUserText.label: 'Custom entity for company names'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_BS_DEMO_CUST_CNAME_QUERY'
define custom entity ZBS_I_RAPCustomEntityCNames
{
key CompanyName : abap.char( 60 );
Branch : abap.char( 50 );
CompanyDescription : abap.char( 255 );
}
However, before you can activate the CDS view, you must create the query class, otherwise the compiler will throw an error. The empty class looks like this and implements the "IF_RAP_QUERY_PROVIDER" interface:
CLASS zcl_bs_demo_cust_cname_query DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES:
if_rap_query_provider.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_bs_demo_cust_cname_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
ENDMETHOD.
ENDCLASS.
Data acquisition
For data retrieval we now have to implement the SELECT method, so let's look at the interface:
Structure
There is a REQUEST object that passes the request context to the class. Information such as paging, requested fields and restrictions are provided here. The RESPONSE object then awaits the result of this query. In between we can now implement our logic.
Proxy Object
As in the previous examples, we now need access to the interface. In the first step, we have to create a proxy, which we do again via a destination, and then open a read request in the next step:
DATA(lo_request) = get_proxy( )->create_resource_for_entity_set( c_entity )->create_request_for_read( ).
Disassemble request
In the next step, we build the request towards on-premises by extracting all requested elements and properties from IO_REQUEST. The following code for this:
DATA(lt_filter_condition) = io_request->get_filter( )->get_as_ranges( ).
DATA(lt_requested_elements) = io_request->get_requested_elements( ).
DATA(lt_sort_elements) = io_request->get_sort_elements( ).
DATA(ld_skip) = io_request->get_paging( )->get_offset( ).
DATA(ld_top) = io_request->get_paging( )->get_page_size( ).
DATA(ld_is_data_requested) = io_request->is_data_requested( ).
DATA(ld_is_count_requested) = io_request->is_total_numb_of_rec_requested( ).
We now accept the elements step by step and transfer them to the request. In the first step, we take over all filters and set up access via the filter factory, which we then transfer to the request:
" Build filter condition
DATA(lo_filter_factory) = lo_request->create_filter_factory( ).
LOOP AT lt_filter_condition INTO DATA(ls_filter_condition).
DATA(lo_filter_node) = lo_filter_factory->create_by_range(
iv_property_path = ls_filter_condition-name
it_range = ls_filter_condition-range
).
IF lo_root_filter_node IS INITIAL.
lo_root_filter_node = lo_filter_node.
ELSE.
lo_root_filter_node = lo_root_filter_node->and( lo_filter_node ).
ENDIF.
ENDLOOP.
" Set filter
IF lo_root_filter_node IS NOT INITIAL.
lo_request->set_filter( lo_root_filter_node ).
ENDIF.
In the query, only certain fields that the table fills can be queried, then we pass them to the appropriate method. If an OData Service contains many fields in the entity, it is always a good idea to limit the number of fields:
" Set requested fields
IF lt_requested_elements IS NOT INITIAL.
lo_request->set_select_properties( CORRESPONDING #( lt_requested_elements ) ).
ENDIF.
We can also provide a sorting if this has been requested. Is especially important when we work with a paging on the data, otherwise wrong results can be returned.
" Set Sorting
IF lt_sort_elements IS NOT INITIAL.
lo_request->set_orderby( CORRESPONDING #( lt_sort_elements MAPPING property_path = element_name ) ).
ENDIF.
Is data requested at all or is only the number of data records required in the backend? To do this, the two flags are queried and the methods in the query are supplied. When data is retrieved, we also pass the corresponding TOP/SKIP values from the original query:
" Data requested -> Set top/skip values
IF ld_is_data_requested = abap_true.
lo_request->set_skip( CONV #( ld_skip ) ).
IF ld_top > 0.
lo_request->set_top( CONV #( ld_top ) ).
ENDIF.
ELSE.
lo_request->request_no_business_data( ).
ENDIF.
" Count is requested
IF ld_is_count_requested = abap_true.
lo_request->request_count( ).
ENDIF.
Finally, we execute the request towards on-premises and get the data into the program:
" Execute and return data
DATA(lo_response) = lo_request->execute( ).
lo_response->get_business_data( IMPORTING et_business_data = et_result ).
ed_count = lo_response->get_count( ).
Fill Response
As a last step we have to fill IO_RESPONSE of the SELECT method. We check whether data and count have been requested and transfer the elements.
" Handle data over to respone
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( ld_count ).
ENDIF.
IF io_request->is_data_requested( ).
io_response->set_data( lt_result ).
ENDIF.
Hint: The structure of the request in the direction of on-premises is quite complex, but can easily be outsourced to a reusable class, which we also recommend, as it saves a lot of coding that would otherwise be created redundantly (DRY principle).
Value help
One way to use and test a custom entity is to use it as an input help in a field. To do this, we are expanding the core data service "ZBS_C_RAPPartner" from our RAP series. We add a search help to the PartnerName field with our custom entity as a search element, as well as this excerpt from the CDS view:
define root view entity ZBS_C_RAPPartner
provider contract transactional_query
as projection on ZBS_I_RAPPartner
{
key PartnerNumber,
@Consumption.valueHelpDefinition: [{ entity: { name: 'ZBS_I_RAPCustomEntityCNames', element: 'CompanyName' } }]
PartnerName,
In our app there is now a new search help in the "Name" field, which we can open using the icon in the back area:
Since we have not made any further restrictions in the entity or maintained UI annotations, the field names are used as headings. When opening the search help, the data is loaded from on-premises and displayed in the list. Further restrictions can be carried out using the fields above.
Debugging
Now how can we debug the entity? To do this, we want to take a look at what is passed to the class via the request. To do this, we set a breakpoint on the filter creation, right after reading the IO_REQUEST object. In the search help, we filter the industry to "*soft*" to find all companies related to software:
Now that we have started the search with "Go", we load the debugger in Eclipse and can take a closer look at the values.
Hint: The determination of the data only works via the OData Service, when the data is displayed in the data preview, an empty table is displayed and the determination logic is not run through.
Complete example
The complete class for the query implementation now looks like this, but you have to take care to use an existing destination.
CLASS zcl_bs_demo_cust_cname_query DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES:
if_rap_query_provider.
PROTECTED SECTION.
PRIVATE SECTION.
TYPES:
tt_result TYPE STANDARD TABLE OF ZBS_I_RAPCustomEntityCNames WITH EMPTY KEY.
CONSTANTS:
c_destination TYPE string VALUE `<destination-service-id>`,
c_entity TYPE /iwbep/if_cp_runtime_types=>ty_entity_set_name VALUE 'COMPANYNAMES'.
METHODS:
get_proxy
RETURNING VALUE(ro_result) TYPE REF TO /iwbep/if_cp_client_proxy,
read_data_by_request
IMPORTING
io_request TYPE REF TO if_rap_query_request
EXPORTING
et_result TYPE tt_result
ed_count TYPE int8.
ENDCLASS.
CLASS zcl_bs_demo_cust_cname_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
" Read data from OData on-premise
read_data_by_request(
EXPORTING
io_request = io_request
IMPORTING
et_result = DATA(lt_result)
ed_count = DATA(ld_count)
).
" Handle data over to respone
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( ld_count ).
ENDIF.
IF io_request->is_data_requested( ).
io_response->set_data( lt_result ).
ENDIF.
ENDMETHOD.
METHOD get_proxy.
TRY.
DATA(lo_destination) = cl_http_destination_provider=>create_by_cloud_destination(
i_name = c_destination
i_authn_mode = if_a4c_cp_service=>service_specific
).
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination( lo_destination ).
ro_result = cl_web_odata_client_factory=>create_v2_remote_proxy(
EXPORTING
iv_service_definition_name = 'ZBS_DEMO_RAP_ONPREM_ODATA'
io_http_client = lo_client
iv_relative_service_root = '/sap/opu/odata/sap/ZBS_API_COMPANY_NAMES_O2' ).
CATCH cx_root.
ENDTRY.
ENDMETHOD.
METHOD read_data_by_request.
DATA:
lo_root_filter_node TYPE REF TO /iwbep/if_cp_filter_node.
TRY.
DATA(lo_request) = get_proxy( )->create_resource_for_entity_set( c_entity )->create_request_for_read( ).
" Get informations from request
DATA(lt_filter_condition) = io_request->get_filter( )->get_as_ranges( ).
DATA(lt_requested_elements) = io_request->get_requested_elements( ).
DATA(lt_sort_elements) = io_request->get_sort_elements( ).
DATA(ld_skip) = io_request->get_paging( )->get_offset( ).
DATA(ld_top) = io_request->get_paging( )->get_page_size( ).
DATA(ld_is_data_requested) = io_request->is_data_requested( ).
DATA(ld_is_count_requested) = io_request->is_total_numb_of_rec_requested( ).
" Build filter condition
DATA(lo_filter_factory) = lo_request->create_filter_factory( ).
LOOP AT lt_filter_condition INTO DATA(ls_filter_condition).
DATA(lo_filter_node) = lo_filter_factory->create_by_range(
iv_property_path = ls_filter_condition-name
it_range = ls_filter_condition-range
).
IF lo_root_filter_node IS INITIAL.
lo_root_filter_node = lo_filter_node.
ELSE.
lo_root_filter_node = lo_root_filter_node->and( lo_filter_node ).
ENDIF.
ENDLOOP.
" Set filter
IF lo_root_filter_node IS NOT INITIAL.
lo_request->set_filter( lo_root_filter_node ).
ENDIF.
" Set requested fields
IF lt_requested_elements IS NOT INITIAL.
lo_request->set_select_properties( CORRESPONDING #( lt_requested_elements ) ).
ENDIF.
" Set Sorting
IF lt_sort_elements IS NOT INITIAL.
lo_request->set_orderby( CORRESPONDING #( lt_sort_elements MAPPING property_path = element_name ) ).
ENDIF.
" Data requested -> Set top/skip values
IF ld_is_data_requested = abap_true.
lo_request->set_skip( CONV #( ld_skip ) ).
IF ld_top > 0.
lo_request->set_top( CONV #( ld_top ) ).
ENDIF.
ELSE.
lo_request->request_no_business_data( ).
ENDIF.
" Count is requested
IF ld_is_count_requested = abap_true.
lo_request->request_count( ).
ENDIF.
" Execute and return data
DATA(lo_response) = lo_request->execute( ).
lo_response->get_business_data( IMPORTING et_business_data = et_result ).
ed_count = lo_response->get_count( ).
CATCH cx_root.
ENDTRY.
ENDMETHOD.
ENDCLASS.
Conclusion
The Custom Entity extends RAP to provide a way to provide data while being easy and flexible to implement through ABAP code. This allows you to make Core Data Services available as data sources for input help or in services.