Skip to Content
Author's profile photo Leigh Mason

Make your SAP Gateway Work Smarter not Harder – Part 1.2 ) Search Patterns and Service inclusion

Introduction

Part 1 of this series can be found here if you have not seen it yet:

https://scn.sap.com/community/gateway/blog/2016/04/08/making-your-sap-gateway-work-smarter-not-harder-re-use-patternsa

In Part 1 we discussed development patterns in SAP Gateway and how we can achieve re-use and business logic encapsulation.

Two concepts we covered were a “search pattern” where we used Dynamic SQL to retrieve entity sets and a class hierarchy to encapsulate business logic and separate out functions on a module and gateway level.

In this blog I’d like to clarify and enhance the discussion by covering different options for Search and how to do Service Inclusion for re-use purposes.


Search Pattern and Class Hierarchy

In Part 1 we covered a possible class hierarchy where we could gain re-use and business logic encapsulation across our OData services and also included SAP module specific functions.

We also covered a generic search pattern that uses Dynamic SQL, that is ABAP code where you build up your own query and execute the SQL statements by manipulating “From” “Where” clauses.

The dynamic SQL doesn’t sit well with a lot of people and here a few reasons why:

  • Code can be difficult to maintain, there are a lot of hard coded variables and requires a deep understanding of the SAP data model.


  • It is easy to go from simple use cases to complex ones and therefore create a larger technical debt than required.


  • There are obviously other choices and where a good “framework” is available there is certainly no rule that says don’t use that instead.

Re-Jigging the Class Hierarchy

  • Removing the “Search Pattern” from ZCL_ODATA_RT.  This ensures that only SAP Gateway level functions are encapsulated here, this makes more sense to perform things like “E-Tag” handling rather than search.


  • Have a separate inheritance for the search pattern, in this case I’ve put a new class in the hierarchy called ZCL_ODATA_QRY_[search_function] where “[search_function]” is a framework specific search pattern. This doesn’t have to be module specific, in systems like CRM I have different frameworks like BOL or Reporting Framework that I can re-use here across different modules like Service or Sales.

/wp-content/uploads/2016/05/blog_image_948211.png

Search Pattern – Example with Reporting Framework

This is an example I put together using Reporting Framework in CRM.  We created a new class “ZCL_ODATA_QRY_RF” which designates this class is coupled with the “Reporting Framework”.  It still inherits from our ZCL_CRM_ODATA_RT class.


/wp-content/uploads/2016/05/blog_image_948211.png

We have a simple constructor that takes in a CRM BOL query and object type ( like a Business Activity BUS2000126 ) and instantiates a instance of of the IO_SEARCH class provided by SAP.


/wp-content/uploads/2016/05/blog_image_948211.png

And of course a “Search” method that takes in some query parameters, the IS_PAGING structure from our OData service and another flag that allows to return only the GUID’s as a result rather than all the functional data.


/wp-content/uploads/2016/05/blog_image_948211.png

This is our search method implementation:


METHOD search.

 DATA: lt_return TYPE bapiret2_tab,
 ls_message_key TYPE scx_t100key,
  lv_max_hits    TYPE i.

 FIELD-SYMBOLS: <result_tab> TYPE STANDARD TABLE.

 IF it_query_parameters IS INITIAL.
 RETURN.
 ENDIF.

 CALL METHOD gr_search->set_selection_parameters
 EXPORTING
  iv_obj_il = gv_query
  iv_obj_type = gv_object_type
 it_selection_parameters   = it_query_parameters
 IMPORTING
  et_return = lt_return
 EXCEPTIONS
  partner_fct_error         = 1
  object_type_not_found     = 2
 multi_value_not_supported = 3
 OTHERS = 4.

 READ TABLE lt_return ASSIGNING FIELD-SYMBOL(<fs_message>) WITH KEY type = 'E'.
 IF sy-subrc = 0.
 ls_message_key-msgid = 'CLASS'.
 ls_message_key-msgno = 000.
 RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception EXPORTING textid = ls_message_key.
 ENDIF.

 IF iv_keys_only = abap_true.
 CALL METHOD gr_search->get_result_guids
 EXPORTING
 iv_max_hits  = lv_max_hits
 IMPORTING
 et_guid_list = rt_guids
  et_return    = lt_return.

 READ TABLE lt_return ASSIGNING <fs_message> WITH KEY type = 'E'.
 IF sy-subrc = 0.
 ls_message_key-msgid = 'CLASS'.
 ls_message_key-msgno = 000.
 RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception EXPORTING textid = ls_message_key.
 ENDIF.

 ELSE.

 CALL METHOD gr_search->get_result_values
 EXPORTING
 iv_max_hits  = lv_max_hits
 IMPORTING
 et_results   = rt_results
 et_guid_list = rt_guids
  et_return    = lt_return.

 READ TABLE lt_return ASSIGNING <fs_message> WITH KEY type = 'E'.
 IF sy-subrc = 0.
 ls_message_key-msgid = 'CLASS'.
 ls_message_key-msgno = 000.
 RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception EXPORTING textid = ls_message_key.
 ENDIF.

 ENDIF.

 **********************************************************************
 * Process Top / Skip tokens for paginglts
 **********************************************************************
 IF is_paging-skip > 0.
 DELETE rt_results FROM 1 TO is_paging-skip.
 DELETE rt_guids   FROM 1 TO is_paging-skip.
 ENDIF.
 IF is_paging-top > 0.
 DELETE rt_results FROM ( is_paging-top + 1 ).
 DELETE rt_guids   FROM ( is_paging-top + 1 ).
 ENDIF.

 ENDMETHOD.





