RAP mit Custom Entities: Externe Datenquellen einbinden

kategorie
RAP
Veröffentlicht
autor
Johannes

Custom Entities ermöglichen es, externe Datenquellen in RAP-basierte Anwendungen einzubinden. Anders als CDS Views, die auf Datenbanktabellen basieren, werden Custom Entities durch ABAP-Code mit Daten befüllt.

Anwendungsfälle für Custom Entities

Custom Entities sind ideal für:

  • Externe APIs - REST-Services, SOAP-Webservices, OData-Services
  • Legacy-Systeme - Funktionsbausteine, BAPIs, Remote Function Calls
  • Berechnete Daten - Komplexe Aggregationen, die nicht in CDS möglich sind
  • Nicht-HANA-Quellen - Dateien, externe Datenbanken via ADBC
  • Virtuelle Daten - Daten, die erst zur Laufzeit generiert werden

Unterschied zu CDS Views

AspektCDS ViewCustom Entity
DatenquelleDatenbanktabellenABAP-Code
SQL-PushdownJaNein
DatenmodellierungDeklarativImperativ
PerformanceOptimiert durch HANAAbhängig von Implementation
FlexibilitätEingeschränktVollständig
WartungsaufwandGeringHöher

CDS Custom Entity Definition

Eine Custom Entity wird im CDS definiert, aber die Daten kommen aus einer ABAP-Klasse:

@EndUserText.label: 'Externe Produkte'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_PRODUCT_QUERY'
define custom entity ZI_ExternalProduct
{
key ProductId : abap.char(10);
ProductName : abap.char(100);
Category : abap.char(40);
Price : abap.dec(13,2);
Currency : abap.cuky;
Stock : abap.int4;
Supplier : abap.char(80);
LastUpdated : abap.dats;
}

Wichtige Annotationen

AnnotationBeschreibung
@ObjectModel.query.implementedByVerweist auf die Query-Implementierungsklasse

Die Klasse muss mit dem Präfix ABAP: angegeben werden.

Query Implementation Class

Die Query-Klasse implementiert das Interface IF_RAP_QUERY_PROVIDER:

CLASS zcl_product_query DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
ENDCLASS.
CLASS zcl_product_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
" 1. Prüfen ob Daten angefordert werden
IF io_request->is_data_requested( ).
" 2. Paging-Informationen abrufen
DATA(lv_offset) = io_request->get_paging( )->get_offset( ).
DATA(lv_page_size) = io_request->get_paging( )->get_page_size( ).
" 3. Sortierung abrufen
DATA(lt_sort) = io_request->get_sort_elements( ).
" 4. Filter abrufen
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
" 5. Daten aus externer Quelle laden
DATA lt_products TYPE STANDARD TABLE OF zi_externalproduct.
" Hier: API-Aufruf oder andere Datenquelle
lt_products = load_products_from_api(
iv_offset = lv_offset
iv_page_size = lv_page_size
it_filter = lt_filter
it_sort = lt_sort
).
" 6. Daten an Response übergeben
io_response->set_data( lt_products ).
ENDIF.
" 7. Count angefordert?
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lines( lt_products ) ).
ENDIF.
ENDMETHOD.
ENDCLASS.

Paging implementieren

Für große Datenmengen ist Paging essentiell:

METHOD if_rap_query_provider~select.
" Paging-Parameter abrufen
DATA(lo_paging) = io_request->get_paging( ).
DATA(lv_offset) = lo_paging->get_offset( ).
DATA(lv_page_size) = lo_paging->get_page_size( ).
" Maximale Seitengröße begrenzen
IF lv_page_size > 1000 OR lv_page_size <= 0.
lv_page_size = 100. " Default-Wert
ENDIF.
" Daten laden (z.B. aus API mit Pagination)
DATA lt_all_products TYPE STANDARD TABLE OF zi_externalproduct.
lt_all_products = call_external_api( ).
" Paging anwenden
DATA lt_paged_products TYPE STANDARD TABLE OF zi_externalproduct.
DATA(lv_from) = lv_offset + 1.
DATA(lv_to) = lv_offset + lv_page_size.
LOOP AT lt_all_products INTO DATA(ls_product)
FROM lv_from TO lv_to.
APPEND ls_product TO lt_paged_products.
ENDLOOP.
" Response setzen
io_response->set_data( lt_paged_products ).
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lines( lt_all_products ) ).
ENDIF.
ENDMETHOD.

