Welcome Back

Welcome back again to our third installment of the series. Today we will cover off navigation and the odata $expand operator. If you haven’t followed along already check out the previous 2 blogs to find out how we ended up here, at part 3.

Part 1 – Developing data driven reusable READ functionality.

Part 2 – Reusable READ functionality. $select, $orderby, $top, $skip, $inlinecount, $count

Part 4 – Reusable READ functionality. Extending with Base Entities

SEGW

Let’s start with defining some new entities to use in our examples. Referring back to part 1 of this series we’ll import from DDIC structures to create our new entities. We’ll create 3 new entities, Flight (using table SFLIGHT), FlightSchedule (using table SPFLI) and Plane (using table SAPLANE). Define them as below with their Name, Key and ABAP fieldname.

/wp-content/uploads/2016/07/entities_998971.png

Association / Navigation / Referential Constraints

Now let’s create some associations and navigations using the wizard, as below. As part of the creation we will also setup some referential constraints. This is the information we will be using in our navigation routine to determine associations between entities.

02_createAssoc_sm.png

AirlineToSchedule

The first association and navigation we will create is from the Airline entity to the FlightSchedule entity. As in the image below, in the first step specify the association details and navigation property followed by the referential constraints. Use the search help to look the values up to make it a little easier. Although you will need to type the Association Name and Navigation Property (if you wish to change it as below ).

/wp-content/uploads/2016/07/airline_schedule_998985.png

ScheduleToFlight

Next we will create the association and navigation from the FlightSchedule entity to the Flight entity. Follow the same steps as above with the below image as reference.

/wp-content/uploads/2016/07/schedule_flight_998986.png

FlightToPlane

Finally we will create an association and navigation from the Flight entity to the Plane entity. Again follow the same steps using the image below as reference.

Though note in this last example we are setting the dependent navigation property.

/wp-content/uploads/2016/07/flight_plane_999008.png

Complete Model

After all the changes made the complete model should look as below in the overview section.

/wp-content/uploads/2016/07/09_final_model_999081.png

Now save the model and generate our runtime objects as we did in part 1. Make sure you can see your new entities and associations by running the service with the $metadata uri option:

     /sap/opu/odata/sap/ZFLIGHT_SRV/$metadata

All good? Let’s continue.

Data Provider Extension Class – ZCL_ZFLIGHT_DPC_EXT

Now we will redefine our data provider methods for the newly created entities.  Simply redefine the below methods of class

ZCL_ZFLIGHT_DPC_EXT and add our standard code for get_entity() and get_entityset(), as we did in part 1 of the series for the Airline entity.

Redefine the methods:

/wp-content/uploads/2016/07/10_redefine_methods_999089.png

Code for …get_entity():


get_gw_helper( )->get_entity(
  exporting io_tech_request_context = io_tech_request_context
  changing  cs_entity = er_entity
   cs_response_context = es_response_context ).











Code for …get_entityset():


get_gw_helper( )->get_entityset(
  exporting io_tech_request_context = io_tech_request_context
  changing  ct_entityset = et_entityset
   cs_response_context = es_response_context ).











This stems back to our original goal of adding as little as possible to our data provider extension class in the way of reading entities and allowing the helper class to do most of the work dynamically for us.

Finished, that’s it for the data provider class. Save the changes and activate.

Helper Class – ZCL_GATEWAY_HELPER

Now let’s add the expand and navigation logic to our helper class.

First we will create two new class attributes as below.

class attrib.PNG

Mv_conv_keys, will be used to determine if we should convert our source keys, mv_entity_name will be used to check if we need to run though our init() method due to a new entity, as explained below in the init() change.

Init(), change

Now we will be running though the gateway frame work executing expands. So we’ll place a check in this method to ensure that we are not loading all the model details again for the same entity. We will check the entity name passed in does not match the entity last processed.

Place these new lines of code after the declaration for the field symbols.


  field-symbols:  <ls_property> like line of lt_table_properties.
* first check if we already loaded this entity, eg we might be processing
* through a $expand.
  if mv_entity_name <> iv_entity_name.










and the following new lines at the end of the method:


* save this for comparison on the next round though.
* if we have already computed this then lets not do it again, eg in an $expand!
    mv_entity_name = iv_entity_name.
  endif.
endmethod.









get_entity_nav(), new method, protected

The first new method we will create is a helper method to be used with our next method process_nav_path(). The purpose of this method is select our current navigation entity in readiness for our next navigation. The method will take 3 parameters the entity name, the where condition and the returned entity selected. This entity can then be used as the data for next navigations required keys. Don’t forget to add the exceptions also.

