RAP Deep Insert/Update - Verschachtelte Entitäten speichern

kategorie
RAP
Veröffentlicht
autor
Johannes

Deep Insert und Deep Update ermöglichen es, Parent- und Child-Entities in einem einzigen Request zu speichern. Dies ist essentiell für komplexe Business Objects mit Composition-Beziehungen – etwa eine Bestellung mit Positionen oder eine Reise mit Buchungen.

Das Problem: Mehrere Requests

Ohne Deep Operations müssen Sie Parent und Children separat anlegen:

" ❌ Ohne Deep Insert: Mehrere Requests nötig
" 1. Parent anlegen
MODIFY ENTITIES OF zi_travel
ENTITY Travel
CREATE FIELDS ( AgencyId CustomerId )
WITH VALUE #( ( %cid = 'TRAVEL_1' AgencyId = '000001' CustomerId = '000042' ) )
MAPPED DATA(mapped_travel).
COMMIT ENTITIES.
" 2. Warten auf generierte TravelId
DATA(lv_travel_id) = mapped_travel-travel[ %cid = 'TRAVEL_1' ]-TravelId.
" 3. Children separat anlegen
MODIFY ENTITIES OF zi_travel
ENTITY Booking
CREATE FIELDS ( CarrierId FlightDate )
WITH VALUE #(
( TravelId = lv_travel_id BookingId = '0001' CarrierId = 'LH' FlightDate = '20260301' )
( TravelId = lv_travel_id BookingId = '0002' CarrierId = 'AA' FlightDate = '20260308' )
).
COMMIT ENTITIES.
" → 2 Round-Trips, keine Transaktionssicherheit über beide Schritte

Die Lösung: Deep Insert

Mit Deep Insert erstellen Sie Parent und Children in einer Operation:

" ✅ Deep Insert: Ein Request für alles
MODIFY ENTITIES OF zi_travel
ENTITY Travel
CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate Description )
WITH VALUE #( (
%cid = 'TRAVEL_1'
AgencyId = '000001'
CustomerId = '000042'
BeginDate = '20260301'
EndDate = '20260315'
Description = 'Geschäftsreise mit Flügen'
) )
" Children über Assoziation erstellen
ENTITY Travel CREATE BY \_Bookings
FIELDS ( CarrierId FlightDate FlightPrice CurrencyCode )
WITH VALUE #( (
%cid_ref = 'TRAVEL_1' " Referenz auf Parent %cid
%target = VALUE #(
( %cid = 'BOOKING_1'
CarrierId = 'LH'
FlightDate = '20260301'
FlightPrice = '599.00'
CurrencyCode = 'EUR' )
( %cid = 'BOOKING_2'
CarrierId = 'AA'
FlightDate = '20260315'
FlightPrice = '749.00'
CurrencyCode = 'EUR' )
)
) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES
RESPONSE OF zi_travel
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
" Alle generierten Keys abrufen:
DATA(lv_travel_id) = mapped-travel[ %cid = 'TRAVEL_1' ]-TravelId.
DATA(lv_booking1_id) = mapped-booking[ %cid = 'BOOKING_1' ]-BookingId.
DATA(lv_booking2_id) = mapped-booking[ %cid = 'BOOKING_2' ]-BookingId.

Composition-Beziehungen in CDS Views

Deep Operations funktionieren nur mit Composition-Beziehungen. Diese definieren eine starke Parent-Child-Beziehung mit Lebenszyklusabhängigkeit.

Root Entity (Parent)

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Travel - Root Entity'
define root view entity ZI_Travel
as select from ztravel
composition [0..*] of ZI_Booking as _Bookings -- Composition!
{
key travel_id as TravelId,
agency_id as AgencyId,
customer_id as CustomerId,
begin_date as BeginDate,
end_date as EndDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
total_price as TotalPrice,
@Semantics.currencyCode: true
currency_code as CurrencyCode,
description as Description,
status as Status,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
_Bookings -- Assoziation exponieren
}

Child Entity

