RAP Abstract Entities: Flexible Custom Queries ohne Datenbank

kategorie
RAP
Veröffentlicht
autor
Johannes

Abstract Entities sind ein spezieller CDS-Typ in RAP, der keine Datenbanktabelle repraesentiert. Sie dienen als Datenstruktur fuer Action-Parameter, Funktionsergebnisse und Custom Queries mit maximaler Flexibilitaet.

Was sind Abstract Entities?

Abstract Entities werden mit define abstract entity definiert und existieren nur als Metadaten-Definition. Sie haben keine persistente Datenquelle und werden durch ABAP-Code mit Daten befuellt.

@EndUserText.label: 'Externe Wetterdaten'
define abstract entity ZA_WeatherData
{
City : abap.char(40);
Temperature : abap.dec(5,2);
Humidity : abap.int2;
Conditions : abap.char(50);
Timestamp : abap.utclong;
}

Unterschied zu anderen Entity-Typen

AspektCDS ViewCustom EntityAbstract Entity
Definitiondefine view entitydefine custom entitydefine abstract entity
DatenquelleDatenbanktabellenQuery Provider KlasseKeine (nur Struktur)
OData-exponierbarJaJaNur ueber Actions/Functions
EinsatzzweckDatenmodellierungExterne DatenquellenParameter, Results
PersistenzDatenbankKeineKeine

Einsatzszenarien

1. Action-Parameter

Abstract Entities definieren komplexe Eingabestrukturen fuer RAP Actions:

@EndUserText.label: 'Buchungsparameter'
define abstract entity ZA_BookingParameters
{
BookingDate : abap.dats;
CustomerID : abap.numc(10);
ProductID : abap.char(18);
Quantity : abap.int4;
DiscountCode : abap.char(10);
PaymentMethod : abap.char(20);
}

Verwendung in der Behavior Definition:

action createBooking parameter ZA_BookingParameters result [1] $self;

2. Function Results

Fuer komplexe Rueckgabewerte von RAP Functions:

@EndUserText.label: 'Preisberechnung Ergebnis'
define abstract entity ZA_PriceCalculationResult
{
NetPrice : abap.dec(15,2);
TaxAmount : abap.dec(15,2);
GrossPrice : abap.dec(15,2);
Currency : abap.cuky;
DiscountApplied : abap_boolean;
DiscountPercent : abap.dec(5,2);
ValidUntil : abap.dats;
}

3. Custom Queries ohne Datenbanktabelle

Fuer Daten aus externen Systemen oder berechnete Werte:

@EndUserText.label: 'Waehrungskurse'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_EXCHANGE_RATE_QUERY'
define custom entity ZI_ExchangeRate
{
key SourceCurrency : abap.cuky;
key TargetCurrency : abap.cuky;
ExchangeRate : abap.dec(15,5);
RateDate : abap.dats;
Provider : abap.char(20);
}

Custom Query mit if_rap_query_provider

Die Query-Implementation-Klasse fuellt die Entity mit Daten:

CLASS zcl_exchange_rate_query DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
PRIVATE SECTION.
METHODS fetch_rates_from_api
RETURNING VALUE(rt_rates) TYPE zt_exchange_rates.
ENDCLASS.
CLASS zcl_exchange_rate_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
" 1. Pruefen ob Daten angefordert werden
IF io_request->is_data_requested( ) = abap_false.
RETURN.
ENDIF.
" 2. Filter abrufen
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
" 3. Paging abrufen
DATA(lv_offset) = io_request->get_paging( )->get_offset( ).
DATA(lv_page_size) = io_request->get_paging( )->get_page_size( ).
" 4. Daten von externer API abrufen
DATA(lt_rates) = fetch_rates_from_api( ).
" 5. Filter anwenden
DATA lt_result TYPE STANDARD TABLE OF zi_exchangerate.
LOOP AT lt_rates INTO DATA(ls_rate).
" Filterkriterien pruefen
IF lt_filter IS NOT INITIAL.
DATA(lv_match) = abap_true.
LOOP AT lt_filter INTO DATA(ls_filter).
CASE ls_filter-name.
WHEN 'SOURCECURRENCY'.
IF NOT ls_rate-source_currency IN ls_filter-range.
lv_match = abap_false.
ENDIF.
WHEN 'TARGETCURRENCY'.
IF NOT ls_rate-target_currency IN ls_filter-range.
lv_match = abap_false.
ENDIF.
ENDCASE.
ENDLOOP.
IF lv_match = abap_false.
CONTINUE.
ENDIF.
ENDIF.
APPEND VALUE #(
SourceCurrency = ls_rate-source_currency
TargetCurrency = ls_rate-target_currency
ExchangeRate = ls_rate-rate
RateDate = ls_rate-rate_date
Provider = ls_rate-provider
) TO lt_result.
ENDLOOP.
" 6. Gesamtanzahl fuer Paging
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lines( lt_result ) ).
ENDIF.
" 7. Paging anwenden
IF lv_page_size > 0.
DATA(lv_to) = lv_offset + lv_page_size.
IF lv_to > lines( lt_result ).
lv_to = lines( lt_result ).
ENDIF.
lt_result = VALUE #( FOR i = lv_offset + 1 WHILE i <= lv_to
( lt_result[ i ] ) ).
ENDIF.
" 8. Daten zurueckgeben
io_response->set_data( lt_result ).
ENDMETHOD.
METHOD fetch_rates_from_api.
" HTTP-Client erstellen
TRY.
DATA(lo_destination) = cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = 'Z_EXCHANGE_RATE_API'
service_id = 'Z_EXCHANGE_RATE_OUTBOUND'
).
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination( lo_destination ).
DATA(lo_request) = lo_client->get_http_request( ).
lo_request->set_uri_path( '/api/v1/rates' ).
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_rates
).
CATCH cx_root INTO DATA(lx_error).
" Fehler loggen, leere Tabelle zurueckgeben
" Oder: RAISE SHORTDUMP
ENDTRY.
ENDMETHOD.
ENDCLASS.

Integration mit externen APIs

REST API Anbindung

METHOD fetch_weather_data.
TRY.
DATA(lo_dest) = cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = 'Z_WEATHER_API'
).
DATA(lo_client) = cl_web_http_client_manager=>create_by_http_destination( lo_dest ).
DATA(lo_request) = lo_client->get_http_request( ).
" Query-Parameter setzen
lo_request->set_uri_path( |/weather?city={ iv_city }&units=metric| ).
lo_request->set_header_field(
i_name = 'Accept'
i_value = 'application/json'
).
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
IF lo_response->get_status( )-code = 200.
DATA(lv_json) = lo_response->get_text( ).
parse_weather_json( EXPORTING iv_json = lv_json
IMPORTING es_weather = rs_weather ).
ENDIF.
CATCH cx_http_dest_provider_error
cx_web_http_client_error INTO DATA(lx_error).
" Fehlerbehandlung
ENDTRY.
ENDMETHOD.

RFC-Anbindung

METHOD fetch_from_legacy_system.
DATA: lv_message TYPE c LENGTH 200.
TRY.
DATA(lo_dest) = cl_rfc_destination_provider=>create_by_cloud_destination(
i_name = 'SAP_ECC_SYSTEM'
).
CALL FUNCTION 'Z_GET_PRODUCT_STOCK'
DESTINATION lo_dest->get_destination_name( )
EXPORTING
iv_material = iv_material
IMPORTING
ev_stock = rv_stock
EXCEPTIONS
communication_failure = 1 MESSAGE lv_message
system_failure = 2 MESSAGE lv_message
OTHERS = 3.
IF sy-subrc <> 0.
" Fehler loggen
ENDIF.
CATCH cx_rfc_dest_provider_error INTO DATA(lx_error).
" Destination nicht gefunden
ENDTRY.
ENDMETHOD.

Komplexes Beispiel: Dashboard-Daten

Ein Dashboard, das Daten aus mehreren Quellen aggregiert:

Abstract Entity Definition

@EndUserText.label: 'Dashboard KPIs'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_DASHBOARD_QUERY'
define custom entity ZI_DashboardKPI
{
key KPIId : abap.char(20);
KPIName : abap.char(60);
KPIValue : abap.dec(15,2);
KPIUnit : abap.char(10);
Trend : abap.int1; // -1, 0, 1
TrendPercent : abap.dec(5,2);
ComparisonValue : abap.dec(15,2);
Status : abap.char(10); // Good, Warning, Critical
Category : abap.char(30);
LastUpdated : abap.utclong;
}