Method Signature

/wp-content/uploads/2016/07/11_get_entity_sig_999104.png

At the start we just free any data which may be left over from a previous call, as you’ll see in the next method the call is embedded in the navigation loop. Next we get the DDIC structure name from the entity as we have seen in previous examples. We create some data based on this DDIC structure and assign to a field symbol ready for selection. The select is then processed using the where condition passed in. An exception will be raised if no data can be selected as we’ll be unable to continue our navigation.


method get_entity_nav.
  data: lr_entity type ref to /iwbep/if_mgw_med_odata_types=>ty_s_med_entity_type.
  field-symbols: <ls_entity> type any.
  lr_entity = mr_model->get_entity( iv_entity_name ).
  free: er_entity.
*   create our data to select into
  create data er_entity type (lr_entity->attribute_struct).
  assign er_entity->* to <ls_entity>.
*   select entity, for navigation
  select single *
         into <ls_entity>
         from (lr_entity->attribute_struct)
         where (iv_db_where).
  if sy-subrc <> 0.
    raise exception type /iwbep/cx_mgw_busi_exception
      exporting
        textid            = /iwbep/cx_mgw_busi_exception=>business_error_unlimited
        message_unlimited = 'Navigation select failed on entity, ' && iv_entity_name && '.'.
  endif.
endmethod.







process_nav_path(), new method, protected

This is a big one! I’ll try to explain most of what is going on here and justifications, in a way which makes sense!

To start the method takes 4 parameters, these being the navigation path, the source keys, a returned parameter to determine if we are navigating and finally the OSQL where condition to be executed in the calling get_entity() or get_entityset() method. Again remember to add the exceptions.

Method Signature

/wp-content/uploads/2016/07/11_process_nav_sig_999131.png

Initially we check to see if there is any navigation, if not simply exit. Otherwise we continue to loop over the navigation table.

First iteration

In the first iteration only (defined by AT FIRST.) we build our OSQL where condition based on the source keys. The source keys will only be converted if this is the first time though a get…() method, for example not an subsequent call via an expand or navigation, as in this case the keys should already be converted.

To convert the keys we use the key converter contained in the navigation structure attribute KEY_CONVERTER. We do this by defining some dynamic data based on the DDIC structure and call the EXECUTE() method of the key converter, the results are returned back into our field symbol to be used in our source key loop, again only if this is the first time in a get…() method. Otherwise we simply use the keys passed into parameter it_source_keys.

Once the OSQL where condition has been built we call our new method above get_entity_nav() to retrieve our entity details.

All iterations

Continuing on from the above first / initial processing, we can either process navigation keys or process our referential constraints.

Navigations with keys

Following on from the first lot of processing, we check to see if we have any key fields from navigations. For example:

/wp-content/uploads/2016/07/src_nav_keys_999132.png

If we do then we convert the navigation keys using the class: /iwbep/cl_mgw_req_key_convert. We first create the key converter using the target entity type. Next create some dynamic data based on the DDIC structure of the target entity, and finally call execute() method of the key converter to convert our key values. We loop over the key tab first attempting to get the same key value from the entity details we have at hand. This is in an attempt to stop invalid navigations where keys do not match, for example:

/wp-content/uploads/2016/07/keys_no_match_999133.png

If the key value can be found in the entity and it does not match the value in the navigation key an exception is raised, otherwise the value is added to the OSQL where condition. If the key value cannot be found in the entity then the value is taken directly from our converted navigation keys.

Lastly we check if this is the last navigation, if so we want to return this OSQL where condition back to the caller to process. Otherwise we select our next entity, set the source entity to the previous target entity and continue our next loop iteration.

Referential Constraints

If we had no navigation keys then we will drop into processing the referential constraints. We set these up at the start of this blog during the association and navigation section. We first retrieve the navigation property, from this we can then pick up the association and from the association we then pick up the referential constraints.  We loop over the referential constraints retrieving the target property and matching this up with the source key property value from our entity. Using this information we can build our OSQL where condition, to be passed back to the caller.

