Custom Entity Value Help Deep Dive : Aides de recherche dynamiques dans RAP

Catégorie
RAP
Publié
Auteur
Johannes

Les Value Helps basées sur CDS standard sont idéales quand les données sont dans des tables de base de données. Mais que faire si l’aide de recherche provient d’API externes, de calculs complexes ou de sources dynamiques ? C’est là qu’interviennent les Custom Entity Value Helps.

Quand utiliser les Custom Entity Value Helps ?

Les Custom Entity Value Helps sont le bon choix pour :

ScénarioExemple
Sources de données externesAéroports depuis API plan de vol externe
Logique complexeVols disponibles basés sur date et route
Filtres dynamiquesAides de recherche dépendant du contexte
Données agrégéesStatistiques comme liste de sélection
Données autoriséesValeurs dépendant de l’utilisateur actuel

Architecture d’une 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 │
│ - Évaluer les filtres │
│ - Implémenter la recherche │
│ - Retourner les données │
└─────────────────────────────────────────────────────────┘

Étape 1 : Définition Custom Entity

La Custom Entity définit la structure de la Value Help :

@EndUserText.label: 'Value Help Aéroport"
@ObjectModel.query.implementedBy: 'ABAP:ZCL_AIRPORT_VH_QUERY"
@Search.searchable: true
@ObjectModel.resultSet.sizeCategory: #M
define custom entity ZI_AirportVH
{
@EndUserText.label: 'Code aéroport"
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 1.0
@Search.ranking: #HIGH
key AirportCode : abap.char(3);
@EndUserText.label: 'Nom aéroport"
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
@Search.ranking: #MEDIUM
AirportName : abap.char(100);
@EndUserText.label: 'Ville"
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.85
City : abap.char(40);
@EndUserText.label: 'Pays"
Country : abap.char(3);
@EndUserText.label: 'Fuseau horaire"
Timezone : abap.char(20);
@EndUserText.label: 'Actif"
IsActive : abap_boolean;
}

Annotations importantes pour Value Helps

AnnotationBut
@Search.searchableActive la recherche générique
@Search.defaultSearchElementChamp considéré lors de la recherche
@Search.fuzzinessThresholdTolérance pour fautes de frappe (1.0 = exact)
@ObjectModel.resultSet.sizeCategoryIndicateur de performance pour Fiori

Étape 2 : Query Implementation Class

La classe Query implémente IF_RAP_QUERY_PROVIDER et fournit les données :

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. Charger les données
DATA(lt_airports) = get_airports( ).
" 2. Appliquer les filtres
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.
" Pas de filtre Range disponible
ENDTRY.
" 3. Appliquer la recherche (si présente)
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.
" Pas de recherche
ENDTRY.
" 4. Appliquer le tri
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. Nombre total avant paging
DATA(lv_total_count) = lines( lt_airports ).
" 6. Appliquer le paging
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. Convertir le résultat en format Entity
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. Retourner le nombre total
IF io_request->is_total_numb_of_rec_requested( ).
io_response->set_total_number_of_records( lv_total_count ).
ENDIF.
ENDMETHOD.
METHOD get_airports.
" En pratique : Appel API, BAPI, RFC, etc.
" Ici : Données d'exemple
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.
" Recherche floue sur plusieurs champs
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.

Étape 3 : Value Help Binding

La Custom Entity est utilisée comme une CDS View normale comme Value Help :

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
}

Filtrage dans Custom Entity Value Helps

Filtres statiques

Les filtres sont automatiquement transmis à la classe Query :

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

La classe Query reçoit le filtre ISACTIVE = 'X' dans get_filter( )->get_as_ranges( ).

Filtres dynamiques

Dépendant d’autres champs :

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

Ici, seuls les aéroports dans le même pays que l’aéroport de départ sont affichés.

Implémenter la recherche

La recherche générique est récupérée via get_search_expression() :

METHOD apply_search.
DATA(lv_search_upper) = to_upper( iv_search ).
" Recherche wildcard simple
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.

Recherche floue avancée

Pour de meilleurs résultats de recherche avec fautes de frappe :

METHOD apply_fuzzy_search.
" Distance de Levenshtein pour matching flou
" Threshold : 0.8 = 80% de correspondance
DATA(lv_threshold) = 80.
LOOP AT ct_data ASSIGNING FIELD-SYMBOL(<ls_data>).
DATA(lv_match) = abap_false.
" Vérifier Airport Code (exact)
IF to_upper( <ls_data>-airport_code ) CS to_upper( iv_search ).
lv_match = abap_true.
ENDIF.
" Vérifier Airport Name (flou)
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.
" Vérifier City (flou)
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.

