Éléments virtuels dans les vues CDS : Champs calculés avec logique ABAP

Catégorie
CDS
Publié
Auteur
Johannes

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 virtuelChamp CDS calculé
CalculLogique ABAPExpression CDS
ComplexitéIllimitéeLimites de syntaxe CDS
Données externesPossibleImpossible
PerformanceRoundtrip supplémentaireCalculé dans la BD
AgrégationImpossiblePossible
Clause WHERENon filtrableFiltrable

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 :

  1. Vue CDS avec la définition de champ et l’annotation
  2. Classe ABAP qui implémente l’interface IF_SADL_EXIT_CALC_ELEMENT_READ
  3. 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 :

LimitationDescriptionAlternative
Pas de filtrageWHERE VirtualField = 'X' ne fonctionne pasPré-filtrer dans la vue CDS
Pas d’agrégationSUM(VirtualField) ne fonctionne pasAgréger dans ABAP
Pas de triORDER BY VirtualField ne fonctionne pasTrier dans ABAP
Limitation OData$filter sur éléments virtuels ignoréFiltrer côté client
PerformanceCalcul après accès BDUtiliser 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.

Articles approfondis