Skip to Content
Technical Articles

Access Gateway exposed CDS within the ABAP stack itself

Introduction

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.

Other updates

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)
      ).

 

Multiple updates

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.

  1. A super class to isolate the usage of the SADL classes from your consumers
  2. Service dependent sub classes of that class, as service name is the main divider
  3. Entity dependent sub classes of the service dependent class

Final thoughts

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.

Extra:

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.

 

 

2 Comments
You must be Logged on to comment or reply to a post.
  • Hi Hans,

    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?

    Regards

    Ben Meijs

    • Hello Ben,

      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:

      @AbapCatalog.sqlViewName: '<YourViewName>'
      @AbapCatalog.compiler.compareFilter: true
      @AbapCatalog.preserveKey: true
      @AccessControl.authorizationCheck: #CHECK
      @EndUserText.label: '<YourViewLabel>'
      
      @ObjectModel.semanticKey: 'Supplier'
      @ObjectModel.modelCategory: #BUSINESS_OBJECT 
      @ObjectModel.compositionRoot: true  
      @ObjectModel.transactionalProcessingEnabled: true  
      @ObjectModel.writeActivePersistence: '<YourTable>'
              
      @ObjectModel.createEnabled: true
      @ObjectModel.deleteEnabled: true 
      @ObjectModel.updateEnabled: true

      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.