RAP Deep Insert/Update - Sauvegarder des entites imbriquees

Catégorie
RAP
Publié
Auteur
Johannes

Deep Insert et Deep Update permettent de sauvegarder des entites Parent et Child en une seule requete. Cela est essentiel pour les Business Objects complexes avec des relations de composition, comme une commande avec ses lignes ou un voyage avec ses reservations.

Le probleme : Plusieurs requetes

Sans les operations Deep, vous devez creer le Parent et les Children separement :

" ❌ Sans Deep Insert : Plusieurs requetes necessaires
" 1. Creer le Parent
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. Attendre le TravelId genere
DATA(lv_travel_id) = mapped_travel-travel[ %cid = 'TRAVEL_1' ]-TravelId.
" 3. Creer les Children separement
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 allers-retours, pas de securite transactionnelle sur les deux etapes

La solution : Deep Insert

Avec Deep Insert, vous creez le Parent et les Children en une seule operation :

" ✅ Deep Insert : Une requete pour tout
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 = 'Voyage d''affaires avec vols"
) )
" Creer les Children via l'association
ENTITY Travel CREATE BY \_Bookings
FIELDS ( CarrierId FlightDate FlightPrice CurrencyCode )
WITH VALUE #( (
%cid_ref = 'TRAVEL_1' " Reference au %cid du Parent
%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).
" Recuperer toutes les cles generees :
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.

Relations Composition dans les CDS Views

Les operations Deep fonctionnent uniquement avec des relations Composition. Celles-ci definissent une relation Parent-Child forte avec dependance du cycle de vie.

Entite Root (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 -- Exposer l'association
}

Entite Child

@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 -- Association Parent
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 -- Exposer l'association Parent
}

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; } -- Activer Deep Create!
}
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;
}

Important : association _Bookings { create; } active le Deep Insert pour la Composition.

Composition vs. Association

AspectCompositionAssociation
RelationDependance forte (partie-tout)Reference lache
Cycle de vieLe Child est supprime avec le ParentIndependant
Operations DeepSupporteNon supporte
Locklock dependent byLock propre
Syntaxe CDScomposition [0..*] ofassociation [0..*] to
ExempleCommande → LignesCommande → Client

Deep Update avec %control

Avec Deep Update, vous mettez a jour le Parent et les Children ensemble. La structure %control controle quels champs sont reellement mis a jour.

Deep Update simple

" Deep Update : Modifier le Parent et les Children ensemble
MODIFY ENTITIES OF zi_travel
" Mettre a jour le Parent
ENTITY Travel
UPDATE FIELDS ( Description Status )
WITH VALUE #( (
TravelId = '00000001"
Description = 'Description mise a jour"
Status = 'A"
) )
" Mettre a jour les Children
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 avec structure %control

La structure %control indique explicitement quels champs doivent etre mis a jour :

" Controle precis avec %control
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE
WITH VALUE #( (
TravelId = '00000001"
Description = 'Nouvelle description"
Status = 'A"
TotalPrice = '1500.00' " Ne sera PAS mis a jour (voir ci-dessous)
" Seuls ces champs seront modifies :
%control = VALUE #(
Description = if_abap_behv=>mk-on
Status = if_abap_behv=>mk-on
" TotalPrice = if_abap_behv=>mk-off " implicite
)
) )
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES.
" → TotalPrice reste inchange, meme si une valeur a ete passee

UPDATE SET FIELDS vs. UPDATE FIELDS

" UPDATE FIELDS : Liste de champs explicite
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE FIELDS ( Description Status ) -- Seulement ces champs
WITH VALUE #( ( TravelId = '00000001' Description = 'Texte' Status = 'A' ) ).
" UPDATE SET FIELDS : Automatiquement tous les champs non-initiaux
DATA(ls_update) = VALUE zi_travel(
TravelId = '00000001"
Description = 'Texte"
Status = 'A"
" BeginDate est initial → ne sera PAS mis a jour
).
MODIFY ENTITIES OF zi_travel
ENTITY Travel
UPDATE SET FIELDS
WITH VALUE #( ( CORRESPONDING #( ls_update ) ) ).

Deep Update avec creation de nouveaux Children

