Managed RAP Part 4: Compositions, Create-by-Association and Draft
Understand the composition tree behind a RAP business object, create items through their parent with create-by-association and %cid ref, and enable draft so users can edit requests over a longer session safely.
Explanation
📌 KEY POINTS
✅ Items are created only via the parent: association _Item { create; }.
✅ Draft needs: with draft, draft tables, total etag, draft actions, "with draft" on associations.
✅ Always use %tky so code works for draft and active instances.
⚠️ Regenerate draft tables after adding fields.
▶ WHERE WE ARE
The Purchase Request now has numbering, locking, authorization and a workflow. In this part you look closely at the header-item relationship you created in Part 1, learn how to create items correctly, and enable draft — the feature that makes a RAP app behave like a real Fiori application.
━━━━━━━━━━━━━━━━━━━━
▶ 1. THE COMPOSITION TREE
A RAP business object is not a single table; it is a tree of entities. The root (PurchaseRequest) owns its children (Item). This ownership is declared twice and both declarations are required:
- In the root CDS view: "composition [0..*] of ZR_PurchReqItem as _Item".
- In the child CDS view: "association to parent ZR_PurchReq as _Request on ...".
What composition gives you:
- Lifecycle — deleting a request deletes its items automatically.
- Locking — "lock dependent by _Request": editing an item locks the whole request.
- Authorization — "authorization dependent by _Request": item operations are checked on the parent.
- Draft — the whole tree is drafted and activated together.
- One transaction — header and items are saved in one LUW, all or nothing.
Only model a composition when the child truly belongs to the parent. A request item belongs to its request; a supplier does not belong to a request — that is a normal association (a reference to another business object).
Deeper trees are allowed (root → item → schedule line). Every level uses association to parent, and lock/authorization dependency points to the root through the chain.
━━━━━━━━━━━━━━━━━━━━
▶ 2. CREATE-BY-ASSOCIATION
Items have no standalone "create" in the BDEF. They can only be created through the parent: "association _Item { create; }". This guarantees every item gets a valid parent key.
In EML this is CREATE BY \_Item. When you create a header and its items in the same request, the header has no key yet (the UUID is assigned by the framework). You link them with content IDs:
- %cid — a temporary ID you give the new header, e.g. 'REQ1'.
- %cid_ref — used on the items to say "my parent is the instance with %cid REQ1".
- %target — the list of child instances to create.
After the call, MAPPED returns the real keys for each %cid. Fiori Elements does the same thing when a user adds items on the object page.
To add an item to an existing request, use %tky of the request instead of %cid_ref.
━━━━━━━━━━━━━━━━━━━━
▶ 3. WHY DRAFT
Without draft, every change a user makes in the UI is sent and saved immediately, validations run on each save, and a half-finished request cannot be left overnight. With draft:
- Edits go to draft tables, not to the active tables. The user can leave and resume later, even on another device.
- Validations and determinations can run while editing, so messages appear early.
- The active version stays unchanged and visible to others until the user clicks Save (activation).
- An exclusive lock protects the active instance while someone has a draft of it.
Draft is the standard for Fiori Elements transactional apps. Web APIs usually do not use it.
━━━━━━━━━━━━━━━━━━━━
▶ 4. HOW TO ENABLE DRAFT — STEP BY STEP
- Add "with draft;" after strict ( 2 ) in the BDEF.
- Add "draft table zpr_head_d" to the root and "draft table zpr_item_d" to the item. Use the ADT quick fix (Ctrl+1) on each name to generate the draft tables; they contain the CDS field names plus the "%admin" include with draft administrative data.
- Add "total etag LastChangedAt" to the root. The total ETag protects the active data during draft editing: when the draft is activated, RAP checks that nobody changed the active instance meanwhile.
- Add the draft actions to the root: Edit (create a draft from an active instance), Activate (copy draft to active and save), Discard (throw the draft away), Resume (continue an expired draft) and Prepare (run chosen validations and determinations before activation).
- Add "with draft" to both association declarations: "association _Item { create; with draft; }" and "association _Request { with draft; }".
- In "draft determine action Prepare" list the validations that should run when the user clicks Save, including item validations (Item~validateQuantity).
━━━━━━━━━━━━━━━━━━━━
▶ 5. WHAT CHANGES IN YOUR CODE
Nothing, if you followed Parts 2 and 3: all methods used %tky, which includes %is_draft, and IN LOCAL MODE reads. The same handler code now works on draft and active instances. This is why %tky matters from day one.
From outside the BO, EML defaults to active instances. To work on a draft you set %is_draft = if_abap_behv=>mk-on.
━━━━━━━━━━━━━━━━━━━━
▶ 6. THE DRAFT LIFECYCLE IN ONE PICTURE
Create → new draft (only in draft table) → user edits → Prepare runs validations → Activate → active instance saved, draft deleted.
Existing request: Edit → draft copy + exclusive lock → user edits → Activate (or Discard) → lock released.
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER PART 4
A complete header-item business object with draft — ready for Fiori Elements. Part 5 adds the projection layer that the UI service will use, plus testing and performance guidance for production.
Code example
// ===== Behavior definition ZR_PurchReq with draft (full) =====managed implementation in class zbp_r_purchreq unique;strict ( 2 );with draft; define behavior for ZR_PurchReq alias PurchaseRequestpersistent table zpr_headdraft table zpr_head_dlock mastertotal etag LastChangedAtauthorization master ( global, instance )etag master LocalLastChangedAt{ create; update ( features : instance ); delete ( features : instance ); association _Item { create; with draft; } field ( readonly, numbering : managed ) RequestUUID; field ( readonly ) RequestID, Status, TotalAmount, Requester, CreatedBy, CreatedAt, LastChangedBy, LocalLastChangedAt, LastChangedAt; field ( mandatory ) Description, CurrencyCode; determination setInitialStatus on modify { create; } determination setRequestID on save { create; } validation validateDescription on save { create; field Description; } action ( features : instance ) submit result [1] $self; action ( features : instance ) approve result [1] $self; action ( features : instance ) reject result [1] $self; draft action Edit; draft action Activate optimized; // "optimized" on current releases; plain Activate on older ones draft action Discard; draft action Resume; draft determine action Prepare { validation validateDescription; validation Item~validateQuantity; } mapping for zpr_head { /* unchanged from Part 1 */ }} define behavior for ZR_PurchReqItem alias Itempersistent table zpr_itemdraft table zpr_item_dlock dependent by _Requestauthorization dependent by _Requestetag master LocalLastChangedAt{ update; delete; association _Request { with draft; } field ( readonly, numbering : managed ) ItemUUID; field ( readonly ) RequestUUID, LocalLastChangedAt; field ( mandatory ) Quantity, Unit; determination calculateTotal on modify { create; field Quantity, Price; } validation validateQuantity on save { create; field Quantity; } mapping for zpr_item { /* unchanged from Part 1 */ }} " ===== EML 1: create header + two items in one call =====MODIFY ENTITIES OF zr_purchreq ENTITY PurchaseRequest CREATE FIELDS ( Description CurrencyCode ) WITH VALUE #( ( %cid = 'REQ1' Description = 'Onboarding kit' CurrencyCode = 'INR' ) ) CREATE BY \_Item FIELDS ( ItemNo ItemText Quantity Unit Price CurrencyCode ) WITH VALUE #( ( %cid_ref = 'REQ1' " parent = the header created above %target = VALUE #( ( %cid = 'I1' ItemNo = '0010' ItemText = 'Laptop' Quantity = 5 Unit = 'ST' Price = '65000.00' CurrencyCode = 'INR' ) ( %cid = 'I2' ItemNo = '0020' ItemText = 'Headset' Quantity = 5 Unit = 'ST' Price = '2500.00' CurrencyCode = 'INR' ) ) ) ) MAPPED DATA(mapped) " mapped-purchaserequest / mapped-item: %cid -> real keys FAILED DATA(failed) REPORTED DATA(reported).COMMIT ENTITIES. " ===== EML 2: add an item to an EXISTING request =====MODIFY ENTITIES OF zr_purchreq ENTITY PurchaseRequest CREATE BY \_Item FIELDS ( ItemNo ItemText Quantity Unit Price CurrencyCode ) WITH VALUE #( ( RequestUUID = existing_uuid " key of the existing header %target = VALUE #( ( %cid = 'I3' ItemNo = '0030' ItemText = 'Docking station' Quantity = 5 Unit = 'ST' Price = '9000.00' CurrencyCode = 'INR' ) ) ) ) FAILED DATA(failed2) REPORTED DATA(reported2).COMMIT ENTITIES. " ===== EML 3: work on a draft from outside the BO =====MODIFY ENTITIES OF zr_purchreq ENTITY PurchaseRequest EXECUTE Edit FROM VALUE #( ( RequestUUID = existing_uuid %param-preserve_changes = abap_true ) ) MAPPED DATA(mapped3) FAILED DATA(failed3) REPORTED DATA(reported3)." ... then UPDATE with %is_draft = if_abap_behv=>mk-on, then EXECUTE ActivateCOMMIT ENTITIES.Real project scenario
A service-request app was first built without draft. Users entering long requests with 30+ items lost work whenever their session timed out, and validations fired on every field change because each change was saved immediately. Some users also opened the same request twice and overwrote their own changes.
Enabling draft solved all three: edits were kept in draft tables and could be resumed the next morning, validations moved into Prepare and ran when the user clicked Save, and the exclusive draft lock plus total ETag stopped conflicting edits. Because the original handlers already used %tky and IN LOCAL MODE, no handler code changed. The only rework was one determination that had used the plain key — it failed on drafts until switched to %tky.
Common mistakes
- Modeling a reference (e.g. supplier, material) as a composition. Composition means ownership and shared lifecycle; references are plain associations.
- Declaring composition in the root but forgetting "association to parent" in the child, or the reverse.
- Giving the child its own "create" instead of creating it via the parent association.
- Mixing up %cid (ID of the instance being created) and %cid_ref (reference to another instance created in the same request).
- Enabling draft but forgetting "with draft" on the associations or the total etag on the root.
- Writing directly to draft tables — they belong to the framework.
- Using the plain key instead of %tky in handlers, which breaks as soon as draft is on.
- Not listing child validations in Prepare, so item errors only appear at the very end.
Best practices
- Use composition only for data with the same lifecycle; everything else is an association.
- Always create children via create-by-association and link new instances with %cid / %cid_ref.
- Plan for draft from the start: use %tky and IN LOCAL MODE in every method.
- Generate draft tables with the ADT quick fix and regenerate after adding fields to the CDS view.
- Add total etag on the root whenever draft is enabled.
- Put all validations that the user should see before saving into draft determine action Prepare.
- Use draft for Fiori UI services; keep Web API services non-draft (see Service Bindings Part 4).
Interview angle
Interviewers ask: "What is a composition in RAP and how is it different from an association?", "How do you create child entities in EML?", "Explain %cid and %cid_ref", "How does draft work and what is the total ETag for?", "What are the draft actions?"
Strong answer: a composition defines ownership, so lifecycle, locking, authorization, draft and transaction are shared across the tree; children are created only via create-by-association. %cid identifies a new instance within one request and %cid_ref lets another operation refer to it before the real key exists; MAPPED returns the final keys. Draft stores edits in draft tables, holds an exclusive lock on the active instance, and the total ETag ensures the active data did not change before activation. Name Edit, Activate, Discard, Resume and Prepare with one sentence each.
Practice this lesson
Quick check
1. In one EML MODIFY you create a new request header and its items. How do the items reference the header that has no key yet?
2. After enabling draft, which root-entity declaration ensures that the active instance was not changed by someone else before a draft is activated?
3. In managed RAP, where are create and update changes held during the interaction phase, before the user saves?
Interview questions
1. What is the difference between a composition and an association in a RAP business object, and how do you create a header with items in one EML call?
Answer it in your own words first, then compare.
2. After enabling draft on a working managed RAP object, one determination fails and the object page behaves oddly. What do you check?
Answer it in your own words first, then compare.
Reference-verified: the steps are checked against official SAP documentation and tutorials, and object and field names in the sample code are checked for consistency. The code has not been compiled or run on an SAP system by ERPClimb. Syntax marked release-dependent varies by SAP release, so check it in your own system.