
RAP - Unmanaged (Local)
In this article an example for the implementation of an unmanaged RAP object based on a locally available API.
Table of contents
In the last article we described how the unmanaged scenario for the ABAP RESTful Programming Model works in theory. In this article, we'll look at an implementation based on objects already implemented in the system that serve as local APIs for us.
Local API
To understand the local API here is a brief explanation of the objects and the implementations that are present. There is already a data model in the system which is based on the table ZBS_DMO_UN_DATA and has a corresponding DAO class (Data Access Object).
For the class there is the interface ZIF_BS_DEMO_RAP_DATA_HANDLER, which provides the CRUDQ methods and data types:
INTERFACE zif_bs_demo_rap_data_handler
PUBLIC.
TYPES:
tt_r_key TYPE RANGE OF zbs_dmo_un_data-gen_key,
tt_r_text TYPE RANGE OF zbs_dmo_un_data-text,
tt_r_date TYPE RANGE OF zbs_dmo_un_data-cdate,
ts_data TYPE zbs_dmo_un_data,
tt_data TYPE STANDARD TABLE OF ts_data WITH EMPTY KEY.
METHODS:
query
IMPORTING it_r_key TYPE tt_r_key OPTIONAL
it_r_text TYPE tt_r_text OPTIONAL
it_r_date TYPE tt_r_date OPTIONAL
RETURNING VALUE(rt_result) TYPE tt_data,
read
IMPORTING id_key TYPE zbs_dmo_unmgnd-gen_key
RETURNING VALUE(rs_result) TYPE ts_data,
modify
IMPORTING is_data TYPE ts_data
RETURNING VALUE(rd_result) TYPE abap_boolean,
delete
IMPORTING id_key TYPE zbs_dmo_unmgnd-gen_key
RETURNING VALUE(rd_result) TYPE abap_boolean.
ENDINTERFACE.
Then the following implementation of the class ZCL_BS_DEMO_RAP_DATA_HANDLER, which provides the implementation and logic:
CLASS zcl_bs_demo_rap_data_handler DEFINITION
PUBLIC
FINAL
CREATE PRIVATE.
PUBLIC SECTION.
INTERFACES zif_bs_demo_rap_data_handler.
CLASS-METHODS create_instance
RETURNING VALUE(ro_result) TYPE REF TO zif_bs_demo_rap_data_handler.
ENDCLASS.
CLASS zcl_bs_demo_rap_data_handler IMPLEMENTATION.
METHOD create_instance.
ro_result = NEW zcl_bs_demo_rap_data_handler( ).
ENDMETHOD.
METHOD zif_bs_demo_rap_data_handler~delete.
DELETE FROM zbs_dmo_un_data WHERE gen_key = @id_key.
rd_result = xsdbool( sy-subrc = 0 ).
ENDMETHOD.
METHOD zif_bs_demo_rap_data_handler~modify.
DATA(ls_data) = is_data.
GET TIME STAMP FIELD ls_data-last_changed.
IF ls_data-gen_key IS INITIAL.
TRY.
ls_data-gen_key = cl_system_uuid=>create_uuid_x16_static( ).
IF ls_data-cdate IS INITIAL.
ls_data-cdate = cl_abap_context_info=>get_system_date( ).
ENDIF.
CATCH cx_uuid_error.
rd_result = abap_false.
RETURN.
ENDTRY.
INSERT zbs_dmo_un_data FROM @ls_data.
ELSE.
UPDATE zbs_dmo_un_data FROM @ls_data.
ENDIF.
rd_result = xsdbool( sy-subrc = 0 ).
ENDMETHOD.
METHOD zif_bs_demo_rap_data_handler~query.
SELECT FROM zbs_dmo_un_data
FIELDS *
WHERE gen_key IN @it_r_key
AND text IN @it_r_text
AND cdate IN @it_r_date
INTO TABLE @rt_result.
ENDMETHOD.
METHOD zif_bs_demo_rap_data_handler~read.
SELECT SINGLE FROM zbs_dmo_un_data
FIELDS *
WHERE gen_key = @id_key
INTO @rs_result.
ENDMETHOD.
ENDCLASS.
The following picture emerges when we look at the existing objects:
Structure
The structure describes the rough structure of the objects and interdependencies. However, we will not go into the individual details here, but refer to the basics of RAP here if you want to understand the objects and structures.
Table
The structure of our unmanaged object is based on a "dummy" table that we use for modeling. This table is not 1:1 comparable to the data-holding table, but only includes the fields that we need for our RAP object:
@EndUserText.label : 'Unmanaged Data'
@AbapCatalog.tableCategory : #TRANSPARENT
define table zbs_dmo_unmgnd {
key client : abap.clnt not null;
key gen_key : sysuuid_x16 not null;
text : abap.char(50);
cdate : abap.dats;
}
CDS Views
Accordingly, we set a root view on the table, which takes up the fields of the table and makes them available. We set the corresponding projection view on this, which then provides the interface to the application:
@EndUserText.label: 'Unmanaged Consumption'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@ObjectModel.query.implementedBy: 'ABAP:ZCL_BS_DEMO_UNMANAGED_QUERY'
@UI: {
headerInfo: {
typeName: 'Unmanaged',
typeNamePlural: 'Unmanaged',
title: { value: 'Description' }
}
}
define root view entity ZBS_C_DMOUnmanaged
provider contract transactional_query
as projection on ZBS_R_DMOUnmanaged
{
@UI.facet : [
{
id : 'FacetData',
label : 'Data',
type : #FIELDGROUP_REFERENCE,
targetQualifier: 'DATA'
}
]
@UI.lineItem: [{ position: 10, label: 'Key' }]
@UI.fieldGroup: [{ position: 10, label: 'Key' }]
key TableKey,
@UI.selectionField: [{ position: 10 }]
@UI.lineItem: [{ position: 20, label: 'Text' }]
@UI.fieldGroup: [{ position: 20, label: 'Text', qualifier: 'DATA' }]
Description,
@UI.selectionField: [{ position: 20 }]
@UI.lineItem: [{ position: 30, label: 'Created at' }]
@UI.fieldGroup: [{ position: 20, label: 'Text', qualifier: 'DATA' }]
CreationDate
}
In this case, we also mix the UI annotations into the view and do not create an extra metadata extension. Both scenarios are possible and lead to the result that our application receives a configured Fiori Elements interface.
Behavior definition
There are differences in the implementation of the behavior definition, as we already showed in the last article. The projection does not differ from the managed approach, so here is the definition at the lowest level:
unmanaged implementation in class zbp_bs_demo_unmanaged unique;
strict ( 1 );
define behavior for ZBS_R_DMOUnmanaged alias Unmanaged
lock master
authorization master ( instance )
{
create;
update;
delete;
field ( readonly ) TableKey;
mapping for zbs_dmo_unmgnd
{
TableKey = gen_key;
Description = text;
CreationDate = cdate;
}
}
The key is assigned automatically by the application, so it should only be displayed in the list and completely hidden on the object page.
More objects
Finally, we now create the service definition and the service binding in order to provide a UI OData service and to be able to carry out a first test with the app. We will create and consume an OData version 2 for the test.
Test Preview
If we now start the Fiori Elements Preview, then we get the following app in the preview, all important functions seem to be available for now.
When executing the selection using the "Go" button, however, no data is displayed. The creation of new data records does not work yet either.
Read
We now have to implement the reading of the data ourselves. The READ method would actually be suitable here in the behavior implementation, the class ZBP_BS_DEMO_UMMANAGED, but this is there for reading individual records and not for the query operation. To do this, we define the following annotation in the CDS of the projection to implement a reading class:
@ObjectModel.query.implementedBy: 'ABAP:ZCL_BS_DEMO_UNMANAGED_QUERY'
The class is always called when the entity is requested via the OData service and returns the corresponding data. We already looked at this concept in the article on the custom entity. The following implementation for the class:
CLASS zcl_bs_demo_unmanaged_query DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
PRIVATE SECTION.
METHODS get_data_from_request
IMPORTING io_request TYPE REF TO if_rap_query_request
RETURNING VALUE(rt_result) TYPE zif_bs_demo_rap_data_handler=>tt_data
RAISING cx_rap_query_filter_no_range.
ENDCLASS.
CLASS zcl_bs_demo_unmanaged_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
DATA lt_output TYPE STANDARD TABLE OF ZBS_C_DMOUnmanaged.
DATA(lt_database) = get_data_from_request( io_request ).
lt_output = CORRESPONDING #( lt_database MAPPING TableKey = gen_key Description = text CreationDate = cdate ).
IF io_request->is_data_requested( ).
io_response->set_data( lt_output ).
ENDIF.
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lines( lt_output ) ).
ENDIF.
ENDMETHOD.
METHOD get_data_from_request.
DATA lt_r_key TYPE zif_bs_demo_rap_data_handler=>tt_r_key.
DATA lt_r_text TYPE zif_bs_demo_rap_data_handler=>tt_r_text.
DATA lt_r_date TYPE zif_bs_demo_rap_data_handler=>tt_r_date.
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
DATA(ld_offset) = io_request->get_paging( )->get_offset( ).
DATA(ld_pagesize) = io_request->get_paging( )->get_page_size( ).
LOOP AT lt_filter INTO DATA(ls_filter).
CASE ls_filter-name.
WHEN 'TABLEKEY'.
lt_r_key = CORRESPONDING #( ls_filter-range ).
WHEN 'DESCRIPTION'.
lt_r_text = CORRESPONDING #( ls_filter-range ).
WHEN 'CREATIONDATE'.
lt_r_date = CORRESPONDING #( ls_filter-range ).
ENDCASE.
ENDLOOP.
rt_result = zcl_bs_demo_rap_data_handler=>create_instance( )->query( it_r_key = lt_r_key
it_r_text = lt_r_text
it_r_date = lt_r_date ).
ENDMETHOD.
ENDCLASS.
For the implementation we unpack the request (IO_REQUEST) and get the filters from the frontend. We can then pass this to the API to get our data. Afterwards, these must be mapped from the database to the Core Data Service and then we transfer the data to the response (IO_RESPONSE). If we now click on the "GO" button in the application, we get the following result, the data is now read:
Buffer
So far, all methods have been implemented for the behavior implementation, but what is missing is the buffer that takes care of the data records before they are passed to the storage sequence. We have to develop this buffer ourselves, but there is also a best practice for implementation. We implement the buffer as a local class with static attributes, for our scenario the class would look like this:
CLASS lcl_data_buffer DEFINITION.
PUBLIC SECTION.
CLASS-DATA gt_create TYPE zif_bs_demo_rap_data_handler=>tt_data.
CLASS-DATA gt_update TYPE zif_bs_demo_rap_data_handler=>tt_data.
CLASS-DATA gt_delete TYPE zif_bs_demo_rap_data_handler=>tt_data.
ENDCLASS.
In principle, the class can also be implemented via an attribute with different characteristics, there are no limits to order and imagination. The attributes will take over the data at runtime and make it available to the storage sequence.
Create
Before we create the data, let's take a look at the content that we get when creating a data set in the method:
With ENTITIES we get a table whose structure contains the data for the system plus additional information such as the %CID and the %CONTROL structure. The CID is the unique key within the processing, so we can match other related records as well. The corresponding fields that can be edited in this process step are active in the CONTROL structure. We could also derive the assignment from this. The implementation of the Create method now looks like this:
METHOD create.
INSERT LINES OF
CORRESPONDING zif_bs_demo_rap_data_handler=>tt_data( entities MAPPING cdate = CreationDate text = Description )
INTO TABLE lcl_data_buffer=>gt_create.
ENDMETHOD.
Basically, after mapping, we transfer the data to our buffer table for the new data records, there is nothing more to consider here.
Update
For updating the data we need to implement some more logic. We are handed back our entity, but only with the key fields and the changed data. This means that in the next step we first have to compare what has actually changed. The implementation would now look like this:
METHOD update.
DATA(lo_data_handler) = zcl_bs_demo_rap_data_handler=>create_instance( ).
LOOP AT entities INTO DATA(ls_entity).
DATA(ls_original) = lo_data_handler->read( ls_entity-TableKey ).
IF ls_entity-%control-Description = if_abap_behv=>mk-on.
ls_original-text = ls_entity-Description.
ENDIF.
IF ls_entity-%control-CreationDate = if_abap_behv=>mk-on.
ls_original-cdate = ls_entity-CreationDate.
ENDIF.
INSERT ls_original INTO TABLE lcl_data_buffer=>gt_update.
ENDLOOP.
ENDMETHOD.
First of all we read the original data record using the key, then we check the CONTROL structure to see whether the field has been changed and then adopt the new content into the data. At the end we append the record to the buffer for follow-up processing.
Delete
The last step, deleting, is now implemented a little differently, since we don't get the whole entity, just the key. Basically we only have to adjust the mapping here and transfer the data to the buffer:
METHOD delete.
INSERT LINES OF
CORRESPONDING zif_bs_demo_rap_data_handler=>tt_data( keys MAPPING gen_key = TableKey )
INTO TABLE lcl_data_buffer=>gt_delete.
ENDMETHOD.
Save
Finally, we can now implement the SAVE method. This is about reading the buffer and performing appropriate actions on the various entries:
METHOD save.
DATA(lo_data_handler) = zcl_bs_demo_rap_data_handler=>create_instance( ).
LOOP AT lcl_data_buffer=>gt_create INTO DATA(ls_create).
lo_data_handler->modify( ls_create ).
ENDLOOP.
LOOP AT lcl_data_buffer=>gt_update INTO DATA(ls_update).
lo_data_handler->modify( ls_update ).
ENDLOOP.
LOOP AT lcl_data_buffer=>gt_delete INTO DATA(ls_delete).
lo_data_handler->delete( ls_delete-gen_key ).
ENDLOOP.
CLEAR: lcl_data_buffer=>gt_create, lcl_data_buffer=>gt_update, lcl_data_buffer=>gt_delete.
ENDMETHOD.
We instantiate the data handler and process all the records from the buffer passing them to our existing logic. Finally, we clear the buffer and we're done with that. The commit work for persisting the data records is set by the RAP framework, we don't have to worry about it here.
Full example
You can see the full example, all objects and changes via the commit on GitHub. The examples shown here are only excerpts from the complete source code and the objects that we have created.
Conclusion
In this first connection to a local API, we have introduced you to the unmanaged approach to a RAP object. This allows you to easily migrate most of your existing source code to RAP and thus build on the latest technologies without having to rework everything.