@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Booking - Child Entity'
define view entity ZI_Booking
as select from zbooking
association to parent ZI_Travel as _Travel -- Parent-Assoziation
on $projection.TravelId = _Travel.TravelId
{
key travel_id as TravelId,
key booking_id as BookingId,
carrier_id as CarrierId,
flight_date as FlightDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
flight_price as FlightPrice,
@Semantics.currencyCode: true
currency_code as CurrencyCode,
booking_status as BookingStatus,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
_Travel -- Parent-Assoziation exponieren
}

Behavior Definition

managed implementation in class zbp_i_travel unique;
strict ( 2 );
define behavior for ZI_Travel alias Travel
persistent table ztravel
lock master
authorization master ( instance )
etag master LastChangedAt
{
create;
update;
delete;
field ( readonly ) TravelId;
field ( numbering : managed ) TravelId;
association _Bookings { create; } -- Deep Create aktivieren!
}
define behavior for ZI_Booking alias Booking
persistent table zbooking
lock dependent by _Travel
authorization dependent by _Travel
etag master LastChangedAt
{
update;
delete;
field ( readonly ) TravelId, BookingId;
field ( numbering : managed ) BookingId;
association _Travel;
}

Wichtig: association _Bookings { create; } aktiviert Deep Insert für die Composition.

Composition vs. Association

AspektCompositionAssociation
BeziehungStarke Abhängigkeit (Teil-Ganzes)Lose Referenz
LebenszyklusChild wird mit Parent gelöschtUnabhängig
Deep OperationsUnterstütztNicht unterstützt
Locklock dependent byEigenes Lock
Syntax CDScomposition [0..*] ofassociation [0..*] to
BeispielBestellung → PositionenBestellung → Kunde

Deep Update mit %control

Bei Deep Update aktualisieren Sie Parent und Children gemeinsam. Die %control-Struktur steuert, welche Felder tatsächlich aktualisiert werden.

Einfaches Deep Update

" Deep Update: Parent und Children gemeinsam ändern
MODIFY ENTITIES OF zi_travel
" Parent aktualisieren
ENTITY Travel
UPDATE FIELDS ( Description Status )
WITH VALUE #( (
TravelId = '00000001'
Description = 'Aktualisierte Beschreibung'
Status = 'A'
) )
" Children aktualisieren
ENTITY Booking
UPDATE FIELDS ( FlightPrice )
WITH VALUE #(
( TravelId = '00000001'
BookingId = '0001'
FlightPrice = '549.00' )
( TravelId = '00000001'
BookingId = '0002'
FlightPrice = '699.00' )
)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.

Update mit %control-Struktur

Die %control-Struktur gibt explizit an, welche Felder aktualisiert werden sollen:

" Präzise Kontrolle mit %control
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE
WITH VALUE #( (
TravelId = '00000001'
Description = 'Neue Beschreibung'
Status = 'A'
TotalPrice = '1500.00' " Wird NICHT aktualisiert (s.u.)
" Nur diese Felder werden geändert:
%control = VALUE #(
Description = if_abap_behv=>mk-on
Status = if_abap_behv=>mk-on
" TotalPrice = if_abap_behv=>mk-off " implizit
)
) )
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.
" → TotalPrice bleibt unverändert, obwohl ein Wert übergeben wurde

UPDATE SET FIELDS vs. UPDATE FIELDS

" UPDATE FIELDS: Explizite Feldliste
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE FIELDS ( Description Status ) -- Nur diese Felder
WITH VALUE #( ( TravelId = '00000001' Description = 'Text' Status = 'A' ) ).
" UPDATE SET FIELDS: Automatisch alle non-initial Felder
DATA(ls_update) = VALUE zi_travel(
TravelId = '00000001'
Description = 'Text'
Status = 'A'
" BeginDate ist initial → wird NICHT aktualisiert
).
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE SET FIELDS
WITH VALUE #( ( CORRESPONDING #( ls_update ) ) ).

Deep Update mit neuen Children erstellen