Finally after the navigation loop we combine the where condition with the one passed in, being the condition from the odata $filter operator.


  method process_nav_path.
    data: lv_tech_name type /iwbep/if_mgw_med_odata_types=>ty_e_med_technical_name,
          lr_entity_type type ref to /iwbep/if_mgw_med_odata_types=>ty_s_med_entity_type,
          lr_nav_prop type ref to /iwbep/if_mgw_med_odata_types=>ty_s_med_reference,
          lr_assoc type ref to /iwbep/if_mgw_odata_re_assoc,
          lr_assoc_ref type ref to /iwbep/if_mgw_med_odata_types=>ty_s_med_reference,
          lt_ref_constraints type  /iwbep/if_mgw_odata_re_assoc=>ty_t_mgw_odata_ref_constraints,
          lv_value type string,
          lv_db_and type string value '',
          lv_db_where type string,
          lr_entity type ref to data,
          lv_max_nav type i,
          lv_src_entity type /iwbep/mgw_tech_name,
          lr_key_values type ref to data,
          lv_ddic type string,
          lr_key_conv type ref to /iwbep/if_mgw_req_key_convert,
          lr_etype type ref to /iwbep/if_mgw_odata_fw_etype.
    field-symbols: <ls_nav_path> like line of it_nav_path,
                   <ls_key_values> type any,
                   <ls_keytab> type /iwbep/s_mgw_tech_pair,
                   <ls_ref_constraints> like line of lt_ref_constraints,
                   <ls_key> like line of it_source_keys,
                   <lv_any> type any.
    ev_navigating = abap_false.
* check if we are navigating
    if it_nav_path is not initial.
      ev_navigating = abap_true.
* get maximum number of navigations, as we what to process the
* last select from the callers method.
      lv_max_nav = lines( it_nav_path ).
      loop at it_nav_path assigning <ls_nav_path>.
        at first.
* in our initial iteration, read our entity using our source keys
* as this is our starting point if first time through a get...()
* method then convert keys, otherwise keys should already be converted
          if mv_conv_keys = abap_true.
            lr_key_conv ?= <ls_nav_path>-key_converter.
            lv_ddic = lr_key_conv->get_entity_type( )->get_structure( ).
            create data lr_key_values type (lv_ddic).
            assign lr_key_values->* to <ls_key_values>.
            lr_key_conv->execute(
              exporting it_tech_pair = it_source_keys
              importing es_key_values = <ls_key_values>
            ).
          endif.
          loop at it_source_keys assigning <ls_key>.
* grab key from converted source keys?
            if mv_conv_keys = abap_true.
              assign component <ls_key>-name of structure <ls_key_values> to <lv_any>.
              lv_value = <lv_any>.
            else.
* otherwise directly from source keys
              lv_value = <ls_key>-value.
            endif.
            lv_db_where = lv_db_where && lv_db_and && `( ` && <ls_key>-name && ` = '` && lv_value && `' )`.
            lv_db_and = ` and `.
          endloop.
* select our entity to complete navigation
          get_entity_nav(
            exporting
              iv_entity_name = <ls_nav_path>-source_entity_type
              iv_db_where = lv_db_where
            importing
              er_entity = lr_entity ).
          lv_src_entity = <ls_nav_path>-source_entity_type.
        endat.
        free: lv_db_where,lv_db_and.
* if we have navigation key then lets process them
        if <ls_nav_path>-key_tab[] is not initial.
* Create a key converter here based on the target entity type and convert our keys
          lr_etype = mr_model->get_entity_type( <ls_nav_path>-target_entity_type ).
          free: lr_key_values, lr_key_conv.
          create object lr_key_conv
            type
            /iwbep/cl_mgw_req_key_convert
            exporting
              io_entity_type = lr_etype.
          lv_ddic = lr_etype->get_structure( ).
          create data lr_key_values type (lv_ddic).
          assign lr_key_values->* to <ls_key_values>.
          lr_key_conv->execute(
             exporting it_tech_pair = <ls_nav_path>-key_tab[]
             importing es_key_values = <ls_key_values>
          ).
          loop at <ls_nav_path>-key_tab[] assigning <ls_keytab>.
*
* first attempt is to retrieve key from source entity, if it can be found use it,
* otherwise use key from keytab, this is to stop this sort of thing returning
* valid results:
*
* ZFLIGHT_SRV/AirlineSet(AirlineId='AA')/FlightScheduleSet(AirlineId='AZ',ConnectionId='555')
*                                    \------> KEYS NOT EQUAL <------- /
*
            assign lr_entity->(<ls_keytab>-name) to <lv_any>.
            if sy-subrc = 0.
              if <lv_any> = <ls_keytab>-value.
                lv_value = <lv_any>.
              else.
                raise exception type /iwbep/cx_mgw_busi_exception
                  exporting
                    textid            = /iwbep/cx_mgw_busi_exception=>business_error_unlimited
                    message_unlimited = 'Key fields differ during navigation.'.
              endif.
            else.
