Custom Entity Value Help Deep Dive: Dynamische Wertehilfen in RAP

Kategorie
RAP
Veröffentlicht
Autor
Johannes

Standard CDS-basierte Value Helps sind ideal, wenn die Daten in Datenbanktabellen vorliegen. Doch was, wenn die Wertehilfe aus externen APIs, komplexen Berechnungen oder dynamischen Quellen stammt? Hier kommen Custom Entity Value Helps ins Spiel.

Wann Custom Entity Value Helps?

Custom Entity Value Helps sind die richtige Wahl für:

SzenarioBeispiel
Externe DatenquellenFlughäfen aus externem Flugplan-API
Komplexe LogikVerfügbare Flüge basierend auf Datum und Route
Dynamische FilterKontextabhängige Wertehilfen
Aggregierte DatenStatistiken als Auswahlliste
Berechtigte DatenWerte abhängig vom aktuellen Benutzer

Architektur einer Custom Entity Value Help

┌─────────────────────────────────────────────────────────┐
│ Consumption View │
│ @Consumption.valueHelpDefinition: 'ZI_AirportVH' │
└──────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Custom Entity: ZI_AirportVH │
│ @ObjectModel.query.implementedBy: 'ABAP:ZCL_...' │
└──────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Query Implementation Class │
│ IF_RAP_QUERY_PROVIDER~SELECT │
│ - Filter auswerten │
│ - Suche implementieren │
│ - Daten zurückgeben │
└─────────────────────────────────────────────────────────┘

Schritt 1: Custom Entity Definition

Die Custom Entity definiert die Struktur der Value Help:

@EndUserText.label: 'Flughafen Value Help'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_AIRPORT_VH_QUERY'
@Search.searchable: true
@ObjectModel.resultSet.sizeCategory: #M
define custom entity ZI_AirportVH
{
@EndUserText.label: 'Flughafen-Code'
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 1.0
@Search.ranking: #HIGH
key AirportCode : abap.char(3);
@EndUserText.label: 'Flughafenname'
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
@Search.ranking: #MEDIUM
AirportName : abap.char(100);
@EndUserText.label: 'Stadt'
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.85
City : abap.char(40);
@EndUserText.label: 'Land'
Country : abap.char(3);
@EndUserText.label: 'Zeitzone'
Timezone : abap.char(20);
@EndUserText.label: 'Aktiv'
IsActive : abap_boolean;
}

Wichtige Annotationen für Value Helps

AnnotationZweck
@Search.searchableAktiviert die generische Suche
@Search.defaultSearchElementFeld wird bei Suche berücksichtigt
@Search.fuzzinessThresholdToleranz für Tippfehler (1.0 = exakt)
@ObjectModel.resultSet.sizeCategoryPerformance-Hinweis für Fiori

Schritt 2: Query Implementation Class

Die Query-Klasse implementiert IF_RAP_QUERY_PROVIDER und liefert die Daten:

CLASS zcl_airport_vh_query DEFINITION
PUBLIC
FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
PRIVATE SECTION.
TYPES: BEGIN OF ty_airport,
airport_code TYPE c LENGTH 3,
airport_name TYPE c LENGTH 100,
city TYPE c LENGTH 40,
country TYPE c LENGTH 3,
timezone TYPE c LENGTH 20,
is_active TYPE abap_boolean,
END OF ty_airport,
tt_airports TYPE STANDARD TABLE OF ty_airport WITH EMPTY KEY.
METHODS get_airports
RETURNING VALUE(rt_result) TYPE tt_airports.
METHODS apply_filter
IMPORTING it_filter TYPE if_rap_query_filter=>tt_name_range_pairs
CHANGING ct_data TYPE tt_airports.
METHODS apply_search
IMPORTING iv_search TYPE string
CHANGING ct_data TYPE tt_airports.
METHODS apply_sorting
IMPORTING it_sort TYPE if_rap_query_request=>tt_sort_elements
CHANGING ct_data TYPE tt_airports.
METHODS apply_paging
IMPORTING iv_offset TYPE i
iv_limit TYPE i
CHANGING ct_data TYPE tt_airports.
ENDCLASS.
CLASS zcl_airport_vh_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
" 1. Daten laden
DATA(lt_airports) = get_airports( ).
" 2. Filter anwenden
IF io_request->is_data_requested( ).
TRY.
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
apply_filter(
EXPORTING it_filter = lt_filter
CHANGING ct_data = lt_airports
).
CATCH cx_rap_query_filter_no_range.
" Kein Range-Filter vorhanden
ENDTRY.
" 3. Suche anwenden (falls vorhanden)
TRY.
DATA(lv_search) = io_request->get_search_expression( ).
IF lv_search IS NOT INITIAL.
apply_search(
EXPORTING iv_search = lv_search
CHANGING ct_data = lt_airports
).
ENDIF.
CATCH cx_rap_query_provider.
" Keine Suche
ENDTRY.
" 4. Sortierung anwenden
DATA(lt_sort) = io_request->get_sort_elements( ).
IF lt_sort IS NOT INITIAL.
apply_sorting(
EXPORTING it_sort = lt_sort
CHANGING ct_data = lt_airports
).
ENDIF.
" 5. Gesamtanzahl vor Paging
DATA(lv_total_count) = lines( lt_airports ).
" 6. Paging anwenden
DATA(lv_offset) = io_request->get_paging( )->get_offset( ).
DATA(lv_limit) = io_request->get_paging( )->get_page_size( ).
IF lv_limit > 0.
apply_paging(
EXPORTING iv_offset = lv_offset
iv_limit = lv_limit
CHANGING ct_data = lt_airports
).
ENDIF.
" 7. Ergebnis in Entity-Format konvertieren
DATA lt_result TYPE STANDARD TABLE OF zi_airportvh.
LOOP AT lt_airports INTO DATA(ls_airport).
APPEND VALUE #(
AirportCode = ls_airport-airport_code
AirportName = ls_airport-airport_name
City = ls_airport-city
Country = ls_airport-country
Timezone = ls_airport-timezone
IsActive = ls_airport-is_active
) TO lt_result.
ENDLOOP.
io_response->set_data( lt_result ).
ENDIF.
" 8. Gesamtanzahl zurückgeben
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lv_total_count ).
ENDIF.
ENDMETHOD.
METHOD get_airports.
" In der Praxis: API-Aufruf, BAPI, RFC, etc.
" Hier: Beispieldaten
rt_result = VALUE #(
( airport_code = 'FRA' airport_name = 'Frankfurt Airport'
city = 'Frankfurt' country = 'DE' timezone = 'Europe/Berlin'
is_active = abap_true )
( airport_code = 'MUC' airport_name = 'Munich Airport'
city = 'München' country = 'DE' timezone = 'Europe/Berlin'
is_active = abap_true )
( airport_code = 'BER' airport_name = 'Berlin Brandenburg'
city = 'Berlin' country = 'DE' timezone = 'Europe/Berlin'
is_active = abap_true )
( airport_code = 'JFK' airport_name = 'John F. Kennedy'
city = 'New York' country = 'US' timezone = 'America/New_York'
is_active = abap_true )
( airport_code = 'LHR' airport_name = 'London Heathrow'
city = 'London' country = 'GB' timezone = 'Europe/London'
is_active = abap_true )
( airport_code = 'CDG' airport_name = 'Charles de Gaulle'
city = 'Paris' country = 'FR' timezone = 'Europe/Paris'
is_active = abap_true )
( airport_code = 'ZRH' airport_name = 'Zürich Airport'
city = 'Zürich' country = 'CH' timezone = 'Europe/Zurich'
is_active = abap_true )
( airport_code = 'VIE' airport_name = 'Vienna International'
city = 'Wien' country = 'AT' timezone = 'Europe/Vienna'
is_active = abap_true )
).
ENDMETHOD.
METHOD apply_filter.
LOOP AT it_filter INTO DATA(ls_filter).
CASE to_upper( ls_filter-name ).
WHEN 'AIRPORTCODE'.
DELETE ct_data WHERE airport_code NOT IN ls_filter-range.
WHEN 'COUNTRY'.
DELETE ct_data WHERE country NOT IN ls_filter-range.
WHEN 'ISACTIVE'.
DELETE ct_data WHERE is_active NOT IN ls_filter-range.
WHEN 'CITY'.
DELETE ct_data WHERE city NOT IN ls_filter-range.
ENDCASE.
ENDLOOP.
ENDMETHOD.
METHOD apply_search.
" Fuzzy-Suche über mehrere Felder
DATA(lv_pattern) = |*{ to_upper( iv_search ) }*|.
DELETE ct_data WHERE NOT (
to_upper( airport_code ) CP lv_pattern OR
to_upper( airport_name ) CP lv_pattern OR
to_upper( city ) CP lv_pattern
).
ENDMETHOD.
METHOD apply_sorting.
LOOP AT it_sort INTO DATA(ls_sort).
CASE to_upper( ls_sort-element_name ).
WHEN 'AIRPORTCODE'.
IF ls_sort-descending = abap_true.
SORT ct_data BY airport_code DESCENDING.
ELSE.
SORT ct_data BY airport_code ASCENDING.
ENDIF.
WHEN 'AIRPORTNAME'.
IF ls_sort-descending = abap_true.
SORT ct_data BY airport_name DESCENDING.
ELSE.
SORT ct_data BY airport_name ASCENDING.
ENDIF.
WHEN 'CITY'.
IF ls_sort-descending = abap_true.
SORT ct_data BY city DESCENDING.
ELSE.
SORT ct_data BY city ASCENDING.
ENDIF.
WHEN 'COUNTRY'.
IF ls_sort-descending = abap_true.
SORT ct_data BY country DESCENDING.
ELSE.
SORT ct_data BY country ASCENDING.
ENDIF.
ENDCASE.
ENDLOOP.
ENDMETHOD.
METHOD apply_paging.
DATA(lv_from) = iv_offset + 1.
DATA(lv_to) = iv_offset + iv_limit.
DATA(lv_count) = lines( ct_data ).
IF lv_from > lv_count.
CLEAR ct_data.
RETURN.
ENDIF.
IF lv_to > lv_count.
lv_to = lv_count.
ENDIF.
DATA lt_paged LIKE ct_data.
LOOP AT ct_data INTO DATA(ls_data) FROM lv_from TO lv_to.
APPEND ls_data TO lt_paged.
ENDLOOP.
ct_data = lt_paged.
ENDMETHOD.
ENDCLASS.

