RAP: Composition e Association — Guida pratica
Una delle scelte di modellazione più importanti quando si costruisce un servizio RAP è capire quando usare una Composition e quando una Association. Sbagliare questa decisione significa combattere contro il framework invece di sfruttarlo. In questa guida pratica costruiamo un servizio completo testata/posizioni — header e items — e vediamo passo per passo come comporre CDS, behavior, service definition e CRUD.
Concetti base: Composition vs Association
Prima di scrivere codice bisogna capire la differenza fondamentale tra i due tipi di relazione.
Composition è una relazione padre → figlio dove il figlio non esiste senza il padre. Il ciclo di vita è condiviso: se cancelli la testata (header), il framework cancella automaticamente le posizioni (items). Il lock del figlio è derivato dal padre. È il pattern classico dei documenti SAP (header/items).
Association è una relazione di semplice riferimento verso un'entità che esiste in modo indipendente. Non influenza il CRUD del root, è tipicamente navigazione read-only verso dati esterni. Esempio classico: il campo "owner" che punta a un Business User del sistema.
Regola pratica: se l'entità figlia ha senso solo nel contesto del padre → Composition. Se è solo un riferimento a qualcosa che esiste per conto suo → Association.
Nel nostro esempio:
ZRAP_SG_HEADER→ZRAP_SG_ITEM: Composition (le posizioni non esistono senza la testata)ZRAP_SG_HEADER→I_BusinessUser: Association (l'utente esiste indipendentemente)
Step 1 — CDS Base Views (ZRAP_SG_I_*)
Le base views leggono direttamente dalla tabella DB. Nessuna logica, nessuna association qui — solo mappatura campi pulita.
ZRAP_SG_I_Header — base view testata
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Header - Base View'
define view entity ZRAP_SG_I_Header
as select from ZRAP_SG_header
{
key header_uuid as HeaderUuid,
header_id as HeaderId,
title as Title,
status as Status,
priority as Priority,
owner as Owner,
start_date as StartDate,
end_date as EndDate,
created_by as CreatedBy,
created_at as CreatedAt,
last_changed_by as LastChangedBy,
last_changed_at as LastChangedAt
}
ZRAP_SG_I_Item — base view posizioni
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Item - Base View'
define view entity ZRAP_SG_I_Item
as select from ZRAP_SG_item
{
key item_uuid as ItemUuid,
header_uuid as HeaderUuid,
item_pos as ItemPos,
item_text as ItemText,
item_status as ItemStatus,
assignee as Assignee,
remark as Remark,
created_by as CreatedBy,
created_at as CreatedAt,
last_changed_by as LastChangedBy,
last_changed_at as LastChangedAt
}
Step 2 — CDS Interface Views: Composition e Association
Questo è il cuore dell'architettura. Le interface views (ZRAP_SG_C_*) dichiarano composition e association. Il framework RAP usa queste dichiarazioni per gestire tutto il comportamento transazionale.
Regole sintattiche fondamentali:
- La composition si dichiara nel padre.
- L'association to parent si dichiara nel figlio — sono sempre in coppia obbligatoria.
- Solo il root usa
define root view entity, il figlio usadefine view entity.
ZRAP_SG_C_Header — interface view testata (ROOT)
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Header - Interface View'
@Metadata.allowExtensions: true
define root view entity ZRAP_SG_C_Header
as projection on ZRAP_SG_I_Header
// COMPOSITION: il padre dichiara i propri figli.
// Il framework gestisce automaticamente lock e draft.
composition [0..*] of ZRAP_SG_C_Item as _Items
// ASSOCIATION: riferimento a entità esterna.
// Owner esiste indipendentemente dalla testata.
association [0..1] to I_BusinessUser as _Owner
on $projection.Owner = _Owner.UserID
{
key HeaderUuid,
HeaderId,
Title,
Status,
Priority,
Owner,
StartDate,
EndDate,
CreatedBy,
CreatedAt,
LastChangedBy,
LastChangedAt,
// Esposizione delle navigazioni
_Items,
_Owner
}
ZRAP_SG_C_Item — interface view posizioni (CHILD)
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Item - Interface View'
@Metadata.allowExtensions: true
define view entity ZRAP_SG_C_Item
as projection on ZRAP_SG_I_Item
// ASSOCIATION TO PARENT: obbligatoria nel figlio.
// Permette la navigazione inversa figlio → padre.
association to parent ZRAP_SG_C_Header as _Header
on $projection.HeaderUuid = _Header.HeaderUuid
// Association ad altra entità esterna (utente assegnato)
association [0..1] to I_BusinessUser as _Assignee
on $projection.Assignee = _Assignee.UserID
{
key ItemUuid,
HeaderUuid,
ItemPos,
ItemText,
ItemStatus,
Assignee,
Remark,
CreatedBy,
CreatedAt,
LastChangedBy,
LastChangedAt,
_Header,
_Assignee
}
Step 3 — Behavior Definition (BDEF)
Il BDEF è il contratto che descrive quali operazioni CRUD sono permesse. In RAP managed il framework implementa automaticamente la persistenza: per un servizio CRUD puro come questo non serve scrivere alcuna logica custom — basta dichiarare le operazioni.
managed;
strict ( 2 );
* ROOT ENTITY: HEADER (testata)
define behavior for ZRAP_SG_C_Header alias Header
persistent table ZRAP_SG_header
lock master
authorization master ( instance )
{
" Operazioni standard CRUD
create;
update;
delete;
" Gestione UUID automatica dal framework
field ( numbering : managed, readonly ) HeaderUuid;
" Campi gestiti dal framework (timestamp/utente)
field ( readonly ) CreatedBy, CreatedAt, LastChangedBy, LastChangedAt;
" HeaderId in sola lettura dopo la creazione
field ( readonly : update ) HeaderId;
" Esposizione navigazione verso le posizioni
association _Items { create; }
}
* CHILD ENTITY: ITEM (posizioni)
define behavior for ZRAP_SG_C_Item alias Item
persistent table ZRAP_SG_item
lock dependent by _Header
authorization dependent by _Header
{
update;
delete;
field ( numbering : managed, readonly ) ItemUuid;
field ( readonly ) HeaderUuid;
field ( readonly ) CreatedBy, CreatedAt, LastChangedBy, LastChangedAt;
" Navigazione verso il padre (obbligatoria per composition)
association _Header;
}
Step 4 — Service Definition e Service Binding
La Service Definition espone root e child; l'associazione _Owner resta accessibile via navigazione OData automaticamente.
@EndUserText.label: 'Service Definition Document'
define service ZRAP_SG_SD_Document {
expose ZRAP_SG_C_Header as Header;
expose ZRAP_SG_C_Item as Item;
expose I_BusinessUser as BusinessUser;
}
Il Service Binding si crea tramite ADT (non da codice):
Binding Type : OData V4 - UI
Service Def : ZRAP_SG_SD_Document
Service Name : ZRAP_SG_SB_DOCUMENT
Version : 0001
Endpoint generato:
/sap/opu/odata4/sap/ZRAP_SG_sb_document/srvd/sap/ZRAP_SG_sd_document/0001/
Step 5 — CRUD in pratica
CREATE — crea testata
POST /sap/opu/odata4/.../Header
Content-Type: application/json
{
"Title" : "Documento di esempio",
"Status" : "OP",
"Priority" : "1",
"Owner" : "USER01",
"StartDate" : "2025-06-01",
"EndDate" : "2025-12-31"
}
CREATE — crea posizione nella testata (navigazione composition)
POST /Header(HeaderUuid=guid'A1B2C3D4-...')/_Items
Content-Type: application/json
{
"ItemPos" : 10,
"ItemText" : "Prima riga di esempio",
"ItemStatus" : "OP",
"Assignee" : "USER02"
}
READ — lettura con $expand (testata + posizioni + owner)
# Legge la testata con expand delle posizioni (composition)
# e dell'owner (association)
GET /Header(HeaderUuid=guid'A1B2C3D4-...')?$expand=_Items,_Owner
# Tutte le testate aperte con priorità alta, con posizioni incluse
GET /Header?$filter=Status eq 'OP' and Priority eq '1'
&$expand=_Items
&$orderby=EndDate asc
UPDATE — modifica testata (solo i campi inviati, con ETag)
PATCH /Header(HeaderUuid=guid'A1B2C3D4-...')
Content-Type: application/json
If-Match: W/"20250617120000.0000000"
{
"Priority" : "2",
"EndDate" : "2025-11-30"
}
DELETE — cancellazione e cascade
# Cancella una singola posizione
DELETE /Item(ItemUuid=guid'E5F6G7H8-...')
If-Match: W/"20250617130000.0000000"
# DELETE sulla TESTATA — cascade automatico: il framework cancella
# PRIMA tutte le posizioni figlie, poi cancella la testata.
# Non serve scrivere codice per questo.
DELETE /Header(HeaderUuid=guid'A1B2C3D4-...')
If-Match: W/"20250617120000.0000000"
# Risposta attesa: 204 No Content
In sintesi
La distinzione tra Composition e Association non è un dettaglio sintattico: definisce chi possiede il ciclo di vita del dato. La Composition lega testata e posizioni in un unico business object transazionale — lock, draft e cascade li gestisce il framework. L'Association resta un riferimento leggero verso dati che vivono per conto loro. Scegliere correttamente fin dal modello CDS significa lasciare che RAP lavori per te, invece di reimplementare a mano ciò che il framework già offre.