
ABAP Quick - Generic Query Implementation
Tired of the same old query class implementation for custom entities in RAP? It's time for a reusable component.
Table of contents
In this article, we'll look at an implementation for starting a reusable query using a custom entity and easily accessing data from an OData, regardless of the system the data is located on.
Introduction
Using a custom entity, there's a simple way to create a value help for another system in one system. The data is read from a remote system at runtime and made available to the user. This makes it very easy to implement remote scenarios. However, implementing the query is quite complex, especially if you have to do it over and over again.
Query
The Query class is defined in the Core Data Service using the annotation "@ObjectModel.query.implementedBy: 'ABAP:xxx'" and the implementation is called in the SADL framework at runtime. We must implement the SELECT method of the IF_RAP_QUERY_PROVIDER interface.
In addition to the actual method, some methods (GET_SORT_ELEMENTS, GET_PAGING) must be called from the IO_REQUEST object, otherwise a dump will occur. You can find more information about this in the Deep Dive on Custom Entities.
Concept
In this article, we plan a generic implementation of the query in various constellations so that the functions can be easily reused in further implementations and services. This should save you implementation costs in subsequent projects.
Architecture
For the architecture, we use the Factory Pattern to cover the topic of decoupling and testability. We also need an exception class if certain steps cannot be implemented or if the query aborts.
Since such a component is usually developed across all components and we have to deal with the concept of software components in ABAP Cloud, we should consider the topic of decoupling and use via SWC.
Configuration
To have a configuration for access, we define it as a structure in the interface. The advantage of a structure is that we only have to pass data through the class once, without defining each individual parameter. This means the interface can be extended at any time without having to adapt much code.
TYPES property TYPE STANDARD TABLE OF if_com_arrangement_factory=>ty_query_param_prop WITH EMPTY KEY.
TYPES:
BEGIN OF arrangement,
comm_scenario TYPE if_com_management=>ty_cscn_id,
service_id TYPE if_com_management=>ty_cscn_outb_srv_id,
comm_system_id TYPE if_com_management=>ty_cs_id,
property TYPE property,
END OF arrangement.
TYPES: BEGIN OF configuration,
arrangement TYPE arrangement,
cloud_destination TYPE string,
consumption_model TYPE /iwbep/if_cp_runtime_types=>ty_proxy_model_id,
service_root TYPE string,
client TYPE string,
language TYPE sy-langu,
protocol TYPE protocol_intern,
END OF configuration.
In principle, you can choose an object (class/interface) or a structure. Here, you should simply decide for yourself whether you also need something like validation or additional methods. In the simplest case, a structure is sufficient to transport the data.
Methods
For the implementation, we choose three methods: complete data processing with request and response, processing via request, and manual passing of all values. This gives us the most flexibility later and allows us to control which components we pass to the component. In any case, we must pass the data via a CHANGING parameter so that the information is passed through our code.
METHODS read_odata_by_values
IMPORTING setting TYPE setting_by_value
CHANGING business_data TYPE ANY TABLE
!count TYPE count
RAISING zcx_bs_demo_provider_error.
METHODS read_odata_by_request
IMPORTING setting TYPE setting_by_request
CHANGING business_data TYPE ANY TABLE
!count TYPE count
RAISING zcx_bs_demo_provider_error.
METHODS read_odata_with_response
IMPORTING setting TYPE setting_with_response
CHANGING business_data TYPE ANY TABLE
RAISING zcx_bs_demo_provider_error.
Implementation
In this chapter, we'll look at the implementation of the various scenarios and which method has which task.
Hierarchy
Basically, the methods call each other in a hierarchy and pursue specific tasks. Depending on how much of the following steps you want to delegate, you start with a different method:
- READ_ODATA_WITH_RESPONSE - Calls the next method and fills the result directly into the response.
- READ_ODATA_BY_REQUEST - Unpacks the request object, deletes missing fields, and adds required fields. Also defines the pagination for the request.
- READ_ODATA_BY_VALUES - Executes the final query, creates the client, and prepares the HTTP request. The result is then written to the variables.
Destination
In the first step, we need a destination for the query. The user currently has the option of specifying a cloud destination (destination service) or a communication arrangement via the configuration.
IF configuration-arrangement IS NOT INITIAL.
result = cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = configuration-arrangement-comm_scenario
service_id = configuration-arrangement-service_id
comm_system_id = determine_communication_system( ) ).
ELSEIF configuration-cloud_destination IS NOT INITIAL.
result = cl_http_destination_provider=>create_by_cloud_destination(
i_name = configuration-cloud_destination
i_authn_mode = if_a4c_cp_service=>service_specific ).
ENDIF.
With the Communication Arrangement, we can theoretically store multiple communication systems using the "Additional Properties." To do this, we implement a determination of the correct system via the standard API.
DATA(query) = VALUE if_com_arrangement_factory=>ty_query(
cscn_id_range = VALUE #( ( sign = 'I' option = 'EQ' low = configuration-arrangement-comm_scenario ) )
ca_property = configuration-arrangement-property ).
DATA(arrangement_factory) = cl_com_arrangement_factory=>create_instance( ).
arrangement_factory->query_ca( EXPORTING is_query = query
IMPORTING et_com_arrangement = DATA(systems) ).
RETURN systems[ 1 ]->get_comm_system_id( ).
Client
In the next step, we create the appropriate client for our query. Using the configuration, we create the appropriate object for OData v2 or v4. We could also implement other clients using the constants if we needed them.
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
CASE configuration-protocol.
WHEN zif_bs_demo_service_prov=>protocol-odata_v2.
result = /iwbep/cl_cp_factory_remote=>create_v2_remote_proxy(
is_proxy_model_key = VALUE #( repository_id = 'DEFAULT'
proxy_model_id = configuration-consumption_model
proxy_model_version = '0001' )
io_http_client = http_client
iv_relative_service_root = configuration-service_root ).
WHEN zif_bs_demo_service_prov=>protocol-odata_v4.
result = /iwbep/cl_cp_factory_remote=>create_v4_remote_proxy(
is_proxy_model_key = VALUE #( repository_id = 'DEFAULT'
proxy_model_id = configuration-consumption_model
proxy_model_version = '0001' )
io_http_client = http_client
iv_relative_service_root = configuration-service_root ).
ENDCASE.
We should now have all the objects we need to begin implementing the methods.
READ_ODATA_WITH_RESPONSE
The method calls the subsequent method RED_ODATA_BY_REQUEST and then passes the result directly to the response. This saves us the assignment, since we first have to check whether the data was actually requested by the request.
IF setting-request->is_total_numb_of_rec_requested( ).
setting-response->set_total_number_of_records( local_count ).
ENDIF.
IF setting-request->is_data_requested( ).
setting-response->set_data( business_data ).
ENDIF.
READ_ODATA_BY_REQUEST
In the first step, we break down the request and retrieve all the information for our query. To do this, we fill the structure for the next step.
DATA(local_setting) = CORRESPONDING zif_bs_demo_service_prov=>setting_by_value( setting ).
local_setting-filter_condition = setting-request->get_filter( )->get_as_ranges( ).
local_setting-requested_elements = setting-request->get_requested_elements( ).
local_setting-sort_order = setting-request->get_sort_elements( ).
local_setting-is_data_requested = setting-request->is_data_requested( ).
local_setting-is_count_requested = setting-request->is_total_numb_of_rec_requested( ).
Before we call the subsequent method READ_ODATA_BY_VALUES, we adjust the corresponding fields that are no longer needed in the query. We also add fields that we might need for other derivations.
LOOP AT setting-delete_fields REFERENCE INTO DATA(field_for_deletion).
DELETE local_setting-filter_condition WHERE name = field_for_deletion->*.
DELETE local_setting-requested_elements WHERE table_line = field_for_deletion->*.
DELETE local_setting-sort_order WHERE element_name = field_for_deletion->*.
ENDLOOP.
LOOP AT setting-read_fields REFERENCE INTO DATA(field_to_read).
INSERT field_to_read->* INTO TABLE local_setting-requested_elements.
ENDLOOP.
Our custom entity may also contain virtual fields or fields that are determined in another way. If the user filters, displays, or sorts the fields in the app, they will be included in the request. If we execute the request against our OData service, these fields would result in an error.
READ_ODATA_BY_VALUES
The last method constructs the actual HTTP request and retrieves our data. To do this, we create a destination and an OData request.
DATA(odata_client) = create_client( ).
DATA(odata_request) = odata_client->create_resource_for_entity_set( setting-entity_name )->create_request_for_read( ).
In the next step, we prepare the request and add the filter, elements, additional options, and restrictions. For the additional options, we set the client and language as additional options if desired in the settings.
set_filter_for_request( odata_request = odata_request
setting = setting ).
set_elements_for_request( odata_request = odata_request
setting = setting ).
set_options_for_request( odata_request = odata_request
setting = setting ).
set_query_options_for_request( odata_request ).
Once we're done, we can execute the query and assign the result to the variables.
DATA(odata_response) = odata_request->execute( ).
IF setting-is_data_requested = abap_true.
odata_response->get_business_data( IMPORTING et_business_data = business_data ).
ENDIF.
IF setting-is_count_requested = abap_true.
count = odata_response->get_count( ).
ENDIF.
This lays the foundation for its use, and the class will be used in one of the next articles.
Complete example
You can find all objects in our GitHub repository in the package ZBS_DEMO_RAP_UTILITY. The source code shown above only shows excerpts of the implementation. In principle, you may have different requirements and should modify the implementation accordingly. For 99% of our reading scenarios, the current implementation is sufficient.
Conclusion
Having a generic implementation for reading remote data saves a lot of time and development work. However, you should be aware of the special features associated with software components.