Sorting implementieren

Sortierung ermöglicht dem Benutzer flexible Darstellung:

METHOD if_rap_query_provider~select.
" Daten laden
DATA lt_products TYPE STANDARD TABLE OF zi_externalproduct.
lt_products = load_products( ).
" Sortierung abrufen und anwenden
DATA(lt_sort) = io_request->get_sort_elements( ).
LOOP AT lt_sort INTO DATA(ls_sort).
CASE ls_sort-element_name.
WHEN 'PRODUCTNAME'.
IF ls_sort-descending = abap_true.
SORT lt_products BY ProductName DESCENDING.
ELSE.
SORT lt_products BY ProductName ASCENDING.
ENDIF.
WHEN 'PRICE'.
IF ls_sort-descending = abap_true.
SORT lt_products BY Price DESCENDING.
ELSE.
SORT lt_products BY Price ASCENDING.
ENDIF.
WHEN 'CATEGORY'.
IF ls_sort-descending = abap_true.
SORT lt_products BY Category DESCENDING.
ELSE.
SORT lt_products BY Category ASCENDING.
ENDIF.
ENDCASE.
ENDLOOP.
io_response->set_data( lt_products ).
ENDMETHOD.

Dynamische Sortierung mit SORT-Statement

Für eine elegantere Lösung können Sie dynamisches Sortieren verwenden:

METHOD apply_sorting.
DATA(lt_sort) = io_request->get_sort_elements( ).
" Sortier-String aufbauen
DATA(lv_sort_string) = ``.
LOOP AT lt_sort INTO DATA(ls_sort).
IF lv_sort_string IS NOT INITIAL.
lv_sort_string = lv_sort_string && ` `.
ENDIF.
lv_sort_string = lv_sort_string && ls_sort-element_name.
IF ls_sort-descending = abap_true.
lv_sort_string = lv_sort_string && ` DESCENDING`.
ELSE.
lv_sort_string = lv_sort_string && ` ASCENDING`.
ENDIF.
ENDLOOP.
" Dynamisches Sortieren
IF lv_sort_string IS NOT INITIAL.
SORT ct_data BY (lv_sort_string).
ENDIF.
ENDMETHOD.

Filter implementieren

Filter ermöglichen gezielte Datenabfragen:

METHOD if_rap_query_provider~select.
" Daten laden
DATA lt_products TYPE STANDARD TABLE OF zi_externalproduct.
lt_products = load_all_products( ).
" Filter abrufen
TRY.
DATA(lt_filter_cond) = io_request->get_filter( )->get_as_ranges( ).
CATCH cx_rap_query_filter_no_range.
" Keine Filterkonvertierung möglich
ENDTRY.
" Filter anwenden
LOOP AT lt_filter_cond INTO DATA(ls_filter).
CASE ls_filter-name.
WHEN 'CATEGORY'.
DELETE lt_products WHERE Category NOT IN ls_filter-range.
WHEN 'PRICE'.
DELETE lt_products WHERE Price NOT IN ls_filter-range.
WHEN 'PRODUCTID'.
DELETE lt_products WHERE ProductId NOT IN ls_filter-range.
ENDCASE.
ENDLOOP.
io_response->set_data( lt_products ).
ENDMETHOD.

Beispiel: Anbindung externer REST-API

Ein vollständiges Beispiel für die Anbindung einer externen Produkt-API:

1. Custom Entity Definition