" Modifier un Parent existant ET ajouter de nouveaux Children
MODIFY ENTITIES OF zi_travel
" Mettre a jour le Parent
ENTITY Travel
UPDATE FIELDS ( Status )
WITH VALUE #( ( TravelId = '00000001' Status = 'P' ) )
" AJOUTER de nouvelles reservations au Travel existant
ENTITY Travel CREATE BY \_Bookings
FIELDS ( CarrierId FlightDate FlightPrice CurrencyCode )
WITH VALUE #( (
TravelId = '00000001' " Parent existant
%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.
" Nouveau BookingId :
DATA(lv_new_booking) = mapped-booking[ %cid = 'NEW_BOOK_1' ]-BookingId.

Scenarios d’erreur et Rollback

Validation lors du 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.
" Lire les donnees
READ ENTITIES OF zi_travel IN LOCAL MODE
ENTITY Booking
FIELDS ( TravelId BookingId FlightDate )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_bookings).
" Charger les donnees du Parent pour la validation
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 doit etre dans la periode du voyage
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 = |La date de vol doit etre entre { ls_travel-BeginDate } et { ls_travel-EndDate }|
)
%element-FlightDate = if_abap_behv=>mk-on
) TO reported-booking.
ENDIF.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

Gestion des erreurs lors des operations Deep

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' ) " Hors periode!
)
) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
" Verifier si les Children ont echoue
IF failed-booking IS NOT INITIAL.
WRITE: / 'Erreurs dans les reservations :'.
LOOP AT reported-booking INTO DATA(ls_book_msg).
WRITE: / ls_book_msg-%msg->if_message~get_text( ).
ENDLOOP.
ENDIF.
" Verifier si le Parent a echoue
IF failed-travel IS NOT INITIAL.
WRITE: / 'Erreur dans Travel :'.
LOOP AT reported-travel INTO DATA(ls_travel_msg).
WRITE: / ls_travel_msg-%msg->if_message~get_text( ).
ENDLOOP.
ENDIF.
" COMMIT seulement en cas de succes
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: / 'Sauvegarde reussie'.
ELSE.
WRITE: / 'Commit echoue - Transaction annulee'.
ENDIF.
ELSE.
WRITE: / 'Validation echouee - pas de commit'.
ENDIF.

Rollback automatique

Pour les operations Deep : Tout ou rien. Si une partie de l’operation echoue, toute la transaction est annulee.

" Scenario : Travel OK, mais une reservation invalide
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' ) " Erreur de validation!
)
) )
MAPPED DATA(mapped)
FAILED DATA(failed)
REPORTED DATA(reported).
COMMIT ENTITIES
RESPONSE OF zi_travel
FAILED DATA(commit_failed).
" En cas d'erreur : RIEN n'a ete sauvegarde!
" → Travel T1 n'a PAS ete cree
" → Booking B1 n'a PAS ete cree
" → Booking B2 n'a PAS ete cree
" Toute la transaction est annulee.

Deep Insert multi-niveaux

Les operations Deep fonctionnent aussi sur plusieurs niveaux :

" Travel → Booking → BookingSupplement (3 niveaux)
MODIFY ENTITIES OF zi_travel
" Niveau 1 : Travel
ENTITY Travel
CREATE FIELDS ( AgencyId CustomerId BeginDate EndDate )
WITH VALUE #( ( %cid = 'T1' AgencyId = '000001' CustomerId = '000042"
BeginDate = '20260301' EndDate = '20260315' ) )
" Niveau 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' )
)
) )
" Niveau 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.

Bonnes pratiques

1. Utiliser %cid et %cid_ref de maniere coherente

" ✅ CIDs uniques et explicites
%cid = 'ORDER_001"
%cid = 'ITEM_001_001"
%cid = 'ITEM_001_002"
" ❌ A eviter : Noms generiques
%cid = 'CID1"
%cid = 'X"

2. Toujours implementer la gestion des erreurs

" ✅ Toujours evaluer FAILED et REPORTED
MODIFY ENTITIES OF zi_travel ...
FAILED DATA(failed)
REPORTED DATA(reported).
IF failed-travel IS NOT INITIAL OR failed-booking IS NOT INITIAL.
" Gestion des erreurs
ENDIF.

3. Validations a tous les niveaux

" Dans BDEF : Validations pour Parent ET 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 pour les gros volumes

" ✅ Bulk-Insert en une seule 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 )
)
)
).
" ❌ A eviter : Boucle avec des inserts individuels
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.

Ressources supplementaires