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:
| Szenario | Beispiel |
|---|---|
| Externe Datenquellen | Flughäfen aus externem Flugplan-API |
| Komplexe Logik | Verfügbare Flüge basierend auf Datum und Route |
| Dynamische Filter | Kontextabhängige Wertehilfen |
| Aggregierte Daten | Statistiken als Auswahlliste |
| Berechtigte Daten | Werte 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: #Mdefine 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
| Annotation | Zweck |
|---|---|
@Search.searchable | Aktiviert die generische Suche |
@Search.defaultSearchElement | Feld wird bei Suche berücksichtigt |
@Search.fuzzinessThreshold | Toleranz für Tippfehler (1.0 = exakt) |
@ObjectModel.resultSet.sizeCategory | Performance-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
| Empfehlung | Grund |
|---|---|
@Search Annotationen setzen | Aktiviert Typeahead-Suche |
| Paging implementieren | Große Datenmengen handhaben |
| Filter an Quelle delegieren | Performance verbessern |
| Caching nutzen | Wiederholte Aufrufe beschleunigen |
| Total Count korrekt setzen | Pagination funktioniert |
| Feldnamen uppercase vergleichen | Filterbedingungen matchen |
Don’ts
| Vermeiden | Grund |
|---|---|
| Alle Daten laden | Memory- und Performance-Probleme |
| Ohne Fehlerbehandlung | App stürzt bei API-Fehlern ab |
| Synchrone lange Aufrufe | UI friert ein |
| Hardcoded Filter | Flexibilitä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:
| Aspekt | Implementation |
|---|---|
| Definition | define custom entity mit @ObjectModel.query.implementedBy |
| Query-Klasse | IF_RAP_QUERY_PROVIDER~SELECT implementieren |
| Filter | io_request->get_filter( )->get_as_ranges( ) |
| Suche | io_request->get_search_expression( ) |
| Paging | get_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
- RAP Value Helps - Annotation-basierte Wertehilfen mit CDS Views
- Custom Entities - Externe Datenquellen in RAP einbinden
- Generische Suche in RAP - @Search Annotationen für Fiori Elements
- RAP Grundlagen - Einführung in das RESTful ABAP Programming