Custom Entity Value Help Deep Dive: Ayudas de Valores Dinámicas en RAP

Kategorie
RAP
Veröffentlicht
Autor
Johannes

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:

EscenarioEjemplo
Fuentes de datos externasAeropuertos desde API externa de horarios de vuelos
Lógica complejaVuelos disponibles según fecha y ruta
Filtros dinámicosValue Helps dependientes del contexto
Datos agregadosEstadísticas como lista de selección
Datos autorizadosValores 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: #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;
}

Anotaciones Importantes para Value Helps

AnotaciónPropósito
@Search.searchableActiva la búsqueda genérica
@Search.defaultSearchElementEl campo se considera en la búsqueda
@Search.fuzzinessThresholdTolerancia para errores de escritura (1.0 = exacto)
@ObjectModel.resultSet.sizeCategoryIndicació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ónRazón
Establecer anotaciones @SearchActiva búsqueda Typeahead
Implementar PagingManejar grandes volúmenes de datos
Delegar filtros a la fuenteMejorar rendimiento
Usar cachingAcelerar llamadas repetidas
Establecer Total Count correctamenteLa paginación funciona
Comparar nombres de campo en mayúsculasLas condiciones de filtro coinciden

Evitar

EvitarRazón
Cargar todos los datosProblemas de memoria y rendimiento
Sin manejo de erroresLa app falla con errores de API
Llamadas síncronas largasLa UI se congela
Filtros hardcodedSe 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:

AspectoImplementación
Definicióndefine custom entity con @ObjectModel.query.implementedBy
Clase QueryImplementar IF_RAP_QUERY_PROVIDER~SELECT
Filtroio_request->get_filter( )->get_as_ranges( )
Búsquedaio_request->get_search_expression( )
Pagingget_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