Schritt 3: Value Help Binding

Die Custom Entity wird wie eine normale CDS View als Value Help verwendet:

define view entity ZC_FlightBooking
as projection on ZI_FlightBooking
{
key BookingUUID,
@Consumption.valueHelpDefinition: [{
entity: {
name: 'ZI_AirportVH',
element: 'AirportCode'
},
additionalBinding: [{
element: 'AirportName',
localElement: 'DepartureAirportName',
usage: #RESULT
}, {
element: 'City',
localElement: 'DepartureCity',
usage: #RESULT
}, {
element: 'Country',
localElement: 'DepartureCountry',
usage: #FILTER
}],
useForValidation: true
}]
DepartureAirport,
DepartureAirportName,
DepartureCity,
DepartureCountry,
@Consumption.valueHelpDefinition: [{
entity: {
name: 'ZI_AirportVH',
element: 'AirportCode'
},
additionalBinding: [{
element: 'AirportName',
localElement: 'ArrivalAirportName',
usage: #RESULT
}]
}]
ArrivalAirport,
ArrivalAirportName,
FlightDate,
Price,
Currency
}

Filterung in Custom Entity Value Helps

Statische Filter

Filter werden automatisch an die Query-Klasse übergeben:

@Consumption.valueHelpDefinition: [{
entity: {
name: 'ZI_AirportVH',
element: 'AirportCode'
},
additionalBinding: [{
element: 'IsActive',
localConstant: 'X',
usage: #FILTER
}]
}]
DepartureAirport,

Die Query-Klasse erhält den Filter ISACTIVE = 'X' in get_filter( )->get_as_ranges( ).

Dynamische Filter

Abhängig von anderen Feldern:

@Consumption.valueHelpDefinition: [{
entity: {
name: 'ZI_AirportVH',
element: 'AirportCode'
},
additionalBinding: [{
element: 'Country',
localElement: 'DepartureCountry',
usage: #FILTER
}]
}]
ArrivalAirport,

Hier werden nur Flughäfen im selben Land wie der Abflughafen angezeigt.

Suche implementieren

Die generische Suche wird über get_search_expression() abgerufen:

METHOD apply_search.
DATA(lv_search_upper) = to_upper( iv_search ).
" Einfache Wildcard-Suche
DATA(lv_pattern) = |*{ lv_search_upper }*|.
DELETE ct_data WHERE NOT (
to_upper( airport_code ) CP lv_pattern OR
to_upper( airport_name ) CP lv_pattern OR
to_upper( city ) CP lv_pattern
).
ENDMETHOD.

Erweiterte Fuzzy-Suche

Für bessere Suchergebnisse bei Tippfehlern:

METHOD apply_fuzzy_search.
" Levenshtein-Distanz für Fuzzy-Matching
" Threshold: 0.8 = 80% Übereinstimmung
DATA(lv_threshold) = 80.
LOOP AT ct_data ASSIGNING FIELD-SYMBOL(<ls_data>).
DATA(lv_match) = abap_false.
" Prüfe Airport Code (exakt)
IF to_upper( <ls_data>-airport_code ) CS to_upper( iv_search ).
lv_match = abap_true.
ENDIF.
" Prüfe Airport Name (fuzzy)
IF lv_match = abap_false.
DATA(lv_similarity) = calculate_similarity(
iv_string1 = to_upper( <ls_data>-airport_name )
iv_string2 = to_upper( iv_search )
).
IF lv_similarity >= lv_threshold.
lv_match = abap_true.
ENDIF.
ENDIF.
" Prüfe Stadt (fuzzy)
IF lv_match = abap_false.
lv_similarity = calculate_similarity(
iv_string1 = to_upper( <ls_data>-city )
iv_string2 = to_upper( iv_search )
).
IF lv_similarity >= lv_threshold.
lv_match = abap_true.
ENDIF.
ENDIF.
IF lv_match = abap_false.
DELETE ct_data.
ENDIF.
ENDLOOP.
ENDMETHOD.

Performance-Optimierung

1. Caching für statische Daten

CLASS zcl_airport_vh_query DEFINITION.
PRIVATE SECTION.
CLASS-DATA: gt_cache TYPE tt_airports,
gv_cache_time TYPE timestamp.
METHODS get_cached_airports
RETURNING VALUE(rt_result) TYPE tt_airports.
ENDCLASS.
METHOD get_cached_airports.
" Cache-Gültigkeit: 10 Minuten
DATA(lv_now) = utclong_current( ).
IF gt_cache IS INITIAL OR
cl_abap_tstmp=>subtract( tstmp1 = lv_now
tstmp2 = gv_cache_time ) > 600.
gt_cache = load_airports_from_source( ).
gv_cache_time = lv_now.
ENDIF.
rt_result = gt_cache.
ENDMETHOD.

2. Filter an Datenquelle delegieren

Wenn die externe API Filter unterstützt:

METHOD get_airports_filtered.
" Filter für API aufbereiten
DATA lv_country_filter TYPE string.
LOOP AT it_filter INTO DATA(ls_filter)
WHERE name = 'COUNTRY'.
READ TABLE ls_filter-range INDEX 1 INTO DATA(ls_range).
IF sy-subrc = 0.
lv_country_filter = ls_range-low.
ENDIF.
ENDLOOP.
" API mit Filter aufrufen
DATA(lo_client) = get_http_client( ).
DATA(lv_uri) = |/api/airports|.
IF lv_country_filter IS NOT INITIAL.
lv_uri = lv_uri && |?country={ lv_country_filter }|.
ENDIF.
lo_client->get_http_request( )->set_uri_path( lv_uri ).
" ...
ENDMETHOD.

3. Ergebnismenge begrenzen

METHOD if_rap_query_provider~select.
" ...
" Maximal 1000 Einträge zurückgeben
DATA(lv_max_results) = 1000.
IF lines( lt_result ) > lv_max_results.
DELETE lt_result FROM ( lv_max_results + 1 ).
ENDIF.
io_response->set_data( lt_result ).
ENDMETHOD.

Vollständiges Flughafen-Beispiel

Custom Entity mit allen Feldern

@EndUserText.label: 'Flughafen Value Help'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_AIRPORT_VALUE_HELP'
@Search.searchable: true
@ObjectModel.resultSet.sizeCategory: #M
@UI.headerInfo: {
typeName: 'Flughafen',
typeNamePlural: 'Flughäfen'
}
define custom entity ZI_AirportValueHelp
{
@EndUserText.label: 'IATA-Code'
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 1.0
@Search.ranking: #HIGH
@UI.lineItem: [{ position: 10 }]
key AirportId : abap.char(3);
@EndUserText.label: 'Name'
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
@Search.ranking: #MEDIUM
@UI.lineItem: [{ position: 20 }]
AirportName : abap.char(100);
@EndUserText.label: 'Stadt'
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.85
@UI.lineItem: [{ position: 30 }]
City : abap.char(40);
@EndUserText.label: 'Land'
@UI.lineItem: [{ position: 40 }]
CountryCode : abap.char(2);
@EndUserText.label: 'Landesname'
CountryName : abap.char(60);
@EndUserText.label: 'Region'
Region : abap.char(20);
@EndUserText.label: 'Breitengrad'
Latitude : abap.dec(9,6);
@EndUserText.label: 'Längengrad'
Longitude : abap.dec(9,6);
@EndUserText.label: 'Zeitzone'
Timezone : abap.char(40);
@EndUserText.label: 'Status'
@UI.lineItem: [{ position: 50, criticality: 'StatusCriticality' }]
Status : abap.char(10);
StatusCriticality : abap.int1;
}

Verwendung in der Consumption View