Query Implementation

CLASS zcl_dashboard_query DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
PRIVATE SECTION.
METHODS get_sales_kpis RETURNING VALUE(rt_kpis) TYPE zt_dashboard_kpis.
METHODS get_inventory_kpis RETURNING VALUE(rt_kpis) TYPE zt_dashboard_kpis.
METHODS get_customer_kpis RETURNING VALUE(rt_kpis) TYPE zt_dashboard_kpis.
METHODS calculate_trend
IMPORTING iv_current TYPE decfloat34
iv_previous TYPE decfloat34
EXPORTING ev_trend TYPE i
ev_percent TYPE decfloat34.
ENDCLASS.
CLASS zcl_dashboard_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
IF io_request->is_data_requested( ) = abap_false.
RETURN.
ENDIF.
" Daten aus verschiedenen Quellen aggregieren
DATA lt_kpis TYPE STANDARD TABLE OF zi_dashboardkpi.
" Sales KPIs aus CDS View
APPEND LINES OF get_sales_kpis( ) TO lt_kpis.
" Inventory KPIs aus Legacy-System via RFC
APPEND LINES OF get_inventory_kpis( ) TO lt_kpis.
" Customer KPIs aus externer API
APPEND LINES OF get_customer_kpis( ) TO lt_kpis.
" Filter anwenden
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
IF lt_filter IS NOT INITIAL.
LOOP AT lt_kpis ASSIGNING FIELD-SYMBOL(<ls_kpi>).
LOOP AT lt_filter INTO DATA(ls_filter).
CASE ls_filter-name.
WHEN 'CATEGORY'.
IF NOT <ls_kpi>-Category IN ls_filter-range.
DELETE lt_kpis.
ENDIF.
ENDCASE.
ENDLOOP.
ENDLOOP.
ENDIF.
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lines( lt_kpis ) ).
ENDIF.
io_response->set_data( lt_kpis ).
ENDMETHOD.
METHOD get_sales_kpis.
" Aktuelle und vorherige Periode aus CDS View
SELECT SUM( net_amount ) AS current_sales
FROM zi_salesorder
WHERE created_at >= @( cl_abap_context_info=>get_system_date( ) - 30 )
INTO @DATA(lv_current_sales).
SELECT SUM( net_amount ) AS previous_sales
FROM zi_salesorder
WHERE created_at >= @( cl_abap_context_info=>get_system_date( ) - 60 )
AND created_at < @( cl_abap_context_info=>get_system_date( ) - 30 )
INTO @DATA(lv_previous_sales).
DATA: lv_trend TYPE i,
lv_percent TYPE decfloat34.
calculate_trend(
EXPORTING iv_current = lv_current_sales
iv_previous = lv_previous_sales
IMPORTING ev_trend = lv_trend
ev_percent = lv_percent ).
APPEND VALUE #(
KPIId = 'SALES_30D'
KPIName = 'Umsatz (30 Tage)'
KPIValue = lv_current_sales
KPIUnit = 'EUR'
Trend = lv_trend
TrendPercent = lv_percent
ComparisonValue = lv_previous_sales
Status = COND #( WHEN lv_trend >= 0 THEN 'Good'
WHEN lv_percent > -10 THEN 'Warning'
ELSE 'Critical' )
Category = 'Sales'
LastUpdated = utclong_current( )
) TO rt_kpis.
ENDMETHOD.
METHOD calculate_trend.
IF iv_previous = 0.
ev_trend = 0.
ev_percent = 0.
ELSE.
ev_percent = ( iv_current - iv_previous ) / iv_previous * 100.
ev_trend = COND #( WHEN ev_percent > 0 THEN 1
WHEN ev_percent < 0 THEN -1
ELSE 0 ).
ENDIF.
ENDMETHOD.
" Weitere Methoden...
ENDCLASS.

Best Practices fuer Performance

1. Caching implementieren

