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énario | Exemple |
|---|---|
| Sources de données externes | Aéroports depuis API plan de vol externe |
| Logique complexe | Vols disponibles basés sur date et route |
| Filtres dynamiques | Aides de recherche dépendant du contexte |
| Données agrégées | Statistiques comme liste de sélection |
| Données autorisées | Valeurs 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: #Mdefine 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
| Annotation | But |
|---|---|
@Search.searchable | Active la recherche générique |
@Search.defaultSearchElement | Champ considéré lors de la recherche |
@Search.fuzzinessThreshold | Tolérance pour fautes de frappe (1.0 = exact) |
@ObjectModel.resultSet.sizeCategory | Indicateur 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
| Recommandation | Raison |
|---|---|
Définir annotations @Search | Active la recherche Typeahead |
| Implémenter le paging | Gérer les grandes quantités de données |
| Déléguer le filtre à la source | Améliorer les performances |
| Utiliser le caching | Accélérer les appels répétés |
| Définir correctement Total Count | La pagination fonctionne |
| Comparer les noms de champs en majuscules | Les conditions de filtre correspondent |
Don’ts
| Éviter | Raison |
|---|---|
| Charger toutes les données | Problèmes de mémoire et performance |
| Sans gestion des erreurs | L’app plante en cas d’erreur API |
| Appels longs synchrones | L’UI gèle |
| Filtres codés en dur | La 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 :
| Aspect | Implémentation |
|---|---|
| Définition | define custom entity avec @ObjectModel.query.implementedBy |
| Classe Query | Implémenter IF_RAP_QUERY_PROVIDER~SELECT |
| Filtre | io_request->get_filter( )->get_as_ranges( ) |
| Recherche | io_request->get_search_expression( ) |
| Paging | get_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
- RAP Value Helps - Aides de recherche basées sur annotations avec CDS Views
- Custom Entities - Intégrer des sources de données externes dans RAP
- Recherche générique dans RAP - Annotations @Search pour Fiori Elements
- RAP Grundlagen - Introduction à la programmation ABAP RESTful