@EndUserText.label: 'Externe Produkte API'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_EXT_PRODUCT_QUERY'
define custom entity ZI_ExtProductAPI
{
key ProductUUID : sysuuid_x16;
ProductId : abap.char(10);
ProductName : abap.char(100);
Description : abap.char(255);
Category : abap.char(40);
Price : abap.dec(13,2);
Currency : abap.cuky;
InStock : abap_boolean;
ImageUrl : abap.char(255);
}

2. Query Implementation mit HTTP-Aufruf

CLASS zcl_ext_product_query DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
PRIVATE SECTION.
TYPES: BEGIN OF ty_api_product,
id TYPE string,
name TYPE string,
description TYPE string,
category TYPE string,
price TYPE decfloat34,
currency TYPE string,
in_stock TYPE abap_bool,
image_url TYPE string,
END OF ty_api_product,
tt_api_products TYPE STANDARD TABLE OF ty_api_product WITH EMPTY KEY.
METHODS call_product_api
RETURNING VALUE(rt_products) TYPE tt_api_products
RAISING cx_http_dest_provider_error
cx_web_http_client_error.
METHODS map_to_entity
IMPORTING it_api_products TYPE tt_api_products
RETURNING VALUE(rt_result) TYPE STANDARD TABLE OF zi_extproductapi.
ENDCLASS.
CLASS zcl_ext_product_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
IF io_request->is_data_requested( ).
TRY.
" API aufrufen
DATA(lt_api_products) = call_product_api( ).
" In Entity-Format konvertieren
DATA(lt_products) = map_to_entity( lt_api_products ).
" Filter anwenden
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
LOOP AT lt_filter INTO DATA(ls_filter).
CASE ls_filter-name.
WHEN 'CATEGORY'.
DELETE lt_products WHERE Category NOT IN ls_filter-range.
ENDCASE.
ENDLOOP.
" Paging anwenden
DATA(lv_offset) = io_request->get_paging( )->get_offset( ).
DATA(lv_page_size) = io_request->get_paging( )->get_page_size( ).
IF lv_page_size > 0.
DATA(lv_from) = lv_offset + 1.
DATA(lv_to) = lv_offset + lv_page_size.
DATA lt_paged TYPE STANDARD TABLE OF zi_extproductapi.
LOOP AT lt_products INTO DATA(ls_prod) FROM lv_from TO lv_to.
APPEND ls_prod TO lt_paged.
ENDLOOP.
io_response->set_data( lt_paged ).
ELSE.
io_response->set_data( lt_products ).
ENDIF.
" Total Count
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lines( lt_products ) ).
ENDIF.
CATCH cx_http_dest_provider_error
cx_web_http_client_error INTO DATA(lx_error).
" Fehlerbehandlung
RAISE EXCEPTION TYPE cx_rap_query_provider
EXPORTING
textid = cx_rap_query_provider=>query_failed
previous = lx_error.
ENDTRY.
ENDIF.
ENDMETHOD.
METHOD call_product_api.
" HTTP-Destination aus dem Destination Service
DATA(lo_destination) = cl_http_destination_provider=>create_by_cloud_destination(
i_name = 'PRODUCT_API'
i_authn_mode = if_a4c_cp_service=>service_specific
).
" HTTP-Client erstellen
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination( lo_destination ).
" Request ausführen
DATA(lo_request) = lo_client->get_http_request( ).
lo_request->set_uri_path( '/api/v1/products' ).
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
DATA(lv_json) = lo_response->get_text( ).
" JSON parsen
/ui2/cl_json=>deserialize(
EXPORTING
json = lv_json
CHANGING
data = rt_products
).
lo_client->close( ).
ENDMETHOD.
METHOD map_to_entity.
LOOP AT it_api_products INTO DATA(ls_api).
APPEND VALUE #(
ProductUUID = cl_system_uuid=>create_uuid_x16_static( )
ProductId = ls_api-id
ProductName = ls_api-name
Description = ls_api-description
Category = ls_api-category
Price = ls_api-price
Currency = ls_api-currency
InStock = ls_api-in_stock
ImageUrl = ls_api-image_url
) TO rt_result.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Custom Entity in RAP Business Object einbinden

