Virtual Elements in CDS Views: Berechnete Felder mit ABAP-Logik

kategorie
CDS
Veröffentlicht
autor
Johannes

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.

AspektVirtual ElementBerechnetes CDS-Feld
BerechnungABAP-LogikCDS-Ausdruck
KomplexitätUnbegrenztCDS-Syntax-Grenzen
Externe DatenMöglichNicht möglich
PerformanceZusätzlicher RoundtripIn DB berechnet
AggregationNicht möglichMöglich
WHERE-KlauselNicht filterbarFilterbar

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:

  1. CDS View mit der Feld-Definition und Annotation
  2. ABAP-Klasse die das Interface IF_SADL_EXIT_CALC_ELEMENT_READ implementiert
  3. Registrierung über die @ObjectModel.virtualElementCalculatedBy Annotation

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änkungBeschreibungAlternative
Kein FilteringWHERE VirtualField = 'X' funktioniert nichtIn CDS View vorfiltern
Keine AggregationSUM(VirtualField) funktioniert nichtIn ABAP aggregieren
Keine SortierungORDER BY VirtualField funktioniert nichtIn ABAP sortieren
OData-Einschränkung$filter auf Virtual Elements ignoriertClient-seitig filtern
PerformanceBerechnung nach DB-ZugriffBatch-Verarbeitung nutzen

Workaround für Filterung

" Im Consumer: Alle Daten holen und in ABAP filtern
SELECT FROM ZI_CustomerRating
FIELDS *
INTO TABLE @DATA(lt_customers).
" Virtual Element in ABAP filtern
DELETE 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.

Weiterführende Artikel