" Bestehenden Parent ändern UND neue Children hinzufügen
MODIFY ENTITIES OF zi_travel
" Parent aktualisieren
ENTITY Travel
UPDATE FIELDS ( Status )
WITH VALUE #( ( TravelId = '00000001' Status = 'P' ) )
" NEUE Bookings zum bestehenden Travel hinzufügen
ENTITY Travel CREATE BY \_Bookings
FIELDS ( CarrierId FlightDate FlightPrice CurrencyCode )
WITH VALUE #( (
TravelId = '00000001' " Existierender Parent
%target = VALUE #(
( %cid = 'NEW_BOOK_1'
CarrierId = 'UA'
FlightDate = '20260320'
FlightPrice = '899.00'
CurrencyCode = 'EUR' )
)
) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.
" Neue BookingId:
DATA(lv_new_booking) = mapped-booking[ %cid = 'NEW_BOOK_1' ]-BookingId.

Fehlerszenarien und Rollback

Validation bei Deep Insert

CLASS lhc_booking DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS validateFlightDate FOR VALIDATE ON SAVE
IMPORTING keys FOR Booking~validateFlightDate.
ENDCLASS.
CLASS lhc_booking IMPLEMENTATION.
METHOD validateFlightDate.
" Daten lesen
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Booking
FIELDS ( TravelId BookingId FlightDate )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_bookings).
" Parent-Daten für Validierung laden
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Booking BY \_Travel
FIELDS ( BeginDate EndDate )
WITH CORRESPONDING #( lt_bookings )
RESULT DATA(lt_travels).
LOOP AT lt_bookings INTO DATA(ls_booking).
" FlightDate muss innerhalb der Reisedaten liegen
DATA(ls_travel) = VALUE #( lt_travels[ TravelId = ls_booking-TravelId ] OPTIONAL ).
IF ls_travel IS NOT INITIAL.
IF ls_booking-FlightDate < ls_travel-BeginDate OR
ls_booking-FlightDate > ls_travel-EndDate.
APPEND VALUE #(
TravelId = ls_booking-TravelId
BookingId = ls_booking-BookingId
%fail-cause = if_abap_behv=>cause-unspecific
) TO failed-booking.
APPEND VALUE #(
TravelId = ls_booking-TravelId
BookingId = ls_booking-BookingId
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Flugdatum muss zwischen { ls_travel-BeginDate } und { ls_travel-EndDate } liegen|
)
%element-FlightDate = if_abap_behv=>mk-on
) TO reported-booking.
ENDIF.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Fehlerbehandlung bei Deep Operations

MODIFY ENTITIES OF zi_travel
ENTITY Travel
CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate )
WITH VALUE #( (
%cid = 'TRAVEL_1'
AgencyId = '000001'
CustomerId = '000042'
BeginDate = '20260301'
EndDate = '20260315'
) )
ENTITY Travel CREATE BY \_Bookings
FIELDS ( CarrierId FlightDate )
WITH VALUE #( (
%cid_ref = 'TRAVEL_1'
%target = VALUE #(
( %cid = 'BOOK_1' CarrierId = 'LH' FlightDate = '20260401' ) " Außerhalb!
)
) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
" Prüfen, ob Children fehlgeschlagen
IF failed-booking IS NOT INITIAL.
WRITE: / 'Fehler bei Buchungen:'.
LOOP AT reported-booking INTO DATA(ls_book_msg).
WRITE: / ls_book_msg-%msg->if_message~get_text( ).
ENDLOOP.
ENDIF.
" Prüfen, ob Parent fehlgeschlagen
IF failed-travel IS NOT INITIAL.
WRITE: / 'Fehler bei Travel:'.
LOOP AT reported-travel INTO DATA(ls_travel_msg).
WRITE: / ls_travel_msg-%msg->if_message~get_text( ).
ENDLOOP.
ENDIF.
" COMMIT nur bei Erfolg
IF failed-travel IS INITIAL AND failed-booking IS INITIAL.
COMMIT ENTITIES
RESPONSE OF zi_travel
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
IF commit_failed-travel IS INITIAL AND commit_failed-booking IS INITIAL.
WRITE: / 'Erfolgreich gespeichert'.
ELSE.
WRITE: / 'Commit fehlgeschlagen - Transaktion zurückgerollt'.
ENDIF.
ELSE.
WRITE: / 'Validation fehlgeschlagen - kein Commit'.
ENDIF.

Automatischer Rollback

Bei Deep Operations gilt: Alles oder nichts. Wenn ein Teil der Operation fehlschlägt, wird die gesamte Transaktion zurückgerollt.

