Managed RAP Part 1: Tables, CDS Data Model and Your First Behavior Definition
Build the foundation of a managed RAP business object step by step: persistence tables, a root/child CDS data model, and a managed behavior definition — and understand exactly what the framework does for you.
What this complete series covers
Build one complete Purchase Request business object step by step — tables, CDS model, behavior definition, numbering, locking, authorization, workflow actions, draft and a projection layer — with working code in every part.
- Who it is for
- Best for SAP consultants, developers, and learners who want a practical understanding of Managed RAP.
- Prerequisites
- Basic ABAP and ADT (Eclipse). Basic CDS view syntax helps. No prior RAP knowledge needed — Part 1 starts from the tables.
- Final outcome
- You will be able to design and build a production-shaped managed RAP business object, explain every BDEF line, write handlers that work in bulk and with draft, and test the object with EML. Continue with the Service Bindings series to expose it as a Fiori app.
- Part 1: Managed RAP Part 1: Tables, CDS Data Model and Your First Behavior Definition
Build the foundation of a managed RAP business object step by step: persistence tables, a root/child CDS data model, and a managed behavior definition — and understand exactly what the framework does for you.
- Part 2: 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.
- Part 3: Managed RAP Part 3: Determinations, Validations, Actions and Feature Control
Give the Purchase Request real business behavior: default and calculated values with determinations, error checks with validations, a Submit/Approve/Reject workflow with actions, and state-based feature control.
- Part 4: 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.
- Part 5: Managed RAP Part 5: Projection Layer, Testing, Performance and Clean Core
Take the Purchase Request object to production: build the projection layer that services consume, write ABAP Unit tests with EML, avoid the common performance traps in handlers, and keep the design clean-core compliant.
System and release assumptions: Examples target recent SAP S/4HANA / ABAP Cloud capabilities. RAP, CDS, OData, and Fiori Elements syntax and tooling can differ by ABAP platform release.
Explanation
📌 KEY POINTS
✅ A managed BO needs: tables → CDS root/child views → behavior definition → behavior pool.
✅ Use UUID keys with numbering : managed; add a readable RequestID for users.
✅ Composition + association to parent connect header and items.
⚠️ Admin fields only update automatically with the correct @Semantics annotations.
▶ WHAT YOU WILL BUILD IN THIS SERIES
Across these five parts you build one complete business object: a Purchase Request with a header and items. Each part adds one layer to the same object, so keep the objects you create:
- Part 1 — tables, CDS data model, first behavior definition (CRUD works with zero code)
- Part 2 — numbering, locking, ETag concurrency and authorization
- Part 3 — determinations, validations, actions and feature control (Submit / Approve / Reject)
- Part 4 — composition (header + items), create-by-association and draft
- Part 5 — projection layer, testing, performance and clean-core rules
After Part 5, continue with the Service Bindings series to expose the same object as a Fiori Elements app.
━━━━━━━━━━━━━━━━━━━━
▶ WHERE MANAGED RAP SITS
The RESTful ABAP Programming Model (RAP) builds a transactional app in layers:
- Database tables — where the data is stored.
- CDS data model (view entities) — the business shape of the data: a root entity, child entities and their relationships.
- Behavior definition (BDEF) — what may happen to the data: create, update, delete, actions, validations, locks.
- Behavior implementation (an ABAP class called the behavior pool) — code for the non-standard parts.
- Projection, service definition and service binding — what is exposed to a UI or an API.
"Managed" is a decision about layers 3 and 4. It means the RAP framework itself implements the standard transactional work. You write code only for business-specific logic.
━━━━━━━━━━━━━━━━━━━━
▶ WHAT THE FRAMEWORK OWNS VS WHAT YOU OWN
The framework owns: standard create/update/delete; the transactional buffer (changes are held in memory until save); locking; ETag comparison; filling administrative fields (created by/at, changed by/at); writing to the database and committing; the draft tables if draft is enabled.
You own: the data model (tables, keys, CDS); the behavior definition; the numbering strategy if you do not use UUIDs; determinations, validations and actions; authorization and feature control; messages.
━━━━━━━━━━━━━━━━━━━━
▶ THE TWO PHASES OF EVERY RAP REQUEST
Interaction phase: the consumer sends create, update or action requests. RAP runs the standard operations and calls your handler methods. All changes land in the transactional buffer — nothing is written to the database yet.
Save sequence: when the consumer commits (a user clicks Save, or code calls COMMIT ENTITIES), RAP runs on-save determinations, then validations, then writes to the database and commits. If a validation fails, nothing is written.
This is why a managed behavior class must never contain COMMIT WORK, ROLLBACK WORK or direct INSERT/UPDATE statements on its own tables: the framework owns the transaction (the LUW).
━━━━━━━━━━━━━━━━━━━━
▶ STEP 1 — CREATE THE PERSISTENCE TABLES
In ADT create two database tables: ZPR_HEAD (request header) and ZPR_ITEM (request items). Use UUID keys of type sysuuid_x16 — managed RAP can generate them. Add the administrative fields using the standard data elements shown in the code example (abp_creation_user, abp_creation_tstmpl, abp_locinst_lastchange_user, abp_locinst_lastchange_tstmpl, abp_lastchange_tstmpl). ZPR_ITEM carries parent_uuid, the foreign key to its header.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 2 — CREATE THE CDS DATA MODEL
ZR_PurchReq is the root view entity ("define root view entity"). It declares "composition [0..*] of ZR_PurchReqItem as _Item": items belong to the request and cannot exist without it. ZR_PurchReqItem declares "association to parent ZR_PurchReq as _Request". Rename columns to readable CamelCase names — these become the business object field names and later the OData property names.
The @Semantics annotations on the admin fields tell RAP which field means "created by", "last changed at" and so on. Managed RAP fills them automatically on every change.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 3 — CREATE THE BEHAVIOR DEFINITION
Right-click ZR_PurchReq, choose New Behavior Definition, implementation type Managed. Read each line:
- managed implementation in class zbp_r_purchreq unique — the behavior pool that will hold your code.
- strict ( 2 ) — the strictest syntax checks. Always use it for new development.
- persistent table zpr_head — tells the framework where to write.
- lock master / lock dependent by _Request — the root is locked, items are locked through the root.
- authorization master ( global, instance ) — authorization is checked on the root and inherited by items. Strict mode requires it to be declared; you implement it in Part 2.
- etag master LocalLastChangedAt — optimistic concurrency (Part 2).
- create; update; delete; and association _Item { create; } — the allowed operations. Items can only be created through their parent.
- field ( readonly, numbering : managed ) RequestUUID — RAP generates the UUID key on create.
- mapping for zpr_head — maps CDS names (RequestUUID) to table columns (request_uuid). Without it the framework cannot write CamelCase fields into snake_case columns.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 4 — GENERATE THE BEHAVIOR POOL
Use the ADT quick fix (Ctrl+1) on the class name to generate ZBP_R_PURCHREQ. It contains a local handler class (lhc_…) with empty authorization methods. For Part 1 you write no further code: create, update and delete already work.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 5 — TEST WITHOUT A UI
Create a class that implements if_oo_adt_classrun and run the EML snippet at the end of the code example with F9. EML (Entity Manipulation Language) is how ABAP code talks to a RAP business object. Outside the business object you call COMMIT ENTITIES to trigger the save sequence; inside handler methods you never do.
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER PART 1
A working business object that creates, changes and deletes purchase requests and items, with keys and admin fields maintained automatically — and zero lines of CRUD code. Part 2 adds a readable request number, locking behaviour, concurrency control and authorization.
Code example
// ===== 1. Table ZPR_HEAD (header) =====@EndUserText.label : 'Purchase request header'@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENT@AbapCatalog.deliveryClass : #A@AbapCatalog.dataMaintenance : #RESTRICTEDdefine table zpr_head { key client : abap.clnt not null; key request_uuid : sysuuid_x16 not null; request_id : abap.numc(10); description : abap.char(80); requester : syuname; status : abap.char(1); @Semantics.amount.currencyCode : 'zpr_head.currency_code' total_amount : abap.curr(15,2); currency_code : abap.cuky; created_by : abp_creation_user; created_at : abp_creation_tstmpl; last_changed_by : abp_locinst_lastchange_user; local_last_changed_at : abp_locinst_lastchange_tstmpl; last_changed_at : abp_lastchange_tstmpl;} // ===== 2. Table ZPR_ITEM (items) =====define table zpr_item { key client : abap.clnt not null; key item_uuid : sysuuid_x16 not null; parent_uuid : sysuuid_x16; item_no : abap.numc(4); item_text : abap.char(60); @Semantics.quantity.unitOfMeasure : 'zpr_item.unit' quantity : abap.quan(13,3); unit : abap.unit(3); @Semantics.amount.currencyCode : 'zpr_item.currency_code' price : abap.curr(15,2); currency_code : abap.cuky; local_last_changed_at : abp_locinst_lastchange_tstmpl;} // ===== 3. Root view entity ZR_PurchReq =====@AccessControl.authorizationCheck: #NOT_REQUIRED // tutorial only - use #CHECK + a DCL role in real projects@EndUserText.label: 'Purchase Request'define root view entity ZR_PurchReq as select from zpr_head composition [0..*] of ZR_PurchReqItem as _Item{ key request_uuid as RequestUUID, request_id as RequestID, description as Description, requester as Requester, status as Status, @Semantics.amount.currencyCode: 'CurrencyCode' total_amount as TotalAmount, currency_code as CurrencyCode, @Semantics.user.createdBy: true created_by as CreatedBy, @Semantics.systemDateTime.createdAt: true created_at as CreatedAt, @Semantics.user.localInstanceLastChangedBy: true last_changed_by as LastChangedBy, @Semantics.systemDateTime.localInstanceLastChangedAt: true local_last_changed_at as LocalLastChangedAt, @Semantics.systemDateTime.lastChangedAt: true last_changed_at as LastChangedAt, _Item} // ===== 4. Child view entity ZR_PurchReqItem =====@AccessControl.authorizationCheck: #NOT_REQUIRED@EndUserText.label: 'Purchase Request Item'define view entity ZR_PurchReqItem as select from zpr_item association to parent ZR_PurchReq as _Request on $projection.RequestUUID = _Request.RequestUUID{ key item_uuid as ItemUUID, parent_uuid as RequestUUID, item_no as ItemNo, item_text as ItemText, @Semantics.quantity.unitOfMeasure: 'Unit' quantity as Quantity, unit as Unit, @Semantics.amount.currencyCode: 'CurrencyCode' price as Price, currency_code as CurrencyCode, @Semantics.systemDateTime.localInstanceLastChangedAt: true local_last_changed_at as LocalLastChangedAt, _Request} // ===== 5. Behavior definition ZR_PurchReq =====managed implementation in class zbp_r_purchreq unique;strict ( 2 ); define behavior for ZR_PurchReq alias PurchaseRequestpersistent table zpr_headlock masterauthorization master ( global, instance )etag master LocalLastChangedAt{ create; update; delete; association _Item { create; } field ( readonly, numbering : managed ) RequestUUID; field ( readonly ) RequestID, Status, TotalAmount, CreatedBy, CreatedAt, LastChangedBy, LocalLastChangedAt, LastChangedAt; mapping for zpr_head { RequestUUID = request_uuid; RequestID = request_id; Description = description; Requester = requester; Status = status; TotalAmount = total_amount; CurrencyCode = currency_code; CreatedBy = created_by; CreatedAt = created_at; LastChangedBy = last_changed_by; LocalLastChangedAt = local_last_changed_at; LastChangedAt = last_changed_at; }} define behavior for ZR_PurchReqItem alias Itempersistent table zpr_itemlock dependent by _Requestauthorization dependent by _Requestetag master LocalLastChangedAt{ update; delete; association _Request; field ( readonly, numbering : managed ) ItemUUID; field ( readonly ) RequestUUID, LocalLastChangedAt; mapping for zpr_item { ItemUUID = item_uuid; RequestUUID = parent_uuid; ItemNo = item_no; ItemText = item_text; Quantity = quantity; Unit = unit; Price = price; CurrencyCode = currency_code; LocalLastChangedAt = local_last_changed_at; }} " ===== 6. Test it with EML (class implementing if_oo_adt_classrun, run with F9) =====MODIFY ENTITIES OF zr_purchreq ENTITY PurchaseRequest CREATE FIELDS ( Description CurrencyCode ) WITH VALUE #( ( %cid = 'REQ1' Description = 'Laptops for new joiners' CurrencyCode = 'INR' ) ) MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported). COMMIT ENTITIES. " allowed here (outside the BO) - never inside a handler methodout->write( mapped-purchaserequest ).Real project scenario
A team migrating a custom requisition transaction to RAP built an "unmanaged-style" managed BO: every handler method issued its own INSERT or UPDATE on the custom table, and one method called COMMIT WORK. Users saw half-saved requests when a later validation failed, and created/changed timestamps were inconsistent because some code paths forgot to set them.
The data mapped cleanly to two custom tables, so the fix was to let the framework do its job: declare persistent table and mapping in the BDEF, annotate the admin fields with @Semantics, delete the hand-written CRUD and the COMMIT WORK, and keep only real business logic (numbering, validations, the approval action). About 60% of the behavior class disappeared, and the half-save problem stopped because nothing reached the database until every validation passed.
Common mistakes
- Writing INSERT/UPDATE/MODIFY statements on the persistent table inside handler methods — the framework already does this at save time, and doing it yourself bypasses the buffer and validations.
- Calling COMMIT WORK or ROLLBACK WORK in a behavior class. The framework owns the LUW; a manual commit can persist half-validated data.
- Forgetting the mapping block when CDS names differ from column names, then wondering why fields are not saved.
- Declaring the child entity as a normal association instead of "composition" / "association to parent" — then it is not part of the business object and cannot share locks, ETags or draft.
- Omitting the @Semantics annotations on admin fields, so CreatedBy/LastChangedAt stay empty.
- Leaving strict ( 2 ) out, which hides syntax problems that surface later during upgrades.
Best practices
- Start every new table-based business object as managed; move to unmanaged or unmanaged save only for a proven persistence reason (see the Unmanaged RAP series).
- Use UUID technical keys with numbering : managed, plus a separate human-readable number (Part 2).
- Always use strict ( 2 ) and the standard abp_* data elements for administrative fields.
- Name layers consistently: ZR_ for the base business object views, ZC_ for projections (Part 5), ZBP_ for behavior pools.
- Keep persistence tables free of UI-only fields; UI concerns belong in the projection layer.
- Test the BO with EML in a console class before building any UI — it isolates backend problems from UI problems.
Interview angle
Typical questions: "What is the difference between managed and unmanaged RAP?", "What does the framework do for you in managed RAP?", "Why can't you use COMMIT WORK in a behavior implementation?"
A strong answer: in managed RAP the framework implements standard CRUD, the transactional buffer, locking, ETag handling, admin fields and the save sequence; the developer declares the contract in the BDEF and implements only business logic. Explain the two phases (interaction phase into the buffer, save sequence to the database) and that the framework owns the LUW, so a manual commit would break atomicity. Senior candidates add when they would not choose managed: when an existing API must own persistence, which leads to unmanaged or managed with unmanaged save.
Practice this lesson
Quick check
1. In managed RAP, where are create and update changes held during the interaction phase, before the user saves?
2. What is the purpose of the "mapping for zpr_head" block in a managed behavior definition?
3. Which declaration lets the managed RAP framework generate a UUID key automatically when an instance is created?
Interview questions
1. Walk me through the layers of a RAP application, from the database table to the Fiori app. What is the job of each layer?
Answer it in your own words first, then compare.
2. In managed RAP, what exactly does the framework do for you, and why must a behavior implementation never contain COMMIT WORK or direct INSERT/UPDATE statements on its own tables?
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.