Les éléments virtuels permettent de définir des champs calculés dans les vues CDS dont les valeurs sont déterminées à l’exécution par du code ABAP. Cela est particulièrement utile lorsque la logique de calcul est trop complexe pour les expressions CDS ou nécessite des sources de données externes.
Que sont les éléments virtuels ?
Les éléments virtuels sont des champs CDS qui ne sont pas basés sur des colonnes de base de données, mais calculés par une classe ABAP à l’exécution. Ils apparaissent pour le consommateur comme des champs normaux.
| Aspect | Élément virtuel | Champ CDS calculé |
|---|---|---|
| Calcul | Logique ABAP | Expression CDS |
| Complexité | Illimitée | Limites de syntaxe CDS |
| Données externes | Possible | Impossible |
| Performance | Roundtrip supplémentaire | Calculé dans la BD |
| Agrégation | Impossible | Possible |
| Clause WHERE | Non filtrable | Filtrable |
Quand utiliser les éléments virtuels ?
Les éléments virtuels sont le bon choix lorsque :
- La logique de calcul est trop complexe pour CDS (par ex. conditions imbriquées)
- Des APIs ou services externes doivent être appelés
- Des recherches de données de référence dans d’autres systèmes sont nécessaires
- Des formatages ou conversions non supportés par CDS sont nécessaires
- La logique métier de classes ABAP existantes doit être réutilisée
Implémentation de base
L’implémentation d’un élément virtuel comprend trois parties :
- Vue CDS avec la définition de champ et l’annotation
- Classe ABAP qui implémente l’interface
IF_SADL_EXIT_CALC_ELEMENT_READ - Enregistrement via l’annotation
@ObjectModel.virtualElementCalculatedBy
Étape 1 : Définir la vue CDS
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Commandes avec montant total"
define view entity ZI_Orders as select from zorders{ key order_id, customer_id, order_date, currency,
-- Élément virtuel pour montant total calculé @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_ORDER_CALCULATIONS" @EndUserText.label: 'Montant total" cast( 0 as abap.dec(15,2) ) as TotalAmount,
-- Élément virtuel pour texte de statut @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_ORDER_CALCULATIONS" @EndUserText.label: 'Statut" cast( '' as abap.char(20) ) as StatusText}Étape 2 : Implémenter la classe ABAP
CLASS zcl_order_calculations DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_sadl_exit_calc_element_read.
ENDCLASS.
CLASS zcl_order_calculations IMPLEMENTATION.
METHOD if_sadl_exit_calc_element_read~get_calculation_info. " Déclarer ici quels champs sont nécessaires pour le calcul IF line_exists( it_requested_calc_elements[ table_line = 'TOTALAMOUNT' ] ). " Pour TotalAmount nous avons besoin de order_id et currency APPEND 'ORDER_ID' TO et_requested_orig_elements. APPEND 'CURRENCY' TO et_requested_orig_elements. ENDIF.
IF line_exists( it_requested_calc_elements[ table_line = 'STATUSTEXT' ] ). " Pour StatusText nous avons besoin de order_id APPEND 'ORDER_ID' TO et_requested_orig_elements. ENDIF. ENDMETHOD.
METHOD if_sadl_exit_calc_element_read~calculate. " Vérifier si TotalAmount doit être calculé DATA(lv_calculate_total) = xsdbool( line_exists( it_requested_calc_elements[ table_line = 'TOTALAMOUNT' ] ) ).
" Vérifier si StatusText doit être calculé DATA(lv_calculate_status) = xsdbool( line_exists( it_requested_calc_elements[ table_line = 'STATUSTEXT' ] ) ).
" Préparer les pointeurs de champ pour la sortie LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). " Lire Order-ID ASSIGN COMPONENT 'ORDER_ID' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_order_id>). CHECK sy-subrc = 0.
" Calculer TotalAmount IF lv_calculate_total = abap_true. ASSIGN COMPONENT 'TOTALAMOUNT' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_total>). IF sy-subrc = 0. " Sommer les positions SELECT SUM( quantity * unit_price ) FROM zorder_items WHERE order_id = @<lv_order_id> INTO @<lv_total>. ENDIF. ENDIF.
" Calculer StatusText IF lv_calculate_status = abap_true. ASSIGN COMPONENT 'STATUSTEXT' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_status>). IF sy-subrc = 0. " Déterminer le statut des données de référence SELECT SINGLE status_text FROM zorder_status WHERE order_id = @<lv_order_id> INTO @<lv_status>. IF sy-subrc <> 0. <lv_status> = 'Inconnu'. ENDIF. ENDIF. ENDIF. ENDLOOP. ENDMETHOD.
ENDCLASS.L’interface IF_SADL_EXIT_CALC_ELEMENT_READ
L’interface définit deux méthodes qui doivent être implémentées pour les éléments virtuels :
Méthode get_calculation_info
Cette méthode est appelée avant l’accès aux données. Ici, la classe informe le framework quels champs originaux sont nécessaires pour le calcul.
METHOD if_sadl_exit_calc_element_read~get_calculation_info. " it_requested_calc_elements : Liste des éléments virtuels demandés " et_requested_orig_elements : Sortie - champs originaux nécessaires
LOOP AT it_requested_calc_elements INTO DATA(lv_element). CASE lv_element. WHEN 'VIRTUALFIELD1'. APPEND 'FIELD_A' TO et_requested_orig_elements. APPEND 'FIELD_B' TO et_requested_orig_elements. WHEN 'VIRTUALFIELD2'. APPEND 'FIELD_C' TO et_requested_orig_elements. ENDCASE. ENDLOOP.ENDMETHOD.Méthode calculate
Cette méthode est appelée après l’accès aux données. C’est ici que les éléments virtuels sont calculés.
METHOD if_sadl_exit_calc_element_read~calculate. " it_requested_calc_elements : Liste des éléments à calculer " ct_calculated_data : Données - les valeurs sont écrites ici " it_original_data : Données originales (lecture seule)
LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). " Assigner les composants avec ASSIGN COMPONENT ENDLOOP.ENDMETHOD.Exemple pratique : Calculer une notation client
Un cas d’utilisation fréquent est le calcul d’une notation basée sur différents facteurs.
Vue CDS avec notation client
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Clients avec notation"
define view entity ZI_CustomerRating as select from zcustomers as Customer association [0..*] to zorders as _Orders on $projection.customer_id = _Orders.customer_id{ key customer_id, customer_name, customer_since, country,
-- Élément virtuel : Notation client (A, B, C, D) @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_CUSTOMER_RATING" @EndUserText.label: 'Notation" cast( '' as abap.char(1) ) as Rating,
-- Élément virtuel : Description de la notation @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_CUSTOMER_RATING" @EndUserText.label: 'Description notation" cast( '' as abap.char(50) ) as RatingDescription,
-- Élément virtuel : Chiffre d'affaires 12 derniers mois @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_CUSTOMER_RATING" @Semantics.amount.currencyCode: 'Currency" @EndUserText.label: 'CA 12M" cast( 0 as abap.dec(15,2) ) as Revenue12Months,
-- Devise pour le chiffre d'affaires cast( 'EUR' as abap.cuky ) as Currency,
-- Association _Orders}Classe ABAP pour notation client
CLASS zcl_customer_rating DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_sadl_exit_calc_element_read.
PRIVATE SECTION. TYPES: BEGIN OF ty_s_customer_data, customer_id TYPE zcustomer_id, revenue_12m TYPE p LENGTH 15 DECIMALS 2, order_count TYPE i, days_since_order TYPE i, rating TYPE c LENGTH 1, rating_desc TYPE c LENGTH 50, END OF ty_s_customer_data. TYPES ty_t_customer_data TYPE HASHED TABLE OF ty_s_customer_data WITH UNIQUE KEY customer_id.
METHODS calculate_customer_metrics IMPORTING iv_customer_id TYPE zcustomer_id RETURNING VALUE(rs_metrics) TYPE ty_s_customer_data.
METHODS determine_rating IMPORTING is_metrics TYPE ty_s_customer_data RETURNING VALUE(rv_rating) TYPE c.
ENDCLASS.
CLASS zcl_customer_rating IMPLEMENTATION.
METHOD if_sadl_exit_calc_element_read~get_calculation_info. " Pour tous les éléments virtuels nous avons besoin du Customer-ID IF line_exists( it_requested_calc_elements[ table_line = 'RATING' ] ) OR line_exists( it_requested_calc_elements[ table_line = 'RATINGDESCRIPTION' ] ) OR line_exists( it_requested_calc_elements[ table_line = 'REVENUE12MONTHS' ] ).
APPEND 'CUSTOMER_ID' TO et_requested_orig_elements. APPEND 'CUSTOMER_SINCE' TO et_requested_orig_elements. ENDIF. ENDMETHOD.
METHOD if_sadl_exit_calc_element_read~calculate. " Vérifier quels champs doivent être calculés DATA(lv_calc_rating) = xsdbool( line_exists( it_requested_calc_elements[ table_line = 'RATING' ] ) ). DATA(lv_calc_desc) = xsdbool( line_exists( it_requested_calc_elements[ table_line = 'RATINGDESCRIPTION' ] ) ). DATA(lv_calc_revenue) = xsdbool( line_exists( it_requested_calc_elements[ table_line = 'REVENUE12MONTHS' ] ) ).
" Collecter les Customer-IDs pour requête en batch DATA lt_customer_ids TYPE RANGE OF zcustomer_id. LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). ASSIGN COMPONENT 'CUSTOMER_ID' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_cust_id>). CHECK sy-subrc = 0. APPEND VALUE #( sign = 'I' option = 'EQ' low = <lv_cust_id> ) TO lt_customer_ids. ENDLOOP.
" Calculer les métriques pour tous les clients dans une requête DATA(lv_date_12m_ago) = cl_abap_context_info=>get_system_date( ) - 365.
SELECT customer_id, SUM( total_amount ) AS revenue_12m, COUNT( * ) AS order_count, MAX( order_date ) AS last_order_date FROM zorders WHERE customer_id IN @lt_customer_ids AND order_date >= @lv_date_12m_ago GROUP BY customer_id INTO TABLE @DATA(lt_metrics).
" Assigner les résultats LOOP AT ct_calculated_data ASSIGNING <ls_data>. ASSIGN COMPONENT 'CUSTOMER_ID' OF STRUCTURE <ls_data> TO <lv_cust_id>. CHECK sy-subrc = 0.
" Trouver les métriques pour ce client DATA(ls_metrics) = VALUE #( lt_metrics[ customer_id = <lv_cust_id> ] OPTIONAL ).
" Calculer Revenue IF lv_calc_revenue = abap_true. ASSIGN COMPONENT 'REVENUE12MONTHS' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_revenue>). IF sy-subrc = 0. <lv_revenue> = ls_metrics-revenue_12m. ENDIF. ENDIF.
" Calculer Rating (A/B/C/D basé sur le chiffre d'affaires) DATA lv_rating TYPE c LENGTH 1. IF ls_metrics-revenue_12m >= 100000. lv_rating = 'A'. ELSEIF ls_metrics-revenue_12m >= 50000. lv_rating = 'B'. ELSEIF ls_metrics-revenue_12m >= 10000. lv_rating = 'C'. ELSE. lv_rating = 'D'. ENDIF.
IF lv_calc_rating = abap_true. ASSIGN COMPONENT 'RATING' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_rating>). IF sy-subrc = 0. <lv_rating> = lv_rating. ENDIF. ENDIF.
IF lv_calc_desc = abap_true. ASSIGN COMPONENT 'RATINGDESCRIPTION' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_desc>). IF sy-subrc = 0. <lv_desc> = SWITCH #( lv_rating WHEN 'A' THEN 'Client premium (>100k EUR)" WHEN 'B' THEN 'Client important (>50k EUR)" WHEN 'C' THEN 'Client standard (>10k EUR)" WHEN 'D' THEN 'Client occasionnel" ). ENDIF. ENDIF. ENDLOOP. ENDMETHOD.
METHOD calculate_customer_metrics. " N'est plus nécessaire - traitement en batch dans calculate ENDMETHOD.
METHOD determine_rating. " N'est plus nécessaire - inline dans calculate ENDMETHOD.
ENDCLASS.Optimisations de performance
Les éléments virtuels peuvent causer des problèmes de performance s’ils ne sont pas implémentés efficacement.
Traitement en batch au lieu de requêtes individuelles
METHOD if_sadl_exit_calc_element_read~calculate. " MAUVAIS : Requête individuelle par ligne LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). SELECT SINGLE ... INTO ... . " Problème N+1 ! ENDLOOP.
" BON : Requête en batch pour toutes les lignes " 1. Collecter tous les IDs nécessaires DATA lt_ids TYPE RANGE OF ty_id. LOOP AT ct_calculated_data ASSIGNING <ls_data>. ASSIGN COMPONENT 'ID' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_id>). APPEND VALUE #( sign = 'I' option = 'EQ' low = <lv_id> ) TO lt_ids. ENDLOOP.
" 2. Requête unique SELECT id, value FROM ztable WHERE id IN @lt_ids INTO TABLE @DATA(lt_values).
" 3. Assigner les résultats LOOP AT ct_calculated_data ASSIGNING <ls_data>. ASSIGN COMPONENT 'ID' OF STRUCTURE <ls_data> TO <lv_id>. DATA(ls_value) = VALUE #( lt_values[ id = <lv_id> ] OPTIONAL ). " Affectation... ENDLOOP.ENDMETHOD.Limitations à observer
Les éléments virtuels ont des limitations importantes :
| Limitation | Description | Alternative |
|---|---|---|
| Pas de filtrage | WHERE VirtualField = 'X' ne fonctionne pas | Pré-filtrer dans la vue CDS |
| Pas d’agrégation | SUM(VirtualField) ne fonctionne pas | Agréger dans ABAP |
| Pas de tri | ORDER BY VirtualField ne fonctionne pas | Trier dans ABAP |
| Limitation OData | $filter sur éléments virtuels ignoré | Filtrer côté client |
| Performance | Calcul après accès BD | Utiliser traitement batch |
Résumé
Les éléments virtuels dans les vues CDS offrent une possibilité puissante d’intégrer la logique ABAP dans des modèles orientés données :
- @ObjectModel.virtualElementCalculatedBy annote le champ avec la classe ABAP
- IF_SADL_EXIT_CALC_ELEMENT_READ interface définit la logique de calcul
- get_calculation_info déclare les champs originaux nécessaires
- calculate effectue le calcul proprement dit
- Traitement en batch évite les problèmes N+1
- Limitations à observer : pas de filtrage, tri ou agrégation
Le grand avantage est la séparation entre le modèle CDS déclaratif et la logique de calcul ABAP impérative. Cela permet des calculs complexes qui ne seraient pas possibles en CDS pur.