CLASS zcl_cached_query DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
PRIVATE SECTION.
CLASS-DATA: gt_cache TYPE zt_cached_data,
gv_cache_timestamp TYPE utclong.
CLASS-METHODS is_cache_valid RETURNING VALUE(rv_valid) TYPE abap_bool.
METHODS refresh_cache.
CONSTANTS: gc_cache_duration_seconds TYPE i VALUE 300. " 5 Minuten
ENDCLASS.
CLASS zcl_cached_query IMPLEMENTATION.
METHOD is_cache_valid.
IF gv_cache_timestamp IS INITIAL.
rv_valid = abap_false.
RETURN.
ENDIF.
DATA(lv_age) = utclong_diff(
high = utclong_current( )
low = gv_cache_timestamp
).
rv_valid = xsdbool( lv_age < gc_cache_duration_seconds ).
ENDMETHOD.
METHOD if_rap_query_provider~select.
IF io_request->is_data_requested( ) = abap_false.
RETURN.
ENDIF.
" Cache pruefen
IF is_cache_valid( ) = abap_false.
refresh_cache( ).
ENDIF.
" Daten aus Cache zurueckgeben
io_response->set_data( gt_cache ).
ENDMETHOD.
METHOD refresh_cache.
" Externe Daten abrufen
gt_cache = fetch_external_data( ).
gv_cache_timestamp = utclong_current( ).
ENDMETHOD.
ENDCLASS.

2. Parallele Verarbeitung

METHOD fetch_data_parallel.
" Mehrere Datenquellen parallel abrufen
DATA: lt_tasks TYPE TABLE OF REF TO zcl_data_fetch_task.
" Tasks erstellen
APPEND NEW zcl_data_fetch_task( 'SOURCE_A' ) TO lt_tasks.
APPEND NEW zcl_data_fetch_task( 'SOURCE_B' ) TO lt_tasks.
APPEND NEW zcl_data_fetch_task( 'SOURCE_C' ) TO lt_tasks.
" Parallel ausfuehren
DATA(lo_parallel) = NEW cl_abap_parallel( ).
lo_parallel->run_instances(
EXPORTING
p_in_tab = lt_tasks
IMPORTING
p_out_tab = DATA(lt_results)
).
" Ergebnisse aggregieren
LOOP AT lt_results INTO DATA(lo_result).
APPEND LINES OF lo_result->get_data( ) TO rt_combined_data.
ENDLOOP.
ENDMETHOD.

3. Fehlerbehandlung

METHOD if_rap_query_provider~select.
TRY.
" Daten abrufen
DATA(lt_data) = fetch_external_data( ).
io_response->set_data( lt_data ).
CATCH cx_http_dest_provider_error
cx_web_http_client_error INTO DATA(lx_http_error).
" HTTP-Fehler: Leere Antwort oder Fallback-Daten
io_response->set_data( VALUE #( ) ).
" Optional: Business Exception ausloesen
RAISE EXCEPTION TYPE zcx_external_service_error
EXPORTING
textid = zcx_external_service_error=>service_unavailable
service_name = 'Weather API'.
CATCH cx_root INTO DATA(lx_error).
" Generischer Fehler loggen
cl_bali_log=>create_with_header(
EXPORTING
header = cl_bali_header_setter=>create(
object = 'ZDASHBOARD'
subobject = 'QUERY'
)
)->add_item(
item = cl_bali_exception_setter=>create(
exception = lx_error
)
)->save( ).
io_response->set_data( VALUE #( ) ).
ENDTRY.
ENDMETHOD.

Abstract Entities vs. Custom Entities

AnwendungEmpfehlung
Action-ParameterAbstract Entity
Function ResultsAbstract Entity
OData-Service mit FilterungCustom Entity
Read-Only DatenCustom Entity
Temporaere StrukturenAbstract Entity
Dashboard-DatenCustom Entity mit Cache

Zusammenfassung

Abstract Entities und Custom Entities mit if_rap_query_provider bieten maximale Flexibilitaet fuer:

  • Externe Datenquellen - REST APIs, RFC, Legacy-Systeme
  • Berechnete Daten - Aggregationen, KPIs, Trends
  • Action/Function Parameter - Komplexe Eingabe-/Ausgabestrukturen
  • Hybride Loesungen - Kombination aus Datenbank und externen Quellen

Wichtige Punkte:

  • Immer Caching fuer externe Datenquellen implementieren
  • Paging und Filterung im Query Provider beruecksichtigen
  • Robuste Fehlerbehandlung mit Fallback-Strategien
  • Performance durch parallele Verarbeitung optimieren

Verwandte Themen