Custom Entities können auch in RAP BO eingebunden werden:

Behavior Definition für Custom Entity

unmanaged implementation in class zbp_i_extproduct unique;
define behavior for ZI_ExtProductAPI alias Product
{
// Nur lesend - keine CUD-Operationen
// Actions können trotzdem definiert werden
action refreshFromAPI;
}

Service Definition und Binding

@EndUserText.label: 'Externe Produkte Service'
define service ZUI_ExtProducts {
expose ZI_ExtProductAPI as Products;
}

Fehlerbehandlung

Robuste Fehlerbehandlung ist bei externen Quellen wichtig:

METHOD if_rap_query_provider~select.
TRY.
" Daten laden mit Timeout
DATA(lt_products) = load_external_data( ).
io_response->set_data( lt_products ).
CATCH cx_http_dest_provider_error INTO DATA(lx_dest).
" Destination nicht gefunden oder Konfigurationsfehler
RAISE EXCEPTION TYPE cx_rap_query_provider
EXPORTING
textid = cx_rap_query_provider=>query_failed
previous = lx_dest.
CATCH cx_web_http_client_error INTO DATA(lx_http).
" HTTP-Fehler (Timeout, Netzwerk, etc.)
RAISE EXCEPTION TYPE cx_rap_query_provider
EXPORTING
textid = cx_rap_query_provider=>query_failed
previous = lx_http.
CATCH cx_sy_conversion_error INTO DATA(lx_conv).
" JSON-Parsing-Fehler
RAISE EXCEPTION TYPE cx_rap_query_provider
EXPORTING
textid = cx_rap_query_provider=>query_failed
previous = lx_conv.
ENDTRY.
ENDMETHOD.

Performance-Tipps

  1. Caching - Häufig abgerufene Daten zwischenspeichern
  2. Lazy Loading - Nur benötigte Daten laden
  3. Pagination an API delegieren - Wenn die externe API Paging unterstützt
  4. Asynchrone Aufrufe - Für lange laufende Abfragen
  5. Connection Pooling - HTTP-Verbindungen wiederverwenden
" Beispiel: Einfaches Caching
CLASS zcl_product_query DEFINITION.
PRIVATE SECTION.
CLASS-DATA: gt_cache TYPE STANDARD TABLE OF zi_extproductapi,
gv_cache_timestamp TYPE timestamp.
METHODS get_cached_data
RETURNING VALUE(rt_data) TYPE STANDARD TABLE OF zi_extproductapi.
ENDCLASS.
CLASS zcl_product_query IMPLEMENTATION.
METHOD get_cached_data.
" Cache-Gültigkeit: 5 Minuten
DATA(lv_now) = utclong_current( ).
DATA(lv_cache_age) = cl_abap_tstmp=>subtract(
tstmp1 = lv_now
tstmp2 = gv_cache_timestamp
).
IF gv_cache_timestamp IS INITIAL OR lv_cache_age > 300.
" Cache neu laden
gt_cache = call_external_api( ).
gv_cache_timestamp = lv_now.
ENDIF.
rt_data = gt_cache.
ENDMETHOD.
ENDCLASS.

Best Practices

  1. Interface verwenden - IF_RAP_QUERY_PROVIDER vollständig implementieren
  2. Fehlerbehandlung - Externe Quellen können fehlschlagen
  3. Paging unterstützen - Große Datenmengen erfordern Pagination
  4. Filter nutzen - Daten möglichst früh filtern (bei API wenn möglich)
  5. Logging - Externe Aufrufe protokollieren für Debugging
  6. Timeouts setzen - Lange Wartezeiten vermeiden
  7. Testen - Externe Abhängigkeiten mocken für Unit Tests

Weiterführende Themen