define view entity ZC_Booking
as projection on ZI_Booking
{
key BookingId,
@Consumption.valueHelpDefinition: [{
entity: {
name: 'ZI_AirportValueHelp',
element: 'AirportId'
},
additionalBinding: [{
element: 'AirportName',
localElement: 'DepartureAirportName',
usage: #RESULT
}, {
element: 'City',
localElement: 'DepartureCity',
usage: #RESULT
}, {
element: 'CountryCode',
localElement: 'DepartureCountry',
usage: #RESULT
}, {
element: 'Timezone',
localElement: 'DepartureTimezone',
usage: #RESULT
}, {
element: 'Status',
localConstant: 'ACTIVE',
usage: #FILTER
}],
useForValidation: true
}]
@UI.lineItem: [{ position: 20, label: 'Von' }]
DepartureAirport,
DepartureAirportName,
DepartureCity,
DepartureCountry,
DepartureTimezone,
@Consumption.valueHelpDefinition: [{
entity: {
name: 'ZI_AirportValueHelp',
element: 'AirportId'
},
additionalBinding: [{
element: 'AirportName',
localElement: 'ArrivalAirportName',
usage: #RESULT
}, {
element: 'City',
localElement: 'ArrivalCity',
usage: #RESULT
}]
}]
@UI.lineItem: [{ position: 30, label: 'Nach' }]
ArrivalAirport,
ArrivalAirportName,
ArrivalCity,
FlightDate,
PassengerName,
BookingStatus
}

Fehlerbehandlung

Robuste Fehlerbehandlung ist bei Custom Entity Value Helps wichtig:

METHOD if_rap_query_provider~select.
TRY.
DATA(lt_airports) = get_airports_from_api( ).
" Filter, Sort, Page...
io_response->set_data( lt_airports ).
CATCH cx_http_dest_provider_error INTO DATA(lx_dest).
" Destination nicht konfiguriert
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).
" API nicht erreichbar
" Fallback: Leere Liste zurückgeben
io_response->set_data( VALUE tt_result( ) ).
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( 0 ).
ENDIF.
ENDTRY.
ENDMETHOD.

Best Practices

Do’s

EmpfehlungGrund
@Search Annotationen setzenAktiviert Typeahead-Suche
Paging implementierenGroße Datenmengen handhaben
Filter an Quelle delegierenPerformance verbessern
Caching nutzenWiederholte Aufrufe beschleunigen
Total Count korrekt setzenPagination funktioniert
Feldnamen uppercase vergleichenFilterbedingungen matchen

Don’ts

VermeidenGrund
Alle Daten ladenMemory- und Performance-Probleme
Ohne FehlerbehandlungApp stürzt bei API-Fehlern ab
Synchrone lange AufrufeUI friert ein
Hardcoded FilterFlexibilität geht verloren

Debugging

Für die Fehlersuche in Custom Entity Value Helps:

METHOD if_rap_query_provider~select.
" Logging aktivieren
DATA(lo_log) = cl_bali_log=>create_with_header(
header = cl_bali_header_setter=>create(
object = 'ZVH_DEBUG'
subobject = 'QUERY'
external_id = |VH_{ sy-uname }_{ sy-datum }|
)
).
TRY.
" Filter loggen
DATA(lt_filter) = io_request->get_filter( )->get_as_ranges( ).
LOOP AT lt_filter INTO DATA(ls_filter).
lo_log->add_item( cl_bali_free_text_setter=>create(
severity = if_bali_constants=>c_severity_information
text = |Filter: { ls_filter-name }|
) ).
ENDLOOP.
" ...
CATCH cx_root INTO DATA(lx_error).
lo_log->add_item( cl_bali_exception_setter=>create(
severity = if_bali_constants=>c_severity_error
exception = lx_error
) ).
ENDTRY.
" Log speichern
cl_bali_log_db=>get_instance( )->save_log( log = lo_log ).
ENDMETHOD.

Zusammenfassung

Custom Entity Value Helps bieten maximale Flexibilität für dynamische Wertehilfen:

AspektImplementation
Definitiondefine custom entity mit @ObjectModel.query.implementedBy
Query-KlasseIF_RAP_QUERY_PROVIDER~SELECT implementieren
Filterio_request->get_filter( )->get_as_ranges( )
Sucheio_request->get_search_expression( )
Pagingget_paging( )->get_offset( ) / get_page_size( )
Binding@Consumption.valueHelpDefinition wie bei CDS Views

Mit Custom Entity Value Helps können Sie beliebige Datenquellen als Wertehilfen einbinden und dabei alle Features wie Filterung, Suche und automatische Feldübernahme nutzen.

Verwandte Themen