* read value from our converted keys
              assign component <ls_keytab>-name of structure <ls_key_values> to <lv_any>.
              lv_value = <lv_any>.
            endif.
            lv_db_where = lv_db_where && lv_db_and && `( ` && <ls_keytab>-name && ` = '` && lv_value && `' )`.
            lv_db_and = ` and `.
          endloop.
* if we are on the last navigation don't process. Pass where condition back
* to our caller, get_entity() or get_entityset() to complete
          if sy-tabix <> lv_max_nav.
* select our entity to continue next navigation loop
            get_entity_nav(
              exporting
                iv_entity_name = <ls_nav_path>-target_entity_type
                iv_db_where = lv_db_where
              importing
                er_entity = lr_entity ).
            lv_src_entity = <ls_nav_path>-target_entity_type.
          endif.
        else.
* if no navigation keys, then try and build our navigation from
* our referential constraints
          if lv_src_entity is not initial.
            lr_entity_type = mr_model->get_entity( lv_src_entity ).
* get navigation property
            lv_tech_name = <ls_nav_path>-nav_prop.
            lr_nav_prop = mr_model->/iwbep/if_mgw_odata_fw_model~get_nav_property_by_tech(
                                        iv_name = lv_tech_name
                                        ir_entity_type = lr_entity_type  ).
* get association, and ref. contraints
            lr_assoc_ref = mr_model->get_association_by_id( lr_nav_prop->target_entity_id ).
            lr_assoc = mr_model->/iwbep/if_mgw_odata_re_model~get_association( lr_assoc_ref->name ).
            lt_ref_constraints = lr_assoc->get_ref_constraints( ).
* loop over ref contraints and marry up targets to sources
            loop at lt_ref_constraints assigning <ls_ref_constraints>.
              assign lr_entity->(<ls_ref_constraints>-target_property-name) to <lv_any>.
              if sy-subrc = 0.
                lv_db_where = lv_db_where && lv_db_and && `( ` && <ls_ref_constraints>-target_property-name && ` = '` && <lv_any> && `' )`.
                lv_db_and = ` and `.
              endif.
            endloop.
          endif.
        endif.
      endloop.
* return our where condition, either as is, or appended to our $filter
      if cv_db_where is not initial.
        cv_db_where = cv_db_where && ` and ` && lv_db_where.
      else.
        cv_db_where = lv_db_where.
      endif.
    endif.
  endmethod.                    "process_nav_path



get_entity(), change

Now time to insert our navigation processing into our get_entity() method. First we create a new variable to determine if we are navigating. Next we add the code to call our new method process_nav_path() passing the required parameters. Then we check to see if we are navigating, if so execute the returned OSQL where condition. Otherwise continue as we previously have. Finally we update our class attribute mv_conv_keys to false, check for source key conversion in our process_nav_path() method.

New modified method


method get_entity.
  data: lt_keys type /iwbep/t_mgw_tech_pairs,
        lv_db_where type string,
        lv_db_and type string value '',
        lv_db_select type string,
        lr_data type ref to data,
        lv_navigating type abap_bool.
  field-symbols: <ls_key> like line of lt_keys,
                 <ls_data> type any,
                 <lv_value> type any.
* initialise
  init( io_tech_request_context->get_entity_type_name( ) ).
  if mv_db_tabname is not initial.
* $select, grab fields to select if any
    lv_db_select = process_select( io_tech_request_context->get_select_entity_properties( ) ).
    lt_keys = io_tech_request_context->get_keys( ).
* read association and keys
    process_nav_path(
      exporting it_nav_path = io_tech_request_context->get_navigation_path( )
                it_source_keys = io_tech_request_context->get_source_keys( )
      importing ev_navigating = lv_navigating
      changing  cv_db_where = lv_db_where ).
    if lv_navigating = abap_false.
* create data struct to grab converted keys
      create data lr_data like cs_entity.
      assign lr_data->* to <ls_data>.
      io_tech_request_context->get_converted_keys(
        importing
          es_key_values = <ls_data> ).
* loop over keys to build where condition
      loop at lt_keys assigning <ls_key>.
        assign component <ls_key>-name of structure <ls_data> to <lv_value>.
        if sy-subrc = 0.
          lv_db_where = lv_db_where && lv_db_and && `( ` && <ls_key>-name && ` = '` && <lv_value> && `' )`.
          lv_db_and = ` and `.
        endif.
      endloop.
    endif.
    if lv_db_where is not initial.
      select single (lv_db_select)
        from (mv_db_tabname)
        into corresponding fields of cs_entity
        where (lv_db_where).
    endif.
  endif.
  mv_conv_keys = abap_false.
