Managed RAP Part 2: Numbering, Locking, ETags and Authorization
Add the transactional controls to the Purchase Request object: a technical UUID plus a readable request number, pessimistic locks, optimistic ETag concurrency, and global and instance authorization.
Explanation
📌 KEY POINTS
✅ Locks stop simultaneous edits; ETags stop saving from an outdated screen.
✅ Root = lock master + etag master; children = lock/authorization dependent.
✅ Global authorization checks create; instance authorization checks update/delete/actions.
⚠️ Semantic numbers from a number range belong in an on-save determination.
▶ WHERE WE ARE
In Part 1 you built ZR_PurchReq with CRUD working and no code. It already has three declarations you have not used yet: numbering, lock master, etag master and authorization master. In this part you understand each one and implement what needs code.
━━━━━━━━━━━━━━━━━━━━
▶ 1. NUMBERING — TWO KEYS, TWO JOBS
A good RAP business object usually has two identifiers:
- A technical key (RequestUUID) — never shown to users, never changes, generated by the framework.
- A semantic key (RequestID, e.g. 0000000042) — what users search for and quote in emails.
Numbering options for the technical key:
- Managed early numbering — "numbering : managed" on a UUID field. The framework generates the UUID when the instance is created. This is the simplest and what we use.
- Unmanaged early numbering — "early numbering" in the entity header; you implement an earlynumbering_create method and assign keys yourself during the interaction phase.
- Late numbering — "late numbering"; a temporary key is used until save, and you assign the final key in the saver method adjust_numbers. Use it only when the final number must be gap-free and may only be drawn once the save is certain.
- External numbering — the consumer supplies the key (field readonly : update).
For the semantic RequestID we use a determination on save that draws a number from a number range object ZPR_REQ (create it in ADT as a Number Range Object, interval 01). Drawing the number at save time means a request that is never saved never consumes a number.
━━━━━━━━━━━━━━━━━━━━
▶ 2. LOCKING — PESSIMISTIC CONTROL
"lock master" on the root and "lock dependent by _Request" on the item mean: whenever anyone changes a request or any of its items, the framework locks the whole request. A second user trying to edit the same request gets a lock error instead of silently overwriting.
In managed RAP you write no lock code — the framework locks by the root key. (In unmanaged RAP you implement the lock method yourself; see the Unmanaged series.)
Locks are exclusive and last until the transaction ends. With draft (Part 4), the lock is held for the whole editing session of the draft.
━━━━━━━━━━━━━━━━━━━━
▶ 3. ETAG — OPTIMISTIC CONTROL
Locks protect a single transaction. ETags protect the gap between reading and changing. Typical case: user A opens a request at 10:00, user B changes it at 10:02, user A clicks Save at 10:05. Without an ETag, A's stale screen would overwrite B's change.
"etag master LocalLastChangedAt" tells RAP to send the current LocalLastChangedAt value to the client with every read. On change, the client sends it back (If-Match header in OData); if the value in the database differs, RAP rejects the change. Because LocalLastChangedAt is annotated as localInstanceLastChangedAt, the managed framework updates it on every change automatically — you never set it yourself.
Items have their own etag master so one item can be changed without invalidating the others.
With draft enabled (Part 4) you also add "total etag LastChangedAt" on the root. It protects the active data while a draft is being edited.
━━━━━━━━━━━━━━━━━━━━
▶ 4. AUTHORIZATION — WHO MAY DO WHAT
"authorization master ( global, instance )" generates two methods:
- get_global_authorizations — checks operations that do not depend on a specific instance, most importantly create. Example: is this user allowed to create purchase requests at all?
- get_instance_authorizations — checks operations on an existing instance: update, delete, actions. Example: may this user change request 42?
Items are "authorization dependent by _Request", so an item update is checked against its parent request.
Use AUTHORITY-CHECK against your own authorization object (here ZPR_REQ with field ACTVT). Return if_abap_behv=>auth-allowed or auth-unauthorized per instance in result.
Authorization vs read access vs feature control — three different things:
- Authorization (this method) — the user is not allowed to do it. Example: a requester may not approve.
- Read access — which rows the user may see. That is controlled by a CDS access control (DCL role) on the view entity with @AccessControl.authorizationCheck: #CHECK, not by the BDEF.
- Feature control (Part 3) — the object is not in a state that allows it. Example: an approved request cannot be edited, no matter who you are.
Mixing these up is one of the most common RAP design errors.
━━━━━━━━━━━━━━━━━━━━
▶ STEP BY STEP
- Create number range object ZPR_REQ with interval 01 (e.g. 0000000001–9999999999).
- Create authorization object ZPR_REQ with field ACTVT (activities 01 create, 02 change, 06 delete) and assign it to your test role.
- Add the determination "setRequestID on save { create; }" to the root behavior and let the quick fix create the method.
- Implement setRequestID, get_global_authorizations and get_instance_authorizations as in the code example.
- Test: create a request via EML and commit; RequestID is filled. Remove the create authorization from your role; create must fail with a FAILED entry.
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER PART 2
A business object that assigns readable numbers, prevents parallel edits, detects stale data and enforces who may create, change and delete. Part 3 adds business logic: status workflow, validations and actions.
Code example
// ===== BDEF additions (root entity PurchaseRequest) ===== determination setRequestID on save { create; } " ===== Behavior pool ZBP_R_PURCHREQ - local handler class =====CLASS lhc_PurchaseRequest DEFINITION INHERITING FROM cl_abap_behavior_handler. PRIVATE SECTION. METHODS get_global_authorizations FOR GLOBAL AUTHORIZATION IMPORTING REQUEST requested_authorizations FOR PurchaseRequest RESULT result. METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION IMPORTING keys REQUEST requested_authorizations FOR PurchaseRequest RESULT result. METHODS setRequestID FOR DETERMINE ON SAVE IMPORTING keys FOR PurchaseRequest~setRequestID.ENDCLASS. CLASS lhc_PurchaseRequest IMPLEMENTATION. METHOD get_global_authorizations. " Create does not depend on an instance -> global check IF requested_authorizations-%create = if_abap_behv=>mk-on. AUTHORITY-CHECK OBJECT 'ZPR_REQ' ID 'ACTVT' FIELD '01'. result-%create = COND #( WHEN sy-subrc = 0 THEN if_abap_behv=>auth-allowed ELSE if_abap_behv=>auth-unauthorized ). ENDIF. ENDMETHOD. METHOD get_instance_authorizations. AUTHORITY-CHECK OBJECT 'ZPR_REQ' ID 'ACTVT' FIELD '02'. DATA(update_ok) = xsdbool( sy-subrc = 0 ). AUTHORITY-CHECK OBJECT 'ZPR_REQ' ID 'ACTVT' FIELD '06'. DATA(delete_ok) = xsdbool( sy-subrc = 0 ). " One result line per requested instance result = VALUE #( FOR key IN keys ( %tky = key-%tky %update = COND #( WHEN update_ok = abap_true THEN if_abap_behv=>auth-allowed ELSE if_abap_behv=>auth-unauthorized ) %delete = COND #( WHEN delete_ok = abap_true THEN if_abap_behv=>auth-allowed ELSE if_abap_behv=>auth-unauthorized ) ) ). ENDMETHOD. METHOD setRequestID. " 1. Read only what we need, for all keys at once (bulk) READ ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest FIELDS ( RequestID ) WITH CORRESPONDING #( keys ) RESULT DATA(requests). " 2. Skip instances that already have a number DELETE requests WHERE RequestID IS NOT INITIAL. CHECK requests IS NOT INITIAL. " 3. Draw one block of numbers for all new requests TRY. cl_numberrange_runtime=>number_get( EXPORTING nr_range_nr = '01' object = 'ZPR_REQ' quantity = CONV #( lines( requests ) ) IMPORTING number = DATA(last_number) returned_quantity = DATA(returned_qty) ). CATCH cx_nr_object_not_found cx_number_ranges. RETURN. " real code: log and raise a message in a validation ENDTRY. DATA(next_number) = CONV int8( last_number ) - returned_qty. " 4. Write the numbers back through EML (never direct UPDATE) MODIFY ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest UPDATE FIELDS ( RequestID ) WITH VALUE #( FOR request IN requests INDEX INTO idx ( %tky = request-%tky RequestID = CONV #( next_number + idx ) ) ). ENDMETHOD. ENDCLASS. " IN LOCAL MODE = skip authorization and feature checks, because the BO" itself is acting. Use it only inside the behavior pool." %tky = transactional key (key + draft indicator). Always use it" instead of the plain key so the same code works after draft is added.Real project scenario
Two buyers in the same purchasing team edited the same request from different Fiori sessions. The original design had no ETag, so the buyer who saved last silently overwrote the first buyer's quantity change, and nobody noticed until the supplier shipped the wrong amount.
Adding "etag master LocalLastChangedAt" with the @Semantics.systemDateTime.localInstanceLastChangedAt annotation fixed it without application code: the second save now fails with a "data has changed" message and the user reloads. In the same review the team found that "cannot edit after approval" had been coded in get_instance_authorizations. That made the error look like a missing role to the support desk. It was moved to instance feature control (Part 3), and authorization was left to answer only "is this user allowed".
Common mistakes
- Using the UUID as the number users see — people cannot read or quote it. Keep a separate semantic number.
- Drawing the semantic number in a determination "on modify": draft instances and abandoned creates then burn numbers. Draw it on save.
- Setting LocalLastChangedAt manually — the framework maintains it; manual values break ETag comparison.
- Putting business-state rules ("approved requests cannot be changed") in authorization instead of feature control.
- Forgetting that create is checked in get_global_authorizations, not the instance method.
- Omitting IN LOCAL MODE inside the behavior pool, so the BO's own internal updates are blocked by feature control or authorization.
- Using the plain key instead of %tky, then having to rewrite every method when draft is enabled.
Best practices
- Technical UUID key with numbering : managed, plus a semantic number drawn on save from a number range.
- Always pair lock master with an etag master field that the framework maintains automatically.
- Add total etag on the root as soon as you enable draft.
- Check authorization against your own authorization object; keep read restrictions in a CDS DCL role.
- Keep authorization (who) and feature control (state) strictly separate.
- Process keys as a set: one READ ENTITIES, one number block, one MODIFY ENTITIES.
Interview angle
Common questions: "Explain early vs late numbering", "What is the difference between lock and ETag?", "Global vs instance authorization?", "What is %tky?"
Strong answers: locks are pessimistic and protect a running transaction, while ETags are optimistic and detect that data changed between read and write. Early numbering assigns keys in the interaction phase; late numbering assigns them in adjust_numbers during save, used for gap-free numbers. Global authorization covers create and other instance-independent operations; instance authorization covers update, delete and actions per instance. Mention that read restrictions belong in a CDS DCL, and that state-dependent restrictions belong in feature control, not authorization. Explaining why %tky should be used everywhere (it includes %is_draft) signals practical experience.
Practice this lesson
Quick check
1. Which declaration lets the managed RAP framework generate a UUID key automatically when an instance is created?
2. User B saves a change to a request. Later, user A saves from a screen opened before B's change. Which RAP mechanism rejects A's change?
3. In managed RAP, where are create and update changes held during the interaction phase, before the user saves?
Interview questions
1. Two users open the same purchase request. User B saves a change first; user A saves later from a stale screen. Which RAP mechanisms prevent A from overwriting B, and how do locks and ETags differ?
Answer it in your own words first, then compare.
2. Design the numbering for a purchase request business object: users need a readable, sequential request number, but drafts are often abandoned. Which RAP numbering options exist and which would you choose?
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.