" Szenario: Travel OK, aber ein Booking ungültig
MODIFY ENTITIES OF zi_travel
ENTITY Travel
CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate )
WITH VALUE #( (
%cid = 'T1'
AgencyId = '000001'
CustomerId = '000042'
BeginDate = '20260301'
EndDate = '20260310'
) )
ENTITY Travel CREATE BY \_Bookings
WITH VALUE #( (
%cid_ref = 'T1'
%target = VALUE #(
( %cid = 'B1' CarrierId = 'LH' FlightDate = '20260305' ) " OK
( %cid = 'B2' CarrierId = 'AA' FlightDate = '20260401' ) " Validation Error!
)
) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES
RESPONSE OF zi_travel
FAILED DATA(commit_failed).
" Bei Fehler: NICHTS wurde gespeichert!
" → Travel T1 wurde NICHT angelegt
" → Booking B1 wurde NICHT angelegt
" → Booking B2 wurde NICHT angelegt
" Die gesamte Transaktion ist zurückgerollt.

Multi-Level Deep Insert

Deep Operations funktionieren auch über mehrere Ebenen:

" Travel → Booking → BookingSupplement (3 Ebenen)
MODIFY ENTITIES OF zi_travel
" Level 1: Travel
ENTITY Travel
CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate )
WITH VALUE #( ( %cid = 'T1' AgencyId = '000001' CustomerId = '000042'
BeginDate = '20260301' EndDate = '20260315' ) )
" Level 2: Bookings
ENTITY Travel CREATE BY \_Bookings
FIELDS ( CarrierId FlightDate FlightPrice CurrencyCode )
WITH VALUE #( (
%cid_ref = 'T1'
%target = VALUE #(
( %cid = 'B1' CarrierId = 'LH' FlightDate = '20260301'
FlightPrice = '599.00' CurrencyCode = 'EUR' )
)
) )
" Level 3: Booking Supplements
ENTITY Booking CREATE BY \_BookingSupplements
FIELDS ( SupplementId Price CurrencyCode )
WITH VALUE #( (
%cid_ref = 'B1'
%target = VALUE #(
( %cid = 'S1' SupplementId = 'ML01' Price = '29.00' CurrencyCode = 'EUR' )
( %cid = 'S2' SupplementId = 'BG01' Price = '49.00' CurrencyCode = 'EUR' )
)
) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.

Best Practices

1. %cid und %cid_ref konsistent verwenden

" ✅ Eindeutige, sprechende CIDs
%cid = 'ORDER_001'
%cid = 'ITEM_001_001'
%cid = 'ITEM_001_002'
" ❌ Vermeiden: Generische Namen
%cid = 'CID1'
%cid = 'X'

2. Fehlerbehandlung immer implementieren

" ✅ Immer FAILED und REPORTED auswerten
MODIFY ENTITIES OF zi_travel ...
FAILED DATA(failed)
REPORTED DATA(reported).
IF failed-travel IS NOT INITIAL OR failed-booking IS NOT INITIAL.
" Fehlerbehandlung
ENDIF.

3. Validierungen auf allen Ebenen

" In BDEF: Validierungen für Parent UND Children
define behavior for ZI_Travel ...
{
validation validateDates on save { field BeginDate, EndDate; }
}
define behavior for ZI_Booking ...
{
validation validateFlightDate on save { field FlightDate; }
validation validateCarrier on save { field CarrierId; }
}

4. Performance bei großen Datenmengen

" ✅ Bulk-Insert in einer Operation
MODIFY ENTITIES OF zi_travel
ENTITY Travel CREATE BY \_Bookings
WITH VALUE #(
FOR i = 1 UNTIL i > 100
( TravelId = '00000001'
%target = VALUE #(
( %cid = |BOOK_{ i }| CarrierId = 'LH' FlightDate = '20260301' + i )
)
)
).
" ❌ Vermeiden: Loop mit einzelnen Inserts
LOOP AT lt_bookings INTO DATA(ls_booking).
MODIFY ENTITIES OF zi_travel
ENTITY Travel CREATE BY \_Bookings
WITH VALUE #( ( TravelId = '00000001' %target = VALUE #( ( ls_booking ) ) ) ).
ENDLOOP.

Weitere Ressourcen