endmethod.


get_entityset(), change

Now let’s put the logic in our get_entityset() method. Again we will create a new variable to determine if we are navigating. Next we call our new method process_nav_path() passing the required parameters. First we leave the check for our has_count(), if we have a count we will execute with our navigated OSQL where condition. Otherwise we check to see if we are navigating, if so execute the returned OSQL where condition, else continue as we have previously. Finally again we update our class attribute mv_conv_keys to false, check for source key conversion in our process_nav_path() method.

New modified method


method get_entityset.
  data: lv_db_where type string,
        lv_db_select type string,
        lv_db_orderby type string,
        lv_top type i,
        lv_skip type i,
        lv_navigating type abap_bool.
* initialise
  init(
    exporting iv_entity_name = io_tech_request_context->get_entity_type_name( )
              iv_max_top = iv_max_top ).
  if mv_db_tabname is not initial.
* $top, $skip, process our paging
    process_paging(
      exporting io_tech_request_context = io_tech_request_context
      importing ev_top = lv_top
                ev_skip = lv_skip ).
* $filter, grab our converted filter
    lv_db_where = io_tech_request_context->get_osql_where_clause_convert( ).
* read association and keys
    process_nav_path(
      exporting it_nav_path = io_tech_request_context->get_navigation_path( )
                it_source_keys = io_tech_request_context->get_source_keys( )
      importing ev_navigating = lv_navigating
      changing  cv_db_where = lv_db_where ).
* check for $count if present just count the records
* no need to order results, or select fields!
    if io_tech_request_context->has_count( ) = abap_true.
* execute our select
      select count(*) up to lv_top rows
        into cs_response_context-count
        from (mv_db_tabname)
        where (lv_db_where).
    else.
* $select, grab fields to select if any
      lv_db_select = process_select( io_tech_request_context->get_select_entity_properties( ) ).
* $orderby grab our order
      lv_db_orderby = process_orderby( io_tech_request_context ).
      if lv_navigating = abap_true.
* we are navigating, execute select with determined lv_db_where
* from navigation!
        select (lv_db_select)
            from (mv_db_tabname)
            into corresponding fields of table ct_entityset
            where (lv_db_where)
            order by (lv_db_orderby).
      else.
* execute our select
        select (lv_db_select) up to lv_top rows
          from (mv_db_tabname)
          into corresponding fields of table ct_entityset
          where (lv_db_where)
          order by (lv_db_orderby).
        if lv_skip > 0.
          delete ct_entityset from 1 to lv_skip.
        endif.
      endif.
* $inlinecount, check for inline count and update
      if io_tech_request_context->has_inlinecount( ) = abap_true.
        cs_response_context-inlinecount = lines( ct_entityset ).
      endif.
    endif.
  endif.
  mv_conv_keys = abap_false.
endmethod.


Time to test

Activate all changes and try some of the following examples. Note some key fields may be different depending on demo data created:

/sap/opu/odata/sap/ZFLIGHT_SRV/AirlineSet(‘UA’)/ToFlightSchedules?$format=json

/sap/opu/odata/sap/ZFLIGHT_SRV/AirlineSet(‘UA’)/ToFlightSchedules?$filter=AirportFrom eq ‘FRA’&$format=json

/sap/opu/odata/sap/ZFLIGHT_SRV/AirlineSet(‘AZ’)/ToFlightSchedules(AirlineId=’AZ’,ConnectionId=’555′)?$format=json

/sap/opu/odata/sap/ZFLIGHT_SRV/AirlineSet(‘AZ’)/ToFlightSchedules/$count?$filter=CountryFrom eq ‘IT’

/sap/opu/odata/sap/ZFLIGHT_SRV/AirlineSet(‘AZ’)?$expand=ToFlightSchedules,ToFlightSchedules/ToFlights,ToFlightSchedules/ToFlights/ToPlane&$format=json

/sap/opu/odata/sap/ZFLIGHT_SRV/FlightSet(AirlineId=’AA’,ConnectionId=’17’,FlightDate=datetime’2016-01-13T00:00:00′)/ToPlane?$format=json

Final Words

Here we are at the end of another blog in the series. Hopefully you have been able to follow along and have all the examples working. Either with this set of data or your own set that you have experimented with.

Remember to try and use some views as well as transparent tables, this way we can make use of some inner joins with in the view, to include data from related tables.

Stay tuned for another blog…

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply