
RAP - Unmanaged (Lokal)
In diesem Artikel ein Beispiel zur Implementierung eines Unmanaged RAP Objekts auf Basis einer lokal vorhandenen API.
Inhaltsverzeichnis
Im letzten Aritkel hatten wir beschrieben, wie das Unmanaged Szenario für das ABAP RESTful Programming Model in der Theorie funktioniert. In diesem Artikel schauen wir uns einmal eine Implementierung an, die auf Objekten basiert, die bereits im System implementiert wurden und uns als lokale APIs dienen.
Lokale API
Um die lokale API zu verstehen hier eine kurze Erklärung der Objekte und er Implementierungen die vorhanden sind. Es gibt bereits ein Datenmodell im System, welches auf der Tabelle ZBS_DMO_UN_DATA basiert und eine entsprechende DAO-Klasse (Data Access Object) besitzt.
Für die Klasse gibt es das Interface ZIF_BS_DEMO_RAP_DATA_HANDLER, welches die CRUDQ Methoden und Datentypen zur Verfügung stellt:
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.
Dazu dann noch die folgende Implementierung der Klasse ZCL_BS_DEMO_RAP_DATA_HANDLER, welches die Implementierung und Logik zur Verfügung stellt:
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.
Dabei ergibt sich grob das folgende Bild, wenn wir auf die vorhandenen Objekte schauen:
Aufbau
Der Aufbau beschreibt den groben Aufbau der Objekte und Abhängigkeiten untereinander. Dabei werden wir allerdings nicht in die einzelnen Details eingehen, sondern verweisen hier auf die Grundlagen von RAP, wenn du die Objekte und Strukturen verstehen möchtest.
Tabelle
Der Aufbau unseres Unmanaged Objekts basiert auf einer "Dummy" Tabelle, die wir für die Modellierung verwenden. Diese Tabelle ist nicht 1:1 mit der datenhaltenden Tabelle vergleichbar, sondern umfasst nur die Felder, die wir für unser RAP Objekt benötigen:
@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
Entsprechend setzen wir auf die Tabelle einen Root View, der die Felder der Tabelle aufnimmt und zur Verfügung stellt. Auf diesen setzen wir den entsprechenden Projektions-View, der dann die Schnittstelle zur Anwendung zur Verfügung stellt:
@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 diesem Fall vermischen wir hier auch gleich die UI Annotationen mit in den View und legen nicht extra eine Metadaten Erweiterung an. Beide Szenarien sind soweit möglich und führen zum Ergebnis, dass unsere Anwendung eine konfigurierte Fiori Elements Oberfläche erhält.
Verhaltensdefinition
Unterschiede gibt es bei der Implementierung der Verhaltensdefinition, wie wir bereits im letzten Artikel gezeigt hatten. Die Projektion unterscheidet sich nicht vom Managed Ansatz, deshalb hier nur noch einmal die Definition auf unterster Ebene:
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;
}
}
Der Schlüssel wird von der Anwendung automatisch vergeben, deshalb sollte dieser nur auf Anzeige in der Liste stehen und auf der Objektseite komplett ausgeblendet werden.
Weitere Objekte
Zum Abschluss legen wir nun noch die Service Definition und das Service Binding an, um einen UI OData Service zur Verfügung zu stellen und einen ersten Test mit der App durchführen zu können. Dabei werden wir für den Test ein OData Version 2 anlegen und konsumieren.
Test Preview
Starten wir nun die Fiori Elements Preview, dann erhalten wir die folgende App in der Vorschau, alle wichtigen Funktionen scheinen erst einmal verfügbar zu sein.
Beim Ausführen der Selektion über den Button "Go", werden allerdings keine Daten angezeigt. Auch das Anlegen von neuen Datensätzen funktioniert noch nicht.
Lesen
Das Lesen der Daten müssen wir nun selbst implementieren. Hier würde sich in der Verhaltensimplementierung, der Klasse ZBP_BS_DEMO_UMMANAGED, eigentlich die READ Methode anbieten, diese ist aber zum Lesen von Einzelsätzen da und nicht für die Query Operation. Dazu definieren wir im CDS der Projektion die folgende Annotation, um eine Leseklasse zu implementieren:
@ObjectModel.query.implementedBy: 'ABAP:ZCL_BS_DEMO_UNMANAGED_QUERY'
Die Klasse wird immer aufgerufen, wenn über den OData Service die Entität angefragt wird und liefert entsprechende Daten zurück. Bereits im Artikel zur Custom Entity haben wir uns dieses Konzept angeschaut. Dazu die folgende Implementierung zur Klasse:
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.
Für die Implementierung packen wir die Anfrage (IO_REQUEST) aus und besorgen uns die Filter aus dem Frontend. Diese können wir dann an die API übergeben, um so unsere Daten zu erhalten. Im Anschluss müssen diese von der Datenbank auf den Core Data Service gemappt werden und im Anschluss übergeben wir die Daten an die Antwort (IO_RESPONSE). Wenn wir nun in der Anwendung auf den "GO"-Button klicken, erhalten wir das folgende Ergebnis, die Daten werden nun gelesen:
Puffer
Für die Verhaltensimplementierung wurden so weit alle Methoden implementiert, was aber fehlt ist der Puffer, der sich um die die Datensätze kümmert, bevor diese an die Speichersequenz übergeben werden. Diesen Puffer müssen wir selbst entwickeln, dazu gibt es allerdings auch eine Best-Practise für die Umsetzung. Wir implementieren den Puffer als lokale Klasse mit statischen Attributen, für unser Szenario würde die Klasse wie folgt aussehen:
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.
Grundsätzlich kann die Klasse auch über ein Attribut mit unterschiedlichen Merkmalen umgesetzt werden, hier sind der Ordnung und der Fantasie keine Grenzen gesetzt. Die Attribute werden zur Laufzeit die Daten übernehmen und der Speichersequenz zur Verfügung stellen.
Anlegen
Bevor wir die Daten anlegen, schauen wir uns einmal den Inhalt an, den wir bei der Anlage eines Datensatzes in die Methode bekommen:
Wir bekommen mit ENTITIES eine Tabelle übergeben, dessen Struktur die Daten enthält für die Anlage plus Zusatzinformationen, wie die %CID und die %CONTROL Struktur. Die CID ist der eindeutige Schlüssel innerhalb der Verarbeitung, sodass wir auch andere verknüpfte Datensätze zuordnen kann. In der CONTROL Struktur sind die entsprechenden Felder aktiv, die in diesem Prozessschritt bearbeitet werden können. Hierrüber könnten wir auch die Zuordnung ableiten. Die Implementierung der Create Methode sieht nun wie folgt aus:
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.
Im Grunde übernehmen wir die Daten nach dem Mapping in unsere Puffertabelle für die neuen Datensätze, mehr ist hier nicht zu beachten.
Aktualisieren
Für das Aktualisieren der Daten müssen wir etwas mehr Logik implementieren. Wir bekommen wieder unsere Entität übergeben, aber nur mit den Schlüsselfeldern und den geänderten Daten. Das heißt im nächsten Schritt müssen wir zuerst abgleichen, was sich eigentlich geändert hat. Die Implementierung würde nun wie folgt aussehen:
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.
Zuerst einmal lesen wir über den Schlüssel den Originaldatensatz nach, dann prüfen wir die CONTROL Struktur, ob das Feld geändert wurde und übernehmen dann den neuen Inhalt in die Daten. Am Ende hängen wir den Datensatz an den Puffer für die Folgeverarbeitung.
Löschen
Der letzte Schritt, das Löschen, wird nun etwas anders implementiert, da wir nicht die ganze Entität übergeben bekommen, sondern nur den Schlüssel. Im Grunde müssen wir hier nur das Mapping anpassen und übergeben die Daten an den Puffer:
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.
Speichern
Zum Abschluss können wir nun die SAVE Methode implementieren. Hier geht es darum den Puffer zu lesen und entsprechende Aktionen auf die verschiedenen Einträge durchzuführen:
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.
Wir instanziieren den Daten Handler und verarbeiten alle Datensätze aus dem Puffer, in dem wir sie an unsere bestehende Logik übergeben. Zum Abschluss löschen wir den Puffer und sind damit fertig. Der Commit Work zur Persistierung der Datensätze wird durch das RAP Framework gesetzt, hier müssen wir uns nicht darum kümmern.
Komplettes Beispiel
Das vollständige Beispiel, alle Objekte und Änderungen kannst du über den Commit auf GitHub einsehen. Die hier gezeigten Beispiele sind nur Auszüge aus dem vollständigen Quellcode und den Objekten, die wir angelegt haben.
Fazit
In dieser ersten Anbindung einer lokalen API haben wir dir den Unmanaged Ansatz für ein RAP Objekt näher gebracht. Damit kannst du einen Großteil deines bestehenden Quellcodes leicht nach RAP migrieren und so auf den neuesten Technologien aufbauen, ohne alles überarbeiten zu müssen.