Access Gateway exposed CDS within the ABAP stack itself
At a development team I work with we make extensively use of CDS views. Not only for reporting purposes or exposing data to HTML5 frontend applications, but also for updating, and I must say that the Gateway options in combination with the BOBF/SADL framework appears to be a rock solid platform that is also very easy to learn and fun to program with.
To get clear what context I am talking about:
- ABAP CDS views
- with ObjectModel.modelCategory is #BUSINESS_OBJECT
- with ObjectModel.writeActivePersistency set to a database table
- with ObjectModel.createEnabled (updateEnabled, deleteEnabled) set to true, so with updating logic in mind
- Service generation through SEGW
- Hook methods for data validation, determinations and so as provided for business objects by BOBF/SADL framework (transaction BOBX)
Now, the issue I came across is that updating through Gateway works fine, but the question came up what to do for an update from that very same ABAP system, for instance with batch processes. Searching on the web I have seen some workarounds or calling BOBF Api’s using the /BOBF/IF_TRA* framework. Maybe I missed something there, but I could not get that working for SADL entities. I don’t like other options like calling CL_HTTP_CLIENT either, so I searched for another way.
Why not use other ways to update my (non-standard SAP) data? Because I do not want to have multiple validation or update logic. Why use ABAP anyway at this era? Perhaps I’ll make you another blog on that.
For this blog, I only take the standard methods create, update, read set and delete into account, which leaves specific actions or complicated stuff like reading by associations out of scope. For the readers sake I did not pay any attention to proper exception handling.
Please note that have written a supplementary blog especially for CDS views with annotation OData.publish set to true https://blogs.sap.com/2020/02/23/access-gateway-exposed-cds-within-the-abap-stack-itself-odata.publish/, as this is slightly different.
Scanning the Gateway service classes
Looking the way Gateway handles SADL requests can be started in two ways
- By setting an external break-point in the BOBF hook methods
- By checking the <SVCNAME>_DPC class
Both lead to the methods from interface IF_SADL_GW_DPC_UTIL implemented in <SVCNAME>_DPC. Within this part of the implementation we find methods like
- if_sadl_gw_dpc_util~get_dpc( )->create_entity
- if_sadl_gw_dpc_util~get_dpc( )->get_entity
- and the likes
Focussing on what is happening inside these, in a private method _init, we find that processing at least requires:
- An entity mapper
subclass of cl_sadl_mp_entity, at least provinding an (empty) implementation of method get_sadl_definition( )
- A runtime, instantiated by a factory within cl_sadl_entity_api_factory
- For queries, a condition provider
a class implementing at least a method for if_sadl_condition_provider~get_condition( )
- A SADL transaction manager
a subclass of cl_sadl_entity_transactional that has not to implement nothing but is not an abstract
Starting with a getlist
From a functional point of view definitely not interesting, since we have OpenSQL providing us access to CDS views. However pleasant as a starter. We use a simple ABAP report form as example with some local classes and without any global variables or includes.
First, we need te SADL entity mapper. As said, the get_sadl_definition( ) has to be redefined and implemented, but does not have anything to do.
class lcl_mp_entity definition inheriting from cl_sadl_mp_entity. protected section. methods: get_sadl_definition redefinition. endclass. class lcl_mp_entity implementation. method get_sadl_definition. " Any implementation if necessary endmethod. endclass.
Apparently the way to pass conditions (select-options) is the most difficult part here, and is only necessary for reading (and not even that). For the implementation I used code of cl_sadl_cond_grouped_ranges->if_sadl_condition_provider~get_condition( ) as example. Since this is quite a lot of code I put this below as extra.
Calling the SADL classes looks pretty simple if set in ABAP code:
try. " Preparation with service name data(lr_iomp) = new lcl_mp_entity( iv_uuid = 'ZPAMV2' iv_timestamp = new zcl_zpamv2_mpc( )->get_last_modified( ) ). data(lr_sapi) = cl_sadl_entity_api_factory=>create( cast #( lr_iomp ) ). " Initialize runtime with interface view name data(lr_runt) = lr_sapi->get_runtime( 'ZI_PAM_ROLE' ). " Set scope for selected fields (from CDS view), and flag data to be fetched data ls_parameters type if_sadl_query_engine_types=>ty_requested. ls_parameters-fill_data = abap_true. append: 'FIELD1' to ls_parameters-elements, 'ETC' to ls_parameters-elements. " Get the data data lt_data type standard table of zi_pam_role. lr_runt->fetch( exporting is_requested = ls_parameters importing et_data_rows = lt_data ). catch cx_root into data(lx). ... . endtry.
ZPAMV2 is the Gateway service in this example. ZI_PAM_ROLE is the CDS (interface-) view we wish to read. The data from the view is available after the fetch. Note that the timestamp comes from the service -MPC class (In this case, ZCL_ZPAMV2_MPC), and is a non static member.
Creating a record
For executing updates with the SADL interfaces, we need an transaction manager, a non-abstract subclass of cl_sadl_entity_transactional, which does not require any implementation.
class lcl_sadl_trans definition inheriting from cl_sadl_entity_transactional. endclass.
It can be instantiated directly by passing entity type SADL and an entity-id <SERVICENAME>~<ENTITYNAME>.
The create itself is implemented in the SADL runtime method if_sadl_entity_transactional~create_single which accepts a normal structure of the ABAP type of the CDS interface view. Code snippet:
data(lr_iomp) = new lcl_mp_entity( iv_uuid = 'ZPAMV2' iv_timestamp = new zcl_zpamv2_mpc( )->get_last_modified( ) ). data(lr_sadl_transaction) = new lcl_sadl_trans( iv_entity_type = 'SADL' " TYPE SADL_ENTITY_TYPE iv_entity_id = 'ZPAMV2~ZI_PAM_ROLE' " SADL_ENTITY_ID ). data(lr_sapi) = cl_sadl_entity_api_factory=>create( cast #( lr_iomp ) ). data(lr_runt) = lr_sapi->get_runtime( 'ZI_PAM_ROLE' ). data ls_role type zi_pam_role. ls_role-field1 = 'VALUE'. " Just values for a record to create ls_role-etc = 'OTHERVALUE'. lr_runt->if_sadl_entity_transactional~create_single( importing ev_failed = data(lv_updatefailed) changing cs_entity_data = ls_role ).
The data supplied to the create is returned, and enriched by the logic you have implemented in your BOBF determinations such as number ranges.
If returned value ev_failed is false you can commit the update, otherwise rollback. This requires access to the transaction manager.
data(lr_trans_man) = cl_sadl_transact_manager_fctr=>get_transaction_manager( ). if lv_updatefailed = abap_false. lv_updatefailed = lr_trans_man->save( ). " Commit the changes else. lr_trans_man->discard_changes( ). " Rollback endif.
Error messages, for instance those you have implemented in your BOBF validations can be fetched with the SADL message handler. Simplified snippet:
if lv_updatefailed eq abap_true. lr_trans_man->get_message_handler( )->get_messages( exporting iv_severity = if_sadl_message_handler=>co_severity-error importing et_messages = data(lt_message) ). loop at lt_message assigning field-symbol(<message>). message id <message>-message->t100key-msgid type <message>-severity number <message>-message->t100key-msgno with cl_sadl_entity_util=>message_value( iv_attr = <message>-message->t100key-attr1 io_message = <message>-message ) " V2, V3 and V4 likewise . endloop.
Code snippet for delete. Assume field1 is the key field for the entity.
data(ls_key_value) = value ZI_PAM_ROLE( field1 = '040000002514' ). lr_runt->if_sadl_entity_transactional~delete_single( exporting is_key_values = ls_key_value importing ev_failed = data(lv_updatefailed) ).
Code snippet for change. it_updated elements is a table with fields to update, so the update behaves like a patch. Field1 is again the key.
data(ls_role) = value zi_pam_role( field1 = '040000002514' etc = 'YD' ). lr_runt->if_sadl_entity_transactional~update_single( exporting is_entity_data = ls_role it_updated_elements = value #( ( |ETC| ) ) importing ev_failed = data(lv_updatefailed) ).
Create, delete en update offers single line and multi line mode. An example for create multi line
data lt_table type table of zi_pam_role. " Fill data r_sadl_runtime->if_sadl_entity_transactional~create( importing et_failed = data(lt_updatefailed) changing ct_entity_data = lt_table ).. data(lr_trans_man) = cl_sadl_transact_manager_fctr=>get_transaction_manager( ). if lines( lt_updatefailed ) > 0. " Rollback
Note that the failed is a table now. With update it_updated_elements_per_tabix is a list of a field set list
Get to proper classes
These examples are coded within an ABAP report. Off course you want this in an OO wrapper. I will not work this out here but I can imagine some structure containing three levels.
- A super class to isolate the usage of the SADL classes from your consumers
- Service dependent sub classes of that class, as service name is the main divider
- Entity dependent sub classes of the service dependent class
I have not been able to match this solution with an ABAP NW752 stack, so maybe there are some SAP developments not taken into account here. Secondly, I don’t like the instantiation of the MPC class to get the timestamp. It seems too much for too little. I also have some doubts with the <service> – tilde – <entity> thing when creating the transaction manager. It does not look that durable; what if someone at SAP decides might decide to change the tilde into a dash or a dot?
Still, programming this way is very easy to learn and provides a very good runtime performance. Any comments or questions on this subject will be highly appreciated if you post them below.
Adding conditions to your query
class lcl_cond definition. " Example taken from CL_SADL_COND_GROUPED_RANGES public section. interfaces: if_sadl_condition_provider. methods: constructor importing it_ranges type if_sadl_cond_provider_grpd_rng=>tt_grouped_range iv_use_placeholders type abap_bool default abap_false. protected section. data mt_ranges type if_sadl_cond_provider_grpd_rng=>tt_grouped_range. data mt_sadl_condition type if_sadl_query_engine_types=>tt_complex_condition. data mt_elements type if_sadl_query_engine_types=>tt_element_info. data mv_use_placeholders type abap_bool. endclass. class lcl_cond implementation. method if_sadl_condition_provider~get_condition. if mt_ranges is not initial and ( mt_sadl_condition is initial or it_elements <> mt_elements ). mt_elements = it_elements. if mv_use_placeholders = abap_false and it_elements is not initial. data lt_elements like it_elements. loop at mt_ranges assigning field-symbol(<s_range>). read table it_elements with key id = <s_range>-field_path assigning field-symbol(<s_element>). if sy-subrc = 0. insert <s_element> into table lt_elements. endif. endloop. cl_sadl_condition_generator=>get_field_descr( exporting it_elements = lt_elements importing et_field_descriptors = data(lt_field_descriptors) ). endif. cl_sadl_condition_generator=>convert_ranges_to_conditions( exporting it_ranges = mt_ranges it_field_descriptors = lt_field_descriptors iv_use_placeholders = mv_use_placeholders importing et_conditions = mt_sadl_condition ). endif. et_sadl_condition = mt_sadl_condition. endmethod. method constructor. * check_range_consistency( it_ranges ). mt_ranges = it_ranges. "#EC CI_CONV_OK mv_use_placeholders = iv_use_placeholders. clear mt_sadl_condition. endmethod. endclass. " In your method fetching the data " Initialize runtime with interface view name data(lr_runt) = lr_sapi->get_runtime( 'ZI_PAM_ROLE' ). " Set (optional query conditions). Note the simple implementation of class lcl_cond lr_runt->if_sadl_query_fetch~register_condition_provider( new lcl_cond( value #( ( column_name = 'PROJECTID' rule_group = 0 field_path = 'PROJECTID' " Just a field from CDS view t_selopt = value #( ( sign = 'I' " Range options option = 'EQ' " low = '000001001366' ) ) ) " Just a value as example ) ) ). " Set scope for selected fields (from CDS view), and flag data to be fetched data ls_parameters type if_sadl_query_engine_types=>ty_requested.
I have just started to read your blog. Interesting subject and I'm glad you added multiple examples.
Just a question in order for me to understand it correctly: if you say "Service generation through SEGW", does that mean you don't use the annotation @OData.publish: true in the CDS-view but create the gateway service manually?
In our team we do this a slightly different, in a way you do not need OData.Publish set to true. We create the CDS view, with the annotations:
Then we import the CDS as referenced data source: SEGW, on node Data model right mouse-click, Reference > Data source.
I would have to check out the way of working using OData.publish, as this should be possible also.