Optimisation des performances

1. Caching pour données statiques

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.
" Validité du cache : 10 minutes
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. Déléguer le filtre à la source de données

Si l’API externe supporte les filtres :

METHOD get_airports_filtered.
" Préparer le filtre pour l'API
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.
" Appeler l'API avec filtre
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. Limiter l’ensemble de résultats

METHOD if_rap_query_provider~select.
" ...
" Retourner maximum 1000 entrées
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.

Exemple complet d’aéroport

Custom Entity avec tous les champs

@EndUserText.label: 'Value Help Aéroport"
@ObjectModel.query.implementedBy: 'ABAP:ZCL_AIRPORT_VALUE_HELP"
@Search.searchable: true
@ObjectModel.resultSet.sizeCategory: #M
@UI.headerInfo: {
typeName: 'Aéroport',
typeNamePlural: 'Aéroports"
}
define custom entity ZI_AirportValueHelp
{
@EndUserText.label: 'Code IATA"
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 1.0
@Search.ranking: #HIGH
@UI.lineItem: [{ position: 10 }]
key AirportId : abap.char(3);
@EndUserText.label: 'Nom"
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
@Search.ranking: #MEDIUM
@UI.lineItem: [{ position: 20 }]
AirportName : abap.char(100);
@EndUserText.label: 'Ville"
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.85
@UI.lineItem: [{ position: 30 }]
City : abap.char(40);
@EndUserText.label: 'Pays"
@UI.lineItem: [{ position: 40 }]
CountryCode : abap.char(2);
@EndUserText.label: 'Nom pays"
CountryName : abap.char(60);
@EndUserText.label: 'Région"
Region : abap.char(20);
@EndUserText.label: 'Latitude"
Latitude : abap.dec(9,6);
@EndUserText.label: 'Longitude"
Longitude : abap.dec(9,6);
@EndUserText.label: 'Fuseau horaire"
Timezone : abap.char(40);
@EndUserText.label: 'Statut"
@UI.lineItem: [{ position: 50, criticality: 'StatusCriticality' }]
Status : abap.char(10);
StatusCriticality : abap.int1;
}

Utilisation dans 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: 'De' }]
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: 'Vers' }]
ArrivalAirport,
ArrivalAirportName,
ArrivalCity,
FlightDate,
PassengerName,
BookingStatus
}

Gestion des erreurs

Une gestion robuste des erreurs est importante pour les 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 non configurée
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 non accessible
" Fallback : Retourner liste vide
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.

Bonnes pratiques

Do’s

RecommandationRaison
Définir annotations @SearchActive la recherche Typeahead
Implémenter le pagingGérer les grandes quantités de données
Déléguer le filtre à la sourceAméliorer les performances
Utiliser le cachingAccélérer les appels répétés
Définir correctement Total CountLa pagination fonctionne
Comparer les noms de champs en majusculesLes conditions de filtre correspondent

Don’ts

ÉviterRaison
Charger toutes les donnéesProblèmes de mémoire et performance
Sans gestion des erreursL’app plante en cas d’erreur API
Appels longs synchronesL’UI gèle
Filtres codés en durLa flexibilité est perdue

Debugging

Pour la recherche d’erreurs dans Custom Entity Value Helps :

METHOD if_rap_query_provider~select.
" Activer le logging
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.
" Logger les filtres
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 = |Filtre : { 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.
" Sauvegarder le log
cl_bali_log_db=>get_instance( )->save_log( log = lo_log ).
ENDMETHOD.

Résumé

Les Custom Entity Value Helps offrent une flexibilité maximale pour les aides de recherche dynamiques :

AspectImplémentation
Définitiondefine custom entity avec @ObjectModel.query.implementedBy
Classe QueryImplémenter IF_RAP_QUERY_PROVIDER~SELECT
Filtreio_request->get_filter( )->get_as_ranges( )
Rechercheio_request->get_search_expression( )
Pagingget_paging( )->get_offset( ) / get_page_size( )
Binding@Consumption.valueHelpDefinition comme avec CDS Views

Avec les Custom Entity Value Helps, vous pouvez intégrer n’importe quelle source de données comme aide de recherche tout en utilisant toutes les fonctionnalités comme le filtrage, la recherche et la reprise automatique de champs.

Sujets connexes