
ABAP Tipp - Generische Query Implementierung
Die immer wieder gleiche Implementierung in Query Klassen für Custom Entities in RAP? Es wird mal Zeit für eine wiederverwendebare Komponente.
Inhaltsverzeichnis
In diesem Artikel schauen wir uns eine Implementierung an, um über die Custom Entity eine wiederverwendbare Abfrage zu starten und einfach an Daten aus einem OData zu kommen, egal auf welchem System die Daten liegen.
Einleitung
Über eine Custom Entity besteht eine einfache Möglichkeit in einem System eine Wertehilfe für ein anderes System zu erstellen. Dabei werden zur Laufzeit die Daten aus einem Remote System gelesen und dem Anwender zur Verfügung gestellt. Damit lassen sich sehr einfach Remote Szenarien umsetzen. allerdings ist die Implementierung des Queries recht aufwändig, vor allem wenn du es immer wieder machen musst.
Query
Die Query Klasse wird im Core Data Service über die Annotation "@ObjectModel.query.implementedBy: 'ABAP:xxx'" definiert und zur Laufzeit wird im SADL Framework die Implementierung aufgerufen. Dabei müssen wir die Methode SELECT des Interfaces IF_RAP_QUERY_PROVIDER implementieren.
Neben der eigentlichen Methode müssen von dem Objekt IO_REQUEST einige Methoden (GET_SORT_ELEMENTS, GET_PAGING) aufgerufen werden, da es sonst zu einem Dump kommt. Weitere Infos dazu findest du im Deep Dive zu Custom Entities.
Konzept
In diesem Artikel planen wir eine generische Implementierung der Abfrage in verschiedenen Konstellationen, um die Funktionen bei weiteren Implementierungen und Services einfach wiederverwenden zu können. Damit solltest du dann bei den weiteren Projekten an der Implementierung sparen.
Architektur
Für die Architektur verwenden wir das Factory Pattern, um das Thema Entkopplung und Testbarkeit mit abzudecken. Zusätzlich benötigen wir noch eine Ausnahmeklasse, wenn bestimmte Schritte nicht umgesetzt werden können oder es zu Abbrüchen in der Abfrage kommt.
Da so eine Komponente normalerweise übergreifend entwickelt wird und wir uns in ABAP Cloud mit dem Konzept der Software Komponenten beschäftigen müssen, sollten wir das Thema Entkopplung und Verwendung über SWC berücksichtigen.
Konfiguration
Damit wir eine Konfiguration für den Zugriff haben, definieren wir uns diese als Struktur im Interface. Der Vorteil einer Struktur ist, dass wir immer nur einmal Daten durch die Klasse schleusen müssen, ohne jeden einzelnen Parameter zu definieren. Damit ist die Schnittstelle jederzeit erweiterbar, ohne dass wir viel Code anpassen müssen.
TYPES property TYPE STANDARD TABLE OF if_com_arrangement_factory=>ty_query_param_prop WITH EMPTY KEY.
TYPES:
BEGIN OF arrangement,
comm_scenario TYPE if_com_management=>ty_cscn_id,
service_id TYPE if_com_management=>ty_cscn_outb_srv_id,
comm_system_id TYPE if_com_management=>ty_cs_id,
property TYPE property,
END OF arrangement.
TYPES: BEGIN OF configuration,
arrangement TYPE arrangement,
cloud_destination TYPE string,
consumption_model TYPE /iwbep/if_cp_runtime_types=>ty_proxy_model_id,
service_root TYPE string,
client TYPE string,
language TYPE sy-langu,
protocol TYPE protocol_intern,
END OF configuration.
Grundsätzlich kannst du dich für ein Objekt (Klasse/Interface) oder eine Struktur entscheiden. Hier solltest du einfach für dich entscheiden, benötigst du auch noch so etwas wie eine Validierung oder zusätzliche Methoden. Im einfachen Fall reicht auch eine Struktur, um die Daten zu transportieren.
Methoden
Bei der Implementierung entscheiden wir uns für drei Methoden, die komplette Verarbeitung der Daten mit Request und Response, die Bearbeitung per Request und die manuelle Übergabe aller Werte. So sind wir später am flexibelsten und können steuern, welche Bestandteile wir an die Komponente abgeben. In jedem Fall müssen wir die Daten per CHANGING Parameter weitergeben, damit die Information durch unseren Code geschleust wird.
METHODS read_odata_by_values
IMPORTING setting TYPE setting_by_value
CHANGING business_data TYPE ANY TABLE
!count TYPE count
RAISING zcx_bs_demo_provider_error.
METHODS read_odata_by_request
IMPORTING setting TYPE setting_by_request
CHANGING business_data TYPE ANY TABLE
!count TYPE count
RAISING zcx_bs_demo_provider_error.
METHODS read_odata_with_response
IMPORTING setting TYPE setting_with_response
CHANGING business_data TYPE ANY TABLE
RAISING zcx_bs_demo_provider_error.
Implementierung
In diesem Kapitel schauen wir uns nun die Implementierung der verschiedenen Szenarien an und welche Methode welche Aufgabe hat.
Hierarchie
Im Grunde rufen sich die Methoden gegenseitig in einer Hierarchie auf und verfolgen spezifische Aufgaben. Je nachdem wieviel du von den folgenden Schritten abgeben möchtest, startest du mit einer anderen Methode:
- READ_ODATA_WITH_RESPONSE - Ruft die nächste Methode auf und befüllt das Ergebnis direkt in den Repsonse.
- READ_ODATA_BY_REQUEST - Entpackt das Request Objekt, löscht nicht vorhandene Felder und ergänzt benötigte Felder. Definiert außerdem die Pagination für die Anfrage.
- READ_ODATA_BY_VALUES - Führt die finale Abfrage durch, es wird der Client erzeugt und der HTTP Request vorbereitet. Das Ergebnis wird dann in die Variablen geschrieben.
Destination
Im ersten Schritt benötigen wir eine Destination für die Abfrage. Über die Konfiguration hat der User aktuell die Möglichkeit eine Cloud Destination (Destination Service) oder ein Communication Arrangement mitzugeben.
IF configuration-arrangement IS NOT INITIAL.
result = cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = configuration-arrangement-comm_scenario
service_id = configuration-arrangement-service_id
comm_system_id = determine_communication_system( ) ).
ELSEIF configuration-cloud_destination IS NOT INITIAL.
result = cl_http_destination_provider=>create_by_cloud_destination(
i_name = configuration-cloud_destination
i_authn_mode = if_a4c_cp_service=>service_specific ).
ENDIF.
Beim Communication Arrangement können wir theoretisch über die "Additional Properties" mehrere Communication Systems hinterlegen. Dazu implementieren wir noch eine Ermittlung des richtigen Systems über die Standard API.
DATA(query) = VALUE if_com_arrangement_factory=>ty_query(
cscn_id_range = VALUE #( ( sign = 'I' option = 'EQ' low = configuration-arrangement-comm_scenario ) )
ca_property = configuration-arrangement-property ).
DATA(arrangement_factory) = cl_com_arrangement_factory=>create_instance( ).
arrangement_factory->query_ca( EXPORTING is_query = query
IMPORTING et_com_arrangement = DATA(systems) ).
RETURN systems[ 1 ]->get_comm_system_id( ).
Client
Im nächsten Schritt erzeugen wir uns den passenden Client für unsere Abfrage. Über die Konfiguration erzeugen wir uns das passende Objekt für OData v2 oder v4. Über die Konstanten könnten wir auch noch andere Clients implementieren, wenn wir sie bräuchten.
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
CASE configuration-protocol.
WHEN zif_bs_demo_service_prov=>protocol-odata_v2.
result = /iwbep/cl_cp_factory_remote=>create_v2_remote_proxy(
is_proxy_model_key = VALUE #( repository_id = 'DEFAULT'
proxy_model_id = configuration-consumption_model
proxy_model_version = '0001' )
io_http_client = http_client
iv_relative_service_root = configuration-service_root ).
WHEN zif_bs_demo_service_prov=>protocol-odata_v4.
result = /iwbep/cl_cp_factory_remote=>create_v4_remote_proxy(
is_proxy_model_key = VALUE #( repository_id = 'DEFAULT'
proxy_model_id = configuration-consumption_model
proxy_model_version = '0001' )
io_http_client = http_client
iv_relative_service_root = configuration-service_root ).
ENDCASE.
Wir sollten nun alle Objekte haben, um mit der Implementierung der Methoden zu beginnen.
READ_ODATA_WITH_RESPONSE
Die Methode ruft die Folgemethode READ_ODATA_BY_REQUEST auf und übergibt das Ergebnis dann direkt an den Response. Damit nimmt uns die Methode die Zuweisung ab, da wir zuvor prüfen müssen, ob die Daten auch vom Request angefordert wurden.
IF setting-request->is_total_numb_of_rec_requested( ).
setting-response->set_total_number_of_records( local_count ).
ENDIF.
IF setting-request->is_data_requested( ).
setting-response->set_data( business_data ).
ENDIF.
READ_ODATA_BY_REQUEST
Im ersten Schritt zerlegen wir den Request und holen uns alle Informationen für unsere Abfrage, dazu befüllen wir die Struktur für den nächsten Schritt.
DATA(local_setting) = CORRESPONDING zif_bs_demo_service_prov=>setting_by_value( setting ).
local_setting-filter_condition = setting-request->get_filter( )->get_as_ranges( ).
local_setting-requested_elements = setting-request->get_requested_elements( ).
local_setting-sort_order = setting-request->get_sort_elements( ).
local_setting-is_data_requested = setting-request->is_data_requested( ).
local_setting-is_count_requested = setting-request->is_total_numb_of_rec_requested( ).
Bevor wir die Folgemethode READ_ODATA_BY_VALUES aufrufen, passen wir noch die entsprechenden Felder an, die in der Anfrage nicht mehr benötigt werden. Dazu ergänzen wir noch Felder, die wir vielleicht für andere Ableitungen benötigt.
LOOP AT setting-delete_fields REFERENCE INTO DATA(field_for_deletion).
DELETE local_setting-filter_condition WHERE name = field_for_deletion->*.
DELETE local_setting-requested_elements WHERE table_line = field_for_deletion->*.
DELETE local_setting-sort_order WHERE element_name = field_for_deletion->*.
ENDLOOP.
LOOP AT setting-read_fields REFERENCE INTO DATA(field_to_read).
INSERT field_to_read->* INTO TABLE local_setting-requested_elements.
ENDLOOP.
Unsere Custom Entity hält vielleicht auch virtuelle Felder oder Felder, die auf einem anderen Weg ermittelt werden. Wenn der Anwender in der App die Felder filtert, einblendet oder sortiert, werden sie in den Request aufgenommen. Wenn wir den Request gegen unseren OData Service durchführen, würde diese Felder zu einem Fehler führen.
READ_ODATA_BY_VALUES
Die letzte Methode baut den eigentlichen HTTP Request auf und ruft unsere Daten ab. Dazu erzeugen wir uns eine Destination und einen OData Request.
DATA(odata_client) = create_client( ).
DATA(odata_request) = odata_client->create_resource_for_entity_set( setting-entity_name )->create_request_for_read( ).
Im nächsten Schritt bereiten wir die Anfrage vor und ergänzen den Filter, die Elemente, weitere Optionen und Einschränkungen. Bei den zusätzlichen Optionen setzen wir den Mandanten und die Sprache als zusätzliche Option, wenn das in den Einstellungen gewünscht ist.
set_filter_for_request( odata_request = odata_request
setting = setting ).
set_elements_for_request( odata_request = odata_request
setting = setting ).
set_options_for_request( odata_request = odata_request
setting = setting ).
set_query_options_for_request( odata_request ).
Sind wir damit fertig, können wir die Anfrage ausführen und weisen das Ergebnis den Variablen zu.
DATA(odata_response) = odata_request->execute( ).
IF setting-is_data_requested = abap_true.
odata_response->get_business_data( IMPORTING et_business_data = business_data ).
ENDIF.
IF setting-is_count_requested = abap_true.
count = odata_response->get_count( ).
ENDIF.
Damit ist die Grundlage für die Verwendung gelegt und in einem der nächsten Artikel wird die Klasse zum Einsatz kommen.
Komplettes Beispiel
Alle Objekte findest du bei uns im GitHub Repository im Paket ZBS_DEMO_RAP_UTILITY. Der oben gezeigte Quellcode zeigt immer nur Ausschnitte der Implementierung. Grundsätzlich kannst du andere Anforderungen haben und solltest die Implementierung entsprechend für dich ändern. Zu 99% reicht uns die aktuelle Implementierung für Leseszenarien.
Fazit
Eine generische Implementierung zum Lesen von Remote Daten zu haben, spart viel Zeit und Entwicklungsarbeit. Die Besonderheit im Zusammenhang mit Software Komponenten solltest du allerdings beachten.