So now when you want to execute the search in your concrete class, you can consume the SEARCH method in the inherited Search Framework plugin you’ve created:


METHOD /iwbep/if_mgw_appl_srv_runtime~get_entityset.

 DATA: lt_query_parameters TYPE genilt_selection_parameter_tab,
  ls_query_parameter LIKE LINE OF lt_query_parameters,
  lt_sort TYPE abap_sortorder_tab,
  ls_sort TYPE abap_sortorder.

 FIELD-SYMBOLS: <fs_results> TYPE STANDARD TABLE.

 CREATE DATA er_entityset TYPE TABLE OF (gv_result_structure).
 ASSIGN er_entityset->* TO <fs_results>.

 **********************************************************************
 * Navigation Path from an Account
 **********************************************************************
 READ TABLE it_key_tab ASSIGNING FIELD-SYMBOL(<fs_key>) WITH KEY name = 'AccountId'.
 IF sy-subrc = 0.
 ls_query_parameter-attr_name = 'ACTIVITY_PARTNER'.
 ls_query_parameter-sign = 'I'.
 ls_query_parameter-option    = 'EQ'.
 MOVE <fs_key>-value TO ls_query_parameter-low.
 APPEND ls_query_parameter TO lt_query_parameters.
 ENDIF.


 **********************************************************************
 * Process Filters
 **********************************************************************
 LOOP AT it_filter_select_options ASSIGNING FIELD-SYMBOL(<fs_filter_select_option>).

 CASE <fs_filter_select_option>-property.

 WHEN 'ProcessType'.
 LOOP AT <fs_filter_select_option>-select_options ASSIGNING FIELD-SYMBOL(<fs_select_option>).
 MOVE-CORRESPONDING <fs_select_option> TO ls_query_parameter.
  ls_query_parameter-attr_name = 'PROCESS_TYPE'.
 APPEND ls_query_parameter TO lt_query_parameters.
 ENDLOOP.
 WHEN OTHERS...
 .....<DO SOME MORE STUFF HERE TO HANDLE FILTERS>....

 ENDCASE.

 ENDLOOP.

 CALL METHOD search
 EXPORTING
 it_query_parameters = lt_query_parameters
  iv_keys_only        = abap_false
  is_paging           = is_paging
 IMPORTING
  rt_results          = <fs_results>.
....COPY THE RESULTS TO THE EXPORT TABLE ETC....





Service Pattern Summary

Whilst this is a brief example, it shows the possibility of plug and play type frameworks for your OData services rather than tackling dynamic SQL that was included in the first part of this blog series.

If you’ve implemented a similar pattern I would love to hear from you with details about what you have put together..


Service Inclusion

Service Inclusion is the process of including another Service Model in your SAP Gateway Service Builder project, like this:

/wp-content/uploads/2016/05/blog_image_948211.png

/wp-content/uploads/2016/05/blog_image_948211.png

/wp-content/uploads/2016/05/blog_image_948211.png

Effectively, it allows you to access the Entities in service B directly from service A:

/wp-content/uploads/2016/05/blog_image_948211.png

This approach maximises the re-use of your services, not only will you get re-use and business logic encapsulation in the class hierarchy, your design time re-use is maximised as well.



Limitations / Gotchas

There are a couple of things to watch out for.

Referring to the diagram above, when you execute the URI for “/SALES_ORDER_SRV/Accounts(‘key’)/SalesOrders” the navigation path is not populated, a Filter parameter is passed to the SalesOrder GET_ENTITYSET where you now must filter the Sales Orders by the Account ID. 

What I mean by “limitation” is that usually you will be passed a navigation path ( IT_NAVIGATION_PATH) where you can assess where the navigation came from and what the keys were, in this use case you are missing the KEY_TAB values in the IT_NAVIGATION_PATH importing table in your GET_ENTITYSET method.

For this to work you must also set the referential constraint in the SEGW project and build your Associations as an external reference, like this:

/wp-content/uploads/2016/05/blog_image_948211.png

When the SAP Gateway framework attempts to assess which model reference your entity belongs to so it can execute a CRUD function, the underlying code loops over the collection of model references you have included ( in Service B ) and tries to find the first available model where your external navigation entity is located. 


In case you have implemented the same entity in multiple included services, SAP picks up the first available which can lead to surprises if you’re wondering why your debug point is not being triggered during a GET_ENTITYSET method call in the wrong service



Service Inclusion Summary


If you haven’t used this feature yet it provides a really great way to maximise re-use of your OData services, just a side note here; a really good use for this pattern is your common configuration entity model, things such as Country and State codes, Status Key Value pairs etc,


I have built a common service before that reads all of the configuration we use across different Fiori applications and are contained in one common service.


This way i simply “include” the common service so i don’t have to keep implementing the same entity set or function import in different services.




Final Summary


I have put together this blog to try and clarify and provide a different perspective on Search capability within our OData services.  I know there are some of us out there that dislike the dynamic SQL scenario and for good reason.


My aim is to encourage some thought leadership in this space, so those customers tackling their OData service development can at least learn from our experience and embrace re-use patterns to really try and reduce TCO not to mention accelerate UX development in parallel.


As always, I’d love to hear from our community about other development patterns you’ve come up with so please share your thoughts and learnings, it’s really important as our customers start to ramp up their OData and Fiori development.

Assigned Tags

      5 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Nabi Zamani
      Nabi Zamani

      Leigh,

      As you know I really enjoyed your first part, and I also like part 2! Thank you for you effort!

      I have a question about the limitations of service inclusion.

      So when I call /SALES_ORDER_SRV/Accounts('key')/SalesOrders I have to use the IT_FILTER_SELECT_OPTIONS in the corresponding SALESORDERS_GET_ENTITYSET even if I have modelled the referential constraints correctly, right?

      On the other side, assuming there is a navigation property from the SalesOrder to the corresponding SalesOrderItems (1:N), then this time with /SALES_ORDER_SRV/SalesOrder(OrderID='111')/SalesOrderItems I would get the IT_KEY_TAB filled in the SALESORDERITEM_GET_ENTITYSET, correct?

      If that is right, then the developer who implements SALESORDERS_GET_ENTITYSET must be aware of the different uses cases in order to make the service implementation "ready for inclusion", right?

      Best,

      Nabi

      Author's profile photo Leigh Mason
      Leigh Mason
      Blog Post Author

      Hi Nabi, yes you are 100% correct, when you put your implementation in the other side where you want they key, you will most likely end up with reading IT_KEY_TAB and IT_FILTER_SELECT_OPTIONS.

      IMHO I don't like this approach, my view is the behaviour should be the same, the OData navigation path regardless is "/SalesOrders('key')/SalesOrderItems".

      I LOVE the fact that you can service include but having to add additional code to read a "key" value from a "filter" is kind of redundant.

      I'm hoping Andre Fischer can provide some comments here, I haven';t fully explored what's coming in the 7.50 release but hoping that this use case is covered in there?

      cheers

      Leigh

      Author's profile photo Nabi Zamani
      Nabi Zamani

      Exactly - I hate to introduce boilerplate code as well. As a developer I don't even want to worry whether the entities of my service are used in a 1:N relation via some include service or not... It should be absolutely enough to model the correct association + referential constraints in the new service. Everything else should be transparent.

      Instead I have some boilerplate code that looks similar to this, and something like this has to be added to the GET_ENTITYSET impelementations to avoid "sideeffects":

      ...

      IF it_key_tab[] IS NOT INITIAL.

             READ TABLE it_key_tab ASSIGNING <key> WITH KEY name = 'Vbeln'.

             lv_order_key = <key>-value.

      ELSEIF it_filter_select_options IS NOT INITIAL.

             " get the key from the parent which is the Vbeln

             " of the order by reading it_filter_select_options
             " this is needed to allow 1:N relations if this
             " entity is used in modelling of external services

             lv_order_key = ...

      ENDIF.


      IF lv_order_key IS INITIAL.

          RETURN.

      ENDIF.

      ...

      Cheers,

      Nabi

      Author's profile photo Nabi Zamani
      Nabi Zamani

      Hi Leigh Mason

      I think here is another limitation of using include services:

      Is it possible that $expand is not really working?!

      I assume that the expanded navigation property is a 1:N relation to an entity from the included service (including the correct referential constraints).

      In your example you could call the following (with the assumptions above):


           /ACCOUNT_SRV/Accounts('key')?$expand=SalesOrders

      For me SAP GW returns some error similar to this one here:


      • code: "/IWBEP/CM_MGW_RT/105"
      • value: "Entity by ID 'NE44C9E66BEAF1FBE1973C7C48D2345B7' not found."

      Maybe I should stop my experiments... πŸ™‚

      Thanks, Nabi

      Author's profile photo Michael Rudolph
      Michael Rudolph

      Hi @Nabi Zamani

      i currently run into the same error mesage while triying to expand an includes OdataEntiy... Any idea how to solve this issue?

       

      regards

      Micha