Introduction
Part 1 of this series can be found here if you have not seen it yet:
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.
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.
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.
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.
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:
Effectively, it allows you to access the Entities in service B directly from service A:
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:
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.