Los Value Helps estándar basados en CDS son ideales cuando los datos están en tablas de base de datos. Pero, ¿qué pasa cuando la ayuda de valores proviene de APIs externas, cálculos complejos o fuentes dinámicas? Aquí entran en juego los Custom Entity Value Helps.
¿Cuándo usar Custom Entity Value Helps?
Los Custom Entity Value Helps son la elección correcta para:
| Escenario | Ejemplo |
|---|---|
| Fuentes de datos externas | Aeropuertos desde API externa de horarios de vuelos |
| Lógica compleja | Vuelos disponibles según fecha y ruta |
| Filtros dinámicos | Value Helps dependientes del contexto |
| Datos agregados | Estadísticas como lista de selección |
| Datos autorizados | Valores dependiendo del usuario actual |
Arquitectura de un 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 │└─────────────────────────────────────────────────────────┘Paso 1: Definición de Custom Entity
La Custom Entity define la estructura del 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;}Anotaciones Importantes para Value Helps
| Anotación | Propósito |
|---|---|
@Search.searchable | Activa la búsqueda genérica |
@Search.defaultSearchElement | El campo se considera en la búsqueda |
@Search.fuzzinessThreshold | Tolerancia para errores de escritura (1.0 = exacto) |
@ObjectModel.resultSet.sizeCategory | Indicación de rendimiento para Fiori |
Paso 2: Clase de Implementación de Query
La clase Query implementa IF_RAP_QUERY_PROVIDER y proporciona los datos:
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.Paso 3: Binding del Value Help
La Custom Entity se usa como Value Help igual que una CDS View normal:
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}Filtrado en Custom Entity Value Helps
Filtros Estáticos
Los filtros se pasan automáticamente a la clase Query:
@Consumption.valueHelpDefinition: [{ entity: { name: 'ZI_AirportVH', element: 'AirportCode' }, additionalBinding: [{ element: 'IsActive', localConstant: 'X', usage: #FILTER }]}]DepartureAirport,La clase Query recibe el filtro ISACTIVE = 'X' en get_filter( )->get_as_ranges( ).
Filtros Dinámicos
Dependiendo de otros campos:
@Consumption.valueHelpDefinition: [{ entity: { name: 'ZI_AirportVH', element: 'AirportCode' }, additionalBinding: [{ element: 'Country', localElement: 'DepartureCountry', usage: #FILTER }]}]ArrivalAirport,Aquí solo se muestran aeropuertos en el mismo país que el aeropuerto de salida.
Implementar Búsqueda
La búsqueda genérica se obtiene mediante get_search_expression():
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.Búsqueda Fuzzy Avanzada
Para mejores resultados de búsqueda con errores de escritura:
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.Optimización de Rendimiento
1. Caching para Datos Estáticos
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. Delegar Filtro a la Fuente de Datos
Si la API externa soporta filtros:
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. Limitar Conjunto de Resultados
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.Ejemplo Completo de Aeropuertos
Custom Entity con Todos los Campos
@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;}Uso en la 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}Manejo de Errores
El manejo robusto de errores es importante para Custom Entity Value Helps:
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.Mejores Prácticas
Recomendado
| Recomendación | Razón |
|---|---|
Establecer anotaciones @Search | Activa búsqueda Typeahead |
| Implementar Paging | Manejar grandes volúmenes de datos |
| Delegar filtros a la fuente | Mejorar rendimiento |
| Usar caching | Acelerar llamadas repetidas |
| Establecer Total Count correctamente | La paginación funciona |
| Comparar nombres de campo en mayúsculas | Las condiciones de filtro coinciden |
Evitar
| Evitar | Razón |
|---|---|
| Cargar todos los datos | Problemas de memoria y rendimiento |
| Sin manejo de errores | La app falla con errores de API |
| Llamadas síncronas largas | La UI se congela |
| Filtros hardcoded | Se pierde flexibilidad |
Depuración
Para la búsqueda de errores en 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.Resumen
Los Custom Entity Value Helps ofrecen máxima flexibilidad para ayudas de valores dinámicas:
| Aspecto | Implementación |
|---|---|
| Definición | define custom entity con @ObjectModel.query.implementedBy |
| Clase Query | Implementar IF_RAP_QUERY_PROVIDER~SELECT |
| Filtro | io_request->get_filter( )->get_as_ranges( ) |
| Búsqueda | io_request->get_search_expression( ) |
| Paging | get_paging( )->get_offset( ) / get_page_size( ) |
| Binding | @Consumption.valueHelpDefinition como con CDS Views |
Con Custom Entity Value Helps puede integrar cualquier fuente de datos como ayuda de valores y usar todas las funciones como filtrado, búsqueda y transferencia automática de campos.
Temas Relacionados
- RAP Value Helps - Ayudas de valores basadas en anotaciones con CDS Views
- Custom Entities - Integrar fuentes de datos externas en RAP
- Búsqueda Genérica en RAP - Anotaciones @Search para Fiori Elements
- Fundamentos de RAP - Introducción al RESTful ABAP Programming