Reusable READ functionality. Part 2
Returning to our adventures in SAP Gateway, following on from my previous blog:
Let’s add some more of those odata operators to our generic handler.
But first… we’ll clean up some of those pesky compiler warnings 🙂 and be good citizens by passing back any exceptions. Add the following missing exception definitions as below:
Now that’s done, on to the operators.
Now let’s add support for the $select operator. This will allow us to return only properties we are interested in instead of the whole entity. We will being by defining two new custom types to store some attributes about the entity properties from the model. At the moment we are interested in storing the ABAP fieldname and if the field is defined as a key.
types: begin of ty_table_field, abap_name type fieldname, is_key type abap_bool, end of ty_table_field . types: ty_table_field_tab type hashed table of ty_table_field with unique key abap_name .
Now we have defined the types, let’s use the table type to define a class attribute to store these details.
Add a new Class attribute mt_table_fields as below.
Open up the init() method and add the below changes to the code.
Define some new data and a field symbol:
data: lt_table_properties type /iwbep/if_mgw_med_odata_types=>ty_t_mgw_odata_properties, lr_property type ref to /iwbep/cl_mgw_odata_property, ls_table_field type ty_table_field. field-symbols: <ls_property> like line of lt_table_properties.
Add the mt_table_fields class attribute to the free syntax to ensure we are all good for the next run though get_entity() or get_entityset().
free: mv_db_tabname, mt_table_fields.
The final code to add to the end of the method deals with retrieving the property details from the model definition we are interested, these being the abap_name and is_key.
* grab some table properties, we are interested in, currently only the * abap name and if the property is a key lt_table_properties = lr_table_entity->/iwbep/if_mgw_odata_entity_typ~get_properties( ). loop at lt_table_properties assigning <ls_property>. lr_property ?= <ls_property>-property. ls_table_field-abap_name = lr_property->/iwbep/if_mgw_odata_re_prop~get_abap_name( ). ls_table_field-is_key = lr_property->/iwbep/if_mgw_odata_re_prop~is_key( ). insert ls_table_field into table mt_table_fields. endloop. endmethod.
process_select() – new method, protected
Define a new protected method process_select(). This will have two parameters one being a table of strings which have been generated from a call to io_tech_request_context->get_select_entity_properties( ) and the other a returned string for our OSQL statement.
NOTE: The reason for passing the string table and not io_tech_request_context is due to this method being called from both get_entity() and get_entityset(), which both have io_tech_request_context defined differently, /IWBEP/IF_MGW_REQ_ENTITY and /IWBEP/IF_MGW_REQ_ENTITYSET respectively. These both seem to resolve back to one private method /IWBEP/CL_MGW_REQUEST-> GET_SELECT_ENTITY_PROPERTIES(), though who knows what might change in the future.
The method is quite simplistic, if the $select operator is used then the table will be filled, we loop around this and build our select string. If the table is not filled, we simply return the select all components, *.
method process_select. field-symbols: <ls_string> type string. * if $select has been supplied, then loop over table * and add list of attributes to our osql select string if it_select_table is not initial. loop at it_select_table assigning <ls_string>. rv_db_select = rv_db_select && <ls_string> && ` `. endloop. else. * else dump all properties of our entity rv_db_select = `*`. endif. endmethod. "get_db_select
Now we make a change to get_entity() to call this new method, and make use of the returned string in our select.
Create a new variable in our method to store the select string:
data: lv_db_select type string.
Make the call to our new method:
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( ) ).
Replace the the select single * with our below line:
select single (lv_db_select)
Perform the same actions as we did for get_entity() above, though this time replace the select * with:
Now let’s activate and test the $select operator, try the following:
Now it’s time to implement our ordering/sorting operator. Define some new constant attributes in the class to handle the conversion from the URI $orderby operator to the OSQL orderby syntax, as below:
process_orderby(), new method, protected
Define a new method process_orderby(). This method will define two parameters, the first being the io_tech_request_context for the entityset and the second the returned string for the OSQL statement.
NOTE: ordering is only implemented with get_entityset() so we can easily pass the io_tech_request_context, as it will always be of type /IWBEP/IF_MGW_REQ_ENTITYSET. Unlike the definition we made for process_select() above.
In this method we make a call to io_tech_request_context->get_orderby() to return our ordering properties supplied on the URI. We loop over these and add them to our ordering string, converting the $orderby asc & desc keywords to the OSQL equivalent.
The second section of this code checks to see if the $top or $skip operator have been supplied. As per odata V2 standard, if it has we must order our data to return consistent results. So if we have not passed a $orderby with a $top or $skip then let’s default to sorting by the entities key properties, making reference to the mt_table_fields is_key we built in the init() method above.
method process_orderby. data: lt_orderby type /iwbep/t_mgw_tech_order, lv_order type string. field-symbols: <ls_orderby> like line of lt_orderby, <ls_table_field> like line of mt_table_fields. lt_orderby = io_tech_request_context->get_orderby( ). * build order by osql string from order by uri table loop at lt_orderby assigning <ls_orderby>. case <ls_orderby>-order. when mc_ascending. lv_order = mc_sql_ascending. when mc_descending. lv_order = mc_sql_descending. endcase. rv_orderby = rv_orderby && <ls_orderby>-property && ` ` && lv_order && ` `. endloop. * if $top or $skip supplied and NO $orderby, then order by keys ascending if ( rv_orderby is initial ) and ( ( io_tech_request_context->get_top( ) > 0 ) or ( io_tech_request_context->get_skip( ) > 0 ) ). * loop over keys and add to ordering loop at mt_table_fields assigning <ls_table_field> where is_key = abap_true. rv_orderby = rv_orderby && <ls_table_field>-abap_name && ` ` && mc_sql_ascending && ` `. endloop. endif. endmethod. "process_orderby
Here we will create a new variable to hold the ordering string returned from our call to process_orderby().
data: lv_db_orderby type string.
Next insert our call to the new method, after the call to process_select(), passing our io_tech_request_context and receiving our ordering string.
* $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 ).
Lastly let’s add the orderby variable to the OSQL statement after the where condition.
where (lv_db_where) order by (lv_db_orderby).
Now active and test.
$top and $skip
Now we have added our ordering we can add our paging, as mentioned to return consistent results with $top and $skip we need to add some ordering.
We’ll start by adding some new attributes to our class, the first being mc_max_top. This is fall back in case no $top has been specified then we will default to mc_max_top. This can be overridden in the call to get_entityset() as we will find out shortly. The second will be the actual maximum value we’ll be limiting to, either mc_max_top or the value passed into get_entityset().
We’ll add a new parameter to the init() method iv_max_top to set our attribute above, just to be consistent and do all our initialization in the one spot.
Add the MV_MAX_TOP to the free statement to ensure we clear it out each time through, and add a line to set mv_max_top as below.
* free last table name used free: mv_db_tabname, mt_table_fields, mv_max_top. * set max top for this entityset. mv_max_top = iv_max_top.
process_paging(), new method, protected
Create a new method process_paging(). We’ll add an importing parameter io_tech_request_context and two exporting parameters ev_skip and ev_top, to be used in our OSQL statement.
First we simply grab what is being passed on the URI for $top and $skip. In the next section we make sure we have a $top greater than zero and haven’t gone over the maximum allowed. A check for $count is also added here as we do not want to limit the $top any further if someone is simply counting the number of records in a set. So leave $top as is in this case.
Funnily enough $count and $top can be used together, I find this strange if anyone knows why I’d love to know… eg try:
In the last section, if we are processing a $skip, then just add this value to our $top value so we return enough records during the select to cover the complete query.
method process_paging. * get our $top and $skip ev_top = io_tech_request_context->get_top( ). ev_skip = io_tech_request_context->get_skip( ). * force top to max allowed if not passed in, and not using $count if ( io_tech_request_context->has_count( ) = abap_false ) and ( ( ev_top <= 0 ) or ( ev_top > mv_max_top ) ). ev_top = mv_max_top. endif. * check $skip, and add to max rows if required if ev_skip > 0. ev_top = ev_top + ev_skip. endif. endmethod. "process_paging
Let’s add a new attribute to the get_entityset() method to allow us to pass the maximum number of records to return, iv_max_top. This allows as to define different maximums for different entities within our services.
Create two new variables to return the top and skip values from the call to process_paging().
data: lv_top type i, lv_skip type i.
Next we will make a call to our new method above process_paging(), passing our io_tech_request_context, top and skip.
* $orderby grab our order lv_db_orderby = process_orderby( io_tech_request_context ). * $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( ).
Finally on our select statement let’s add the syntax UP TO lv_top ROWS to limit our select to the maximum rows determined, and lastly we will add the code to handle the $skip if supplied, deleting the required number of rows from the front of the results table.
* 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.
Activate all changes and let’s test with the following examples:
$inlinecount and $count
Lastly lets add support for our counting operators.
First we will place a check to see if we are processing this URI with the $count operator. If so then let’s call the OSQL with the COUNT(*) syntax and remove our $orderby and $select processing logic as it’s not needed. We will leave the paging, as we saw in the previous example above, a $top can be used with $count? We can also still supply a $filter with $count so let’s leave that process and keep it attached to the WHERE condition.
We finally return the results of the count back into our response context in attribute count.
In the second lot of changes we handle the $inlinecount operator, here we simply count the number or rows in our results set and update the inlinecount attribute of our response context.
So the complete method now looks as follows:
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. * 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( ). * 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 ). * 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. * $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. ENDMETHOD.
Let’s activate the changes and try it out:
Entityset max records
We mentioned the new parameter iv_max_top above in the get_entityset() method. Now we can make use of this new parameter in the get_entityset() method from the data provider class. This will allow as to specify maximums per entityset across our service. As below:
get_gw_helper( )->get_entityset( exporting io_tech_request_context = io_tech_request_context iv_max_top = 15 changing ct_entityset = et_entityset cs_response_context = es_response_context ).
We have reach the end of another blog, thank you for following along. I hope it has all made sense so far and has been helpful.
Stay tuned for another blog or two to follow as we continue this adventure along the gateway…
Keep coding, sharing and staying happy. 🙂