Virtual Elements ermöglichen es, berechnete Felder in CDS Views zu definieren, deren Werte zur Laufzeit durch ABAP-Code bestimmt werden. Dies ist besonders nützlich, wenn die Berechnungslogik zu komplex für CDS-Ausdrücke ist oder externe Datenquellen benötigt werden.
Was sind Virtual Elements?
Virtual Elements sind CDS-Felder, die nicht auf Datenbankspalten basieren, sondern durch eine ABAP-Klasse zur Laufzeit berechnet werden. Sie erscheinen für den Konsumenten wie normale Felder.
| Aspekt | Virtual Element | Berechnetes CDS-Feld |
|---|---|---|
| Berechnung | ABAP-Logik | CDS-Ausdruck |
| Komplexität | Unbegrenzt | CDS-Syntax-Grenzen |
| Externe Daten | Möglich | Nicht möglich |
| Performance | Zusätzlicher Roundtrip | In DB berechnet |
| Aggregation | Nicht möglich | Möglich |
| WHERE-Klausel | Nicht filterbar | Filterbar |
Wann Virtual Elements nutzen?
Virtual Elements sind die richtige Wahl, wenn:
- Die Berechnungslogik zu komplex für CDS ist (z.B. verschachtelte Bedingungen)
- Externe APIs oder Services aufgerufen werden müssen
- Stammdaten-Lookups in anderen Systemen erforderlich sind
- Formatierungen oder Konvertierungen nötig sind, die CDS nicht unterstützt
- Business-Logik aus bestehenden ABAP-Klassen wiederverwendet werden soll
Grundlegende Implementierung
Die Implementierung eines Virtual Elements besteht aus drei Teilen:
- CDS View mit der Feld-Definition und Annotation
- ABAP-Klasse die das Interface
IF_SADL_EXIT_CALC_ELEMENT_READimplementiert - Registrierung über die
@ObjectModel.virtualElementCalculatedByAnnotation
Schritt 1: CDS View definieren
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Bestellungen mit Gesamtwert'
define view entity ZI_Orders as select from zorders{ key order_id, customer_id, order_date, currency,
-- Virtual Element für berechneten Gesamtwert @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_ORDER_CALCULATIONS' @EndUserText.label: 'Gesamtwert' cast( 0 as abap.dec(15,2) ) as TotalAmount,
-- Virtual Element für Status-Text @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_ORDER_CALCULATIONS' @EndUserText.label: 'Status' cast( '' as abap.char(20) ) as StatusText}Schritt 2: ABAP-Klasse implementieren
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. " Hier deklarieren, welche Felder für die Berechnung benötigt werden IF line_exists( it_requested_calc_elements[ table_line = 'TOTALAMOUNT' ] ). " Für TotalAmount brauchen wir order_id und 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' ] ). " Für StatusText brauchen wir order_id APPEND 'ORDER_ID' TO et_requested_orig_elements. ENDIF. ENDMETHOD.
METHOD if_sadl_exit_calc_element_read~calculate. " Prüfen ob TotalAmount berechnet werden soll DATA(lv_calculate_total) = xsdbool( line_exists( it_requested_calc_elements[ table_line = 'TOTALAMOUNT' ] ) ).
" Prüfen ob StatusText berechnet werden soll DATA(lv_calculate_status) = xsdbool( line_exists( it_requested_calc_elements[ table_line = 'STATUSTEXT' ] ) ).
" Feldzeiger für die Ausgabe vorbereiten LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). " Order-ID lesen ASSIGN COMPONENT 'ORDER_ID' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_order_id>). CHECK sy-subrc = 0.
" TotalAmount berechnen IF lv_calculate_total = abap_true. ASSIGN COMPONENT 'TOTALAMOUNT' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_total>). IF sy-subrc = 0. " Positionen summieren SELECT SUM( quantity * unit_price ) FROM zorder_items WHERE order_id = @<lv_order_id> INTO @<lv_total>. ENDIF. ENDIF.
" StatusText berechnen IF lv_calculate_status = abap_true. ASSIGN COMPONENT 'STATUSTEXT' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_status>). IF sy-subrc = 0. " Status aus Stammdaten ermitteln SELECT SINGLE status_text FROM zorder_status WHERE order_id = @<lv_order_id> INTO @<lv_status>. IF sy-subrc <> 0. <lv_status> = 'Unbekannt'. ENDIF. ENDIF. ENDIF. ENDLOOP. ENDMETHOD.
ENDCLASS.Das Interface IF_SADL_EXIT_CALC_ELEMENT_READ
Das Interface definiert zwei Methoden, die für Virtual Elements implementiert werden müssen:
Methode get_calculation_info
Diese Methode wird vor dem Datenzugriff aufgerufen. Hier teilt die Klasse dem Framework mit, welche Original-Felder für die Berechnung benötigt werden.
METHOD if_sadl_exit_calc_element_read~get_calculation_info. " it_requested_calc_elements: Liste der angeforderten Virtual Elements " et_requested_orig_elements: Ausgabe - benötigte Originalfelder
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.Methode calculate
Diese Methode wird nach dem Datenzugriff aufgerufen. Hier werden die Virtual Elements berechnet.
METHOD if_sadl_exit_calc_element_read~calculate. " it_requested_calc_elements: Liste der zu berechnenden Elements " ct_calculated_data: Daten - hier werden die Werte geschrieben " it_original_data: Original-Daten (nur lesend)
LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). " Komponenten mit ASSIGN COMPONENT zuweisen ENDLOOP.ENDMETHOD.Praktisches Beispiel: Kundenrating berechnen
Ein häufiger Anwendungsfall ist die Berechnung eines Ratings basierend auf verschiedenen Faktoren.
CDS View mit Kundenrating
@AbapCatalog.viewEnhancementCategory: [#NONE]@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Kunden mit Rating'
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,
-- Virtual Element: Kundenrating (A, B, C, D) @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_CUSTOMER_RATING' @EndUserText.label: 'Rating' cast( '' as abap.char(1) ) as Rating,
-- Virtual Element: Rating-Beschreibung @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_CUSTOMER_RATING' @EndUserText.label: 'Rating-Beschreibung' cast( '' as abap.char(50) ) as RatingDescription,
-- Virtual Element: Umsatz letzte 12 Monate @ObjectModel.virtualElementCalculatedBy: 'ABAP:ZCL_CUSTOMER_RATING' @Semantics.amount.currencyCode: 'Currency' @EndUserText.label: 'Umsatz 12M' cast( 0 as abap.dec(15,2) ) as Revenue12Months,
-- Währung für Umsatz cast( 'EUR' as abap.cuky ) as Currency,
-- Association _Orders}ABAP-Klasse für Kundenrating
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. " Für alle Virtual Elements brauchen wir die 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. " Prüfen welche Felder berechnet werden sollen 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' ] ) ).
" Customer-IDs sammeln für Batch-Abfrage 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.
" Metriken für alle Kunden in einem Query berechnen 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).
" Ergebnisse zuordnen LOOP AT ct_calculated_data ASSIGNING <ls_data>. ASSIGN COMPONENT 'CUSTOMER_ID' OF STRUCTURE <ls_data> TO <lv_cust_id>. CHECK sy-subrc = 0.
" Metriken für diesen Kunden finden DATA(ls_metrics) = VALUE #( lt_metrics[ customer_id = <lv_cust_id> ] OPTIONAL ).
" Revenue berechnen 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.
" Rating berechnen (A/B/C/D basierend auf Umsatz) 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 'Premium-Kunde (>100k EUR)' WHEN 'B' THEN 'Wichtiger Kunde (>50k EUR)' WHEN 'C' THEN 'Standard-Kunde (>10k EUR)' WHEN 'D' THEN 'Gelegenheitskunde' ). ENDIF. ENDIF. ENDLOOP. ENDMETHOD.
METHOD calculate_customer_metrics. " Wird nicht mehr benötigt - Batch-Verarbeitung in calculate ENDMETHOD.
METHOD determine_rating. " Wird nicht mehr benötigt - inline in calculate ENDMETHOD.
ENDCLASS.Externe Services aufrufen
Virtual Elements eignen sich auch für die Integration externer Services.
Beispiel: Währungskurs-Konvertierung
CLASS zcl_currency_conversion DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_sadl_exit_calc_element_read.
ENDCLASS.
CLASS zcl_currency_conversion IMPLEMENTATION.
METHOD if_sadl_exit_calc_element_read~get_calculation_info. IF line_exists( it_requested_calc_elements[ table_line = 'AMOUNTINEUR' ] ). APPEND 'AMOUNT' TO et_requested_orig_elements. APPEND 'CURRENCY' TO et_requested_orig_elements. ENDIF. ENDMETHOD.
METHOD if_sadl_exit_calc_element_read~calculate. CHECK line_exists( it_requested_calc_elements[ table_line = 'AMOUNTINEUR' ] ).
DATA(lv_target_currency) = 'EUR'. DATA(lv_exchange_date) = cl_abap_context_info=>get_system_date( ).
LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). " Quellwerte lesen ASSIGN COMPONENT 'AMOUNT' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_amount>). ASSIGN COMPONENT 'CURRENCY' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_currency>). ASSIGN COMPONENT 'AMOUNTINEUR' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_amount_eur>).
CHECK sy-subrc = 0.
" Währungskonvertierung IF <lv_currency> = lv_target_currency. <lv_amount_eur> = <lv_amount>. ELSE. TRY. " Released API für Währungsumrechnung cl_currency_conversion=>convert_to_local_currency( EXPORTING date = lv_exchange_date foreign_amount = <lv_amount> foreign_currency = <lv_currency> local_currency = lv_target_currency IMPORTING local_amount = <lv_amount_eur> ). CATCH cx_currency_conversion. <lv_amount_eur> = 0. ENDTRY. ENDIF. ENDLOOP. ENDMETHOD.
ENDCLASS.Performance-Optimierungen
Virtual Elements können Performance-Probleme verursachen, wenn sie nicht effizient implementiert sind.
Batch-Verarbeitung statt Einzelabfragen
METHOD if_sadl_exit_calc_element_read~calculate. " SCHLECHT: Einzelabfrage pro Zeile LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). SELECT SINGLE ... INTO ... . " N+1 Problem! ENDLOOP.
" GUT: Batch-Abfrage für alle Zeilen " 1. Alle benötigten IDs sammeln 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. Einmal abfragen SELECT id, value FROM ztable WHERE id IN @lt_ids INTO TABLE @DATA(lt_values).
" 3. Ergebnisse zuordnen 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 ). " Zuweisung... ENDLOOP.ENDMETHOD.Caching für wiederverwendete Berechnungen
CLASS zcl_cached_calculation DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. INTERFACES if_sadl_exit_calc_element_read.
PRIVATE SECTION. CLASS-DATA gt_cache TYPE HASHED TABLE OF ty_s_cache WITH UNIQUE KEY cache_key.
ENDCLASS.
CLASS zcl_cached_calculation IMPLEMENTATION.
METHOD if_sadl_exit_calc_element_read~calculate. LOOP AT ct_calculated_data ASSIGNING FIELD-SYMBOL(<ls_data>). ASSIGN COMPONENT 'KEY_FIELD' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_key>).
" Cache prüfen DATA(ls_cached) = VALUE #( gt_cache[ cache_key = <lv_key> ] OPTIONAL ).
IF ls_cached IS NOT INITIAL. " Aus Cache ASSIGN COMPONENT 'RESULT' OF STRUCTURE <ls_data> TO FIELD-SYMBOL(<lv_result>). <lv_result> = ls_cached-value. ELSE. " Neu berechnen und cachen DATA(lv_calculated) = calculate_value( <lv_key> ). INSERT VALUE #( cache_key = <lv_key> value = lv_calculated ) INTO TABLE gt_cache. " Zuweisen... ENDIF. ENDLOOP. ENDMETHOD.
ENDCLASS.Bedingte Berechnung
METHOD if_sadl_exit_calc_element_read~get_calculation_info. " Nur angeforderte Felder und deren Abhängigkeiten laden LOOP AT it_requested_calc_elements INTO DATA(lv_element). CASE lv_element. WHEN 'EXPENSIVE_FIELD'. " Nur wenn explizit angefordert APPEND 'BASE_FIELD1' TO et_requested_orig_elements. APPEND 'BASE_FIELD2' TO et_requested_orig_elements. WHEN 'CHEAP_FIELD'. APPEND 'SIMPLE_FIELD' TO et_requested_orig_elements. ENDCASE. ENDLOOP.ENDMETHOD.Einschränkungen beachten
Virtual Elements haben wichtige Einschränkungen:
| Einschränkung | Beschreibung | Alternative |
|---|---|---|
| Kein Filtering | WHERE VirtualField = 'X' funktioniert nicht | In CDS View vorfiltern |
| Keine Aggregation | SUM(VirtualField) funktioniert nicht | In ABAP aggregieren |
| Keine Sortierung | ORDER BY VirtualField funktioniert nicht | In ABAP sortieren |
| OData-Einschränkung | $filter auf Virtual Elements ignoriert | Client-seitig filtern |
| Performance | Berechnung nach DB-Zugriff | Batch-Verarbeitung nutzen |
Workaround für Filterung
" Im Consumer: Alle Daten holen und in ABAP filternSELECT FROM ZI_CustomerRating FIELDS * INTO TABLE @DATA(lt_customers).
" Virtual Element in ABAP filternDELETE lt_customers WHERE rating <> 'A'.Virtual Elements in RAP
Bei der Verwendung von Virtual Elements in RAP-Szenarien gibt es Besonderheiten.
CDS Projection View
@EndUserText.label: 'Kunden Projection'@AccessControl.authorizationCheck: #NOT_REQUIRED@Metadata.allowExtensions: true
define root view entity ZC_Customer provider contract transactional_query as projection on ZI_CustomerRating{ key customer_id, customer_name, country,
-- Virtual Elements werden durchgereicht Rating, RatingDescription, Revenue12Months, Currency}UI-Annotations für Virtual Elements
@UI.lineItem: [ { position: 50, importance: #HIGH } ]@UI.identification: [ { position: 50 } ]@UI.selectionField: [ { position: 50 } ]Rating,
@UI.lineItem: [ { position: 60, importance: #MEDIUM } ]RatingDescription,
@UI.lineItem: [ { position: 70, importance: #HIGH } ]@Semantics.amount.currencyCode: 'Currency'Revenue12Months,Zusammenfassung
Virtual Elements in CDS Views bieten eine mächtige Möglichkeit, ABAP-Logik in datengetriebene Modelle zu integrieren:
- @ObjectModel.virtualElementCalculatedBy annotiert das Feld mit der ABAP-Klasse
- IF_SADL_EXIT_CALC_ELEMENT_READ Interface definiert die Berechnungslogik
- get_calculation_info deklariert benötigte Originalfelder
- calculate führt die eigentliche Berechnung durch
- Batch-Verarbeitung vermeidet N+1 Probleme
- Einschränkungen beachten: kein Filtering, Sorting oder Aggregation
Der große Vorteil ist die Trennung zwischen dem deklarativen CDS-Modell und der imperativen ABAP-Berechnungslogik. Das ermöglicht komplexe Berechnungen, die in reinem CDS nicht möglich wären.