RAP Build-Along Part 3: Behavior Definition — Managed, Draft, Locking and ETags
Part 3 of the RAP Bootcamp turns the Travel + Booking data model into a transactional business object: a managed, draft-enabled behavior definition with persistent and draft table mapping, CRUD and create-by-association, lock and authorization master/dependent, local and total ETag, managed UUID numbering and field control.
Explanation
📌 KEY POINTS
✅ Managed BDEF header: implementation class, strict ( 2 ), with draft.
✅ Root: lock master, total etag, etag master, authorization master.
✅ Declare field control (readonly, mandatory) and numbering (managed UUID).
⚠️ Draft needs draft tables and draft actions (Edit, Activate, Discard, Resume, Prepare).
▶ SAP RAP BEHAVIOR DEFINITION: MANAGED RAP, DRAFT, LOCKING AND ETAGS (PART 3)
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU WILL BUILD
The behavior definition (BDEF) ZI_RAP_Travel, the generated draft tables ZRAP_TRAV_D and ZRAP_BOOK_D, and an (initially empty) behavior pool ZBP_I_RAP_TRAVEL. After this part the BO can be created, changed, and deleted through EML, with drafts, locks and optimistic concurrency handled by the managed framework. Custom logic (determinations, validations, actions) is only *declared* here; Part 4 implements it.
━━━━━━━━━━━━━━━━━━━━
▶ PREREQUISITES / PREVIOUS PART
- [Part 2](/lessons/rap-bootcamp-part-2-persistence-cds) completed: active tables
ZRAP_TRAVEL,ZRAP_BOOKING; rootZI_RAP_Travelwith composition toZI_RAP_Booking. - Your release supports strict mode and draft for managed BOs.
strict ( 2 )is available in current SAP BTP ABAP environment and recent S/4HANA releases; older releases may only supportstrictor no strict mode (release-dependent).
━━━━━━━━━━━━━━━━━━━━
▶ WHY A BEHAVIOR DEFINITION EXISTS
CDS describes *data*. The BDEF describes *what may happen to it*: which operations exist, who owns locks, how concurrency is detected, where data is saved, and which custom rules run. In a managed scenario, the framework implements the standard operations (create, update, delete, read, lock, draft handling, save) from this declaration. You write ABAP only for behavior that is genuinely business-specific.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 1 — CREATE THE BDEF
Right-click ZI_RAP_Travel → *New Behavior Definition*, implementation type Managed. ADT generates a skeleton for the root and every composed child. Replace it with the full BDEF in the code section. Walk through it block by block.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 2 — HEADER: MANAGED, STRICT, DRAFT, IMPLEMENTATION CLASS
┏━━ CODE ━━━━━━━━━━━━
┃ managed implementation in class zbp_i_rap_travel unique;
┃ strict ( 2 );
┃ with draft;
┗━━━━━━━━━━━━━━━━━━━
managed— framework handles standard operations and save.implementation in class ... unique— one behavior pool for the BO; each entity gets its own local handler class inside it.strict ( 2 )— enforces current best-practice rules (for example, explicit authorization, no deprecated syntax). It reports issues that would otherwise surface at runtime.with draft— draft-enables the whole BO. All composed children become draft-enabled with it.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 3 — PERSISTENCE AND DRAFT TABLE MAPPING
┏━━ CODE ━━━━━━━━━━━━
┃ persistent table zrap_travel
┃ draft table zrap_trav_d
┗━━━━━━━━━━━━━━━━━━━
The persistent table is where active data is saved. The draft table holds edit-in-progress data. Put the cursor on zrap_trav_d, press Ctrl+1, choose *Create draft table*, and activate. Do the same for zrap_book_d. The generated table uses CDS alias names and includes "%admin" : include sych_bdl_draft_admin_inc.
Because CDS aliases (TravelUUID) differ from DB field names (travel_uuid), add a mapping for block per entity. Without mapping, the managed save cannot move fields between CDS and DB. In newer releases mapping for zrap_travel corresponding plus exceptions is supported; the explicit list below works on all releases that support draft (release-dependent syntax).
━━━━━━━━━━━━━━━━━━━━
▶ STEP 4 — LOCKING: LOCK MASTER AND LOCK DEPENDENT
- Root:
lock master total etag LastChangedAt— the root owns the lock. Editing any Booking locks the whole Travel. - Child:
lock dependent by _Travel— the child borrows its parent's lock through the parent association.
With draft, an *exclusive draft* is taken when a user presses Edit; the enqueue lock on the active instance is held for the draft's lifetime until activation or discard. A second user sees "locked by user X".
━━━━━━━━━━━━━━━━━━━━
▶ STEP 5 — AUTHORIZATION: MASTER AND DEPENDENT
- Root:
authorization master ( instance, global )— you will implement instance and global authorization methods in Part 4. - Child:
authorization dependent by _Travel— the Booking follows its Travel's authorization.
strict ( 2 ) requires this declaration. DCL (@AccessControl.authorizationCheck: #CHECK plus an access-control object) restricts reading. It does not authorize create, update, delete or actions. Modification authorization is always RAP behavior authorization.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 6 — ETAG: LOCAL AND TOTAL
| ETag | Declared on | Field | Purpose |
|---|---|---|---|
| Total ETag | Root, with draft | LastChangedAt | Detects changes to the active instance while a draft exists; checked when the draft is resumed/activated |
| Local ETag | Each entity | LocalLastChangedAt | Optimistic concurrency per instance for OData requests (If-Match) |
Root: etag master LocalLastChangedAt. Child: etag master LocalLastChangedAt as well (each Booking row has its own local timestamp from Part 2). Alternatively, a child can use etag dependent by _Travel. Because the @Semantics annotations from Part 2 are present, the managed framework updates these timestamps on every save.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 7 — NUMBERING: UUID VS SEMANTIC
┏━━ CODE ━━━━━━━━━━━━
┃ field ( numbering : managed, readonly ) TravelUUID;
┗━━━━━━━━━━━━━━━━━━━
Managed numbering means the framework generates a UUID on create. Consumers never supply a key.
| Strategy | Declaration | When |
|---|---|---|
| Managed UUID | numbering : managed on UUID key | Default for new managed BOs |
| Early numbering | early numbering + earlynumbering_create handler | Semantic key needed at create time |
| Late numbering | late numbering + adjust_numbers in saver | Gap-free numbers assigned at save |
The readable TravelID is not the key, so we fill it in a determination (Part 4) — simple and clean. For gap-free legal numbers, late numbering with a number range is the right pattern.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 8 — FIELD CONTROL
┏━━ CODE ━━━━━━━━━━━━
┃ field ( readonly ) TravelID, TotalPrice, OverallStatus, CreatedBy, CreatedAt,
┃ LastChangedBy, LastChangedAt, LocalLastChangedAt;
┃ field ( mandatory ) CustomerID, BeginDate, EndDate, CurrencyCode;
┃ field ( readonly : update ) AgencyID;
┃ field ( features : instance ) BookingFee;
┗━━━━━━━━━━━━━━━━━━━
readonly— computed or framework-owned.mandatory— UI marks as required; it does not replace a validation (Part 4 still validates dates).readonly : update— may be set on create, not afterwards.features : instance— dynamic: decided per instance in Part 4 (for example,BookingFeeread-only once accepted).
━━━━━━━━━━━━━━━━━━━━
▶ STEP 9 — OPERATIONS, ASSOCIATIONS, AND CUSTOM BEHAVIOR DECLARATIONS
Root: create; update; delete; association _Booking { create; with draft; }. The with draft on the association lets bookings be created inside an open Travel draft.
Child: update; delete; association _Travel { with draft; }. No standalone create — Bookings are created only through their parent (create-by-association).
Declared custom behavior (implemented in Part 4):
- Determinations:
setInitialStatus on modify { create; },calculateTravelID on save { create; },recalcTotalPrice on modify { field BookingFee, CurrencyCode; }, on childcalcTotalOnBooking on modify { field FlightPrice, CurrencyCode; create; }. - Validations:
validateDates on save { create; field BeginDate, EndDate; },validateCustomer, on childvalidateFlightPrice. - Actions:
action ( features : instance ) acceptTravel result [1] $self;andrejectTravel. determine action Prepare { validation validateDates; validation validateCustomer; validation Booking~validateFlightPrice; }.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 10 — DRAFT ACTIONS AND THE DRAFT LIFECYCLE
┏━━ CODE ━━━━━━━━━━━━
┃ draft action Edit;
┃ draft action Activate optimized;
┃ draft action Discard;
┃ draft action Resume;
┃ draft determine action Prepare;
┗━━━━━━━━━━━━━━━━━━━
Lifecycle:
┏━━ CODE ━━━━━━━━━━━━
┃ Active ──Edit──> Draft (exclusive, locked) ──Prepare──> validated
┃ ^ │ └─Discard──> (draft deleted, lock released)
┃ └────Activate───────┘ (Prepare runs, then draft → active, save sequence)
┃ Resume: re-acquire lock for an expired/closed draft session
┗━━━━━━━━━━━━━━━━━━━
Activate optimized (release-dependent, newer releases) reduces redundant determination runs. On older releases use draft action Activate;. Validations run on Prepare and on Activate; draft data may be *invalid* while the user edits — that is intended.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 11 — CREATE THE BEHAVIOR POOL
Put the cursor on zbp_i_rap_travel → Ctrl+1 → *Create behavior implementation class*. ADT generates local handler classes lhc_Travel and lhc_Booking with method stubs for every declared determination, validation, action, feature and authorization method. Activate with empty methods; the BO now works for plain CRUD.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 12 — SMOKE TEST WITH EML
Run the small classrun in the code section. It creates a Travel with one Booking via create-by-association and commits. Part 6 builds full tests; this proves the BDEF, mapping and draft tables are consistent.
━━━━━━━━━━━━━━━━━━━━
▶ CHECKPOINT
- [ ] BDEF active with
managed,strict ( 2 )(or your release's level),with draft. - [ ] Draft tables
ZRAP_TRAV_D/ZRAP_BOOK_Dgenerated and active. - [ ] Mapping blocks present for both entities.
- [ ] Root is lock and authorization master; child is dependent.
- [ ] Total ETag
LastChangedAt, local ETagLocalLastChangedAt. - [ ]
TravelUUID/BookingUUIDuse managed numbering. - [ ] Behavior pool
ZBP_I_RAP_TRAVELgenerated and active. - [ ] Smoke test creates Travel + Booking and they appear in Data Preview.
━━━━━━━━━━━━━━━━━━━━
▶ TRY IT YOURSELF
Add a cancelBooking action on the Booking entity with result [1] $self, add it to the child handler stub, and reactivate. Then decide whether it should be features : instance (hint: a cancelled booking cannot be cancelled again).
━━━━━━━━━━━━━━━━━━━━
▶ INTERVIEW TAKEAWAY
"A managed BDEF declares the transactional contract: persistent and draft tables with field mapping, the root as lock and authorization master with children dependent, a total ETag for draft plus local ETags per instance, managed UUID numbering, static and instance field control, create-by-association for children, and the draft actions Edit, Activate, Discard, Resume and Prepare. The framework implements all of that; my behavior pool only contains business rules."
━━━━━━━━━━━━━━━━━━━━
▶ NEXT PART
[Part 4: Determinations, Validations, Actions, Messages and Authorization](/lessons/rap-bootcamp-part-4-behavior-implementation) implements the declared rules.
Code example
" ============================================================" Behavior definition ZI_RAP_Travel (strict ( 2 ) is release-dependent)" ============================================================managed implementation in class zbp_i_rap_travel unique;strict ( 2 );with draft; define behavior for ZI_RAP_Travel alias Travelpersistent table zrap_traveldraft table zrap_trav_dlock mastertotal etag LastChangedAtauthorization master ( instance, global )etag master LocalLastChangedAt{ create ( authorization : global ); update; delete; association _Booking { create ( features : instance ); with draft; } field ( numbering : managed, readonly ) TravelUUID; field ( readonly ) TravelID, TotalPrice, OverallStatus, CreatedBy, CreatedAt, LastChangedBy, LastChangedAt, LocalLastChangedAt; field ( mandatory ) CustomerID, BeginDate, EndDate, CurrencyCode; field ( readonly : update ) AgencyID; field ( features : instance ) BookingFee; action ( features : instance ) acceptTravel result [1] $self; action ( features : instance ) rejectTravel result [1] $self; determination setInitialStatus on modify { create; } determination recalcTotalPrice on modify { field BookingFee, CurrencyCode; } determination calculateTravelID on save { create; } validation validateDates on save { create; field BeginDate, EndDate; } validation validateCustomer on save { create; field CustomerID; } internal action reCalcTotalPrice; draft action Edit; draft action Activate optimized; " older releases: draft action Activate; draft action Discard; draft action Resume; draft determine action Prepare { validation validateDates; validation validateCustomer; validation Booking~validateFlightPrice; } mapping for zrap_travel { TravelUUID = travel_uuid; TravelID = travel_id; AgencyID = agency_id; CustomerID = customer_id; BeginDate = begin_date; EndDate = end_date; BookingFee = booking_fee; TotalPrice = total_price; CurrencyCode = currency_code; Description = description; OverallStatus = overall_status; CreatedBy = created_by; CreatedAt = created_at; LastChangedBy = last_changed_by; LocalLastChangedAt = local_last_changed_at; LastChangedAt = last_changed_at; }} define behavior for ZI_RAP_Booking alias Bookingpersistent table zrap_bookingdraft table zrap_book_dlock dependent by _Travelauthorization dependent by _Traveletag master LocalLastChangedAt{ update; delete; association _Travel { with draft; } field ( numbering : managed, readonly ) BookingUUID; field ( readonly ) ParentUUID, BookingID, LocalLastChangedAt; field ( mandatory ) CarrierID, ConnectionID, FlightDate, CurrencyCode; determination setBookingDefaults on modify { create; } determination calcTotalOnBooking on modify { create; field FlightPrice, CurrencyCode; } validation validateFlightPrice on save { create; field FlightPrice, CurrencyCode; } mapping for zrap_booking { BookingUUID = booking_uuid; ParentUUID = parent_uuid; BookingID = booking_id; BookingDate = booking_date; CustomerID = customer_id; CarrierID = carrier_id; ConnectionID = connection_id; FlightDate = flight_date; FlightPrice = flight_price; CurrencyCode = currency_code; BookingStatus = booking_status; LocalLastChangedAt = local_last_changed_at; }} " ============================================================" Generated draft table (via quick fix) - shape for reference" ============================================================" @EndUserText.label : 'Draft table for ZI_RAP_Travel'" @AbapCatalog.tableCategory : #TRANSPARENT" @AbapCatalog.deliveryClass : #A" define table zrap_trav_d {" key mandt : mandt not null;" key traveluuid : sysuuid_x16 not null;" travelid : abap.numc(8);" ... one field per CDS element, named after the CDS alias ..." "%admin" : include sych_bdl_draft_admin_inc;" } " ============================================================" Smoke test: create Travel + Booking by association (EML)" ============================================================CLASS zcl_rap_travel_smoke DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. INTERFACES if_oo_adt_classrun.ENDCLASS. CLASS zcl_rap_travel_smoke IMPLEMENTATION. METHOD if_oo_adt_classrun~main. MODIFY ENTITIES OF zi_rap_travel ENTITY Travel CREATE FIELDS ( AgencyID CustomerID BeginDate EndDate BookingFee CurrencyCode Description ) WITH VALUE #( ( %cid = 'T1' AgencyID = '070001' CustomerID = '000010' BeginDate = cl_abap_context_info=>get_system_date( ) + 10 EndDate = cl_abap_context_info=>get_system_date( ) + 15 BookingFee = '25.00' CurrencyCode = 'EUR' Description = 'Smoke test travel' ) ) CREATE BY \_Booking FIELDS ( CarrierID ConnectionID FlightDate FlightPrice CurrencyCode ) WITH VALUE #( ( %cid_ref = 'T1' %target = VALUE #( ( %cid = 'B1' CarrierID = 'LH' ConnectionID = '0400' FlightDate = cl_abap_context_info=>get_system_date( ) + 10 FlightPrice = '300.00' CurrencyCode = 'EUR' ) ) ) ) MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported). IF failed IS NOT INITIAL. out->write( 'Create failed - inspect REPORTED' ). ROLLBACK ENTITIES. RETURN. ENDIF. " Outer transaction boundary: this classrun owns the LUW COMMIT ENTITIES RESPONSE OF zi_rap_travel FAILED DATA(commit_failed) REPORTED DATA(commit_reported). out->write( COND #( WHEN commit_failed IS INITIAL THEN |Created travel { mapped-travel[ 1 ]-TravelUUID }| ELSE 'Save failed (validation?)' ) ). ENDMETHOD.ENDCLASS.Real project scenario
A support team received tickets that two dispatchers could edit the same travel's bookings simultaneously in a custom app, with the last save silently winning. The BO had declared the child as its own lock master and had no local ETag. Changing the child to lock dependent by _Travel, adding etag master LocalLastChangedAt with the managed timestamp annotation, and relying on the root's total etag for drafts turned silent overwrites into a clear "locked by user" or "data changed" message.
Common mistakes
- Forgetting the
mapping forblock when CDS aliases differ from DB fields — saves write nothing or fail. - Hand-writing the draft table instead of generating it; any alias drift breaks activation.
- Declaring
createon the child instead of creating only by association. - Treating
field ( mandatory )as a validation — it is a UI/consumer hint; validate on save anyway. - Omitting
total etagin a draft BO; draft resume cannot detect concurrent active changes. - Believing DCL covers create/update/delete; modification needs
authorization master. - Using
Activate optimizedon a release that does not support it — check ADT syntax help.
Best practices
- Enable the strictest strict mode your release supports.
- Root owns lock, authorization and total ETag; children are dependent.
- Use managed UUID numbering; fill readable IDs in a determination or late numbering.
- Keep static field control in the BDEF, dynamic control in
features : instance. - Generate draft tables and behavior pool from quick fixes.
- Declare every validation that must run before activation in
Prepare. - Smoke-test with EML before building any UI.
Interview angle
Typical questions: "Difference between lock master and lock dependent?" "What are total and local ETag used for in draft?" "Managed vs early vs late numbering?" "What does with draft on an association do?" Answer with the lifecycle: Edit acquires exclusive draft and lock; Prepare runs validations; Activate copies draft to active through the save sequence; Discard releases the lock.
Practice this lesson
Quick check
1. Which declaration makes a field read-only only after the instance is created?
2. Two users open the same travel; the second sees "locked by" another user. Which BDEF feature causes this?
3. A travel's overall status must never be changed directly by users. Which BDEF declaration fits?
Interview questions
1. Users report "Locked by another user" and lost draft changes in a RAP app. How do you analyze it?
Answer it in your own words first, then compare.
2. Explain RAP feature control for operations and fields, and how to troubleshoot it.
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.
More lessons in this topic
- RAP Basics Part 1: RAP Architecture — Layers, Artifacts and When RAP Fits
- RAP Basics Part 10: EML — Entity Manipulation Language
- RAP Basics Part 11: Draft, Locking and Authorization
- RAP Basics Part 2: Data Model Design — Tables, Keys, Administrative Fields and Interface Views
- RAP Basics Part 3: Projection Layer and Metadata Extensions
- RAP Basics Part 4: Implementation Types — Managed, Unmanaged, Additional Save and Unmanaged Save
- RAP Basics Part 5: Root Entity, Child Entity and Composition
- RAP Basics Part 6: Behavior Definition — CRUD, Field Control and Features