RAP Build-Along Part 2: Persistence Tables, Root and Child CDS Views
Part 2 of the RAP Bootcamp builds the data foundation of the Travel + Booking app: active and draft tables, UUID keys with readable business IDs, audit fields, and the CDS root and child view entities connected by composition and parent association — with a working activation order and a checkpoint.
Explanation
📌 KEY POINTS
✅ Persistence tables need client, UUID key, business fields and admin fields.
✅ Root view: "define root view entity"; child: "association to parent".
✅ Composition from root to child; parent key in the child.
⚠️ Annotate admin fields (@Semantics) so managed RAP maintains them.
▶ SAP RAP CDS TUTORIAL: PERSISTENCE TABLES, ROOT AND CHILD CDS VIEWS (PART 2)
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU WILL BUILD
By the end of this part, your package contains:
| Artifact | Name | Purpose |
|---|---|---|
| Active table (root) | ZRAP_TRAVEL | Persisted Travel rows |
| Active table (child) | ZRAP_BOOKING | Persisted Booking rows |
| Draft table (root) | ZRAP_TRAV_D | Draft Travel rows (generated in Part 3) |
| Draft table (child) | ZRAP_BOOK_D | Draft Booking rows (generated in Part 3) |
| Root CDS view entity | ZI_RAP_Travel | Business-object root of the data model |
| Child CDS view entity | ZI_RAP_Booking | Composed child with parent association |
No behavior yet. The goal is a data model that previews correctly in the ADT Data Preview and is ready to carry a managed behavior definition in Part 3.
━━━━━━━━━━━━━━━━━━━━
▶ PREREQUISITES / PREVIOUS PART
- Completed [Part 1](/lessons/rap-basics-architecture-cds-behavior-service): you understand the artifact chain and have a transportable package (for example
ZRAP_TRAVEL_APP) with a transport request. - ADT (Eclipse) connected to an ABAP system that supports RAP managed scenarios with draft. SAP BTP ABAP environment, SAP S/4HANA Cloud Public Edition developer extensibility, or a recent SAP S/4HANA (on-premise/private) release all work, but available syntax differs by release — this lesson labels those spots.
- Authorization to create dictionary objects and CDS in your package.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 1 — DECIDE THE DATA MODEL BEFORE TYPING
A RAP business object is a tree. Here the tree is:
┏━━ CODE ━━━━━━━━━━━━
┃ Travel (root) ZRAP_TRAVEL -> ZI_RAP_Travel
┃ └── Booking (child) ZRAP_BOOKING -> ZI_RAP_Booking
┗━━━━━━━━━━━━━━━━━━━
Decisions made now are expensive to change later:
- Technical key: a 16-byte UUID (
sysuuid_x16) for both entities. UUIDs let the managed framework assign keys at create time without number-range objects and make draft handling simple. - Business ID: a readable
travel_id(for example00000042) shown to users. It is *not* the key; Part 3 and Part 4 show how to fill it. - Parent foreign key:
ZRAP_BOOKING-parent_uuidstores the Travel UUID. The child's own key isbooking_uuid. - Admin fields: created by/at, last changed by/at, and a *local* last-changed timestamp. These feed ETag and audit in Part 3.
- Amounts: every amount has a currency field next to it.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 2 — CREATE THE ACTIVE TABLES IN ADT
Order: root table first, then child table, because the child's structure refers to the parent's key conceptually (no DDIC foreign key is required for RAP).
In ADT: right-click the package → *New* → *Other ABAP Repository Object* → *Dictionary* → *Database Table*. Name ZRAP_TRAVEL, description "RAP Bootcamp Travel". Replace the template with the definition in the code section and activate. Repeat for ZRAP_BOOKING.
Notes on the definitions:
client : abap.clntis the first key field in on-premise and private cloud systems. In ABAP Cloud, client handling is still present in tables you create.@AbapCatalog.deliveryClass : #Amarks application data.@Semantics.amount.currencyCodeon the amount field links the amount to its currency field. Without it, activation reports an error forabap.currfields.local_last_changed_atchanges on *every* save of that row;last_changed_atis the total (root-level) timestamp. Part 3 uses them for local and total ETag.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 3 — PLAN THE DRAFT TABLES (DO NOT HAND-WRITE THEM YET)
Draft-enabled RAP BOs need one draft table per entity. The draft table mirrors the CDS entity's field names (not the DB table's) and adds the technical include sych_bdl_draft_admin_inc ("%admin"). The easiest correct path is to let ADT generate it:
- Write the behavior definition in Part 3 with
with draft;anddraft table zrap_trav_d. - Place the cursor on the unknown table name and use the quick fix "Create draft table".
Doing it that way guarantees field names and types match the CDS aliases. If you hand-write the draft table now and later rename a CDS alias, activation of the BDEF fails with a field-mismatch error. We reserve the names ZRAP_TRAV_D and ZRAP_BOOK_D now so the whole series is consistent.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 4 — CREATE THE ROOT CDS VIEW ENTITY
Right-click the package → *New* → *Data Definition*. Name ZI_RAP_Travel, referenced object ZRAP_TRAVEL, template *Define Root View Entity*.
Key points:
define root view entitymarks this as the root of the BO. Only one root per BO.- Aliases in CamelCase (
TravelUUID,TravelID) become the RAP field names used in BDL, EML and the UI. Choose them carefully; they flow through every later part. composition [0..*] of ZI_RAP_Booking as _Bookingdeclares ownership: bookings live and die with their Travel. A composition cannot activate until the target entity exists and declares the matching parent association — see the activation sequence below.@Semantics.user.createdBy,@Semantics.systemDateTime.createdAt,...localInstanceLastChangedAt,...lastChangedAttell the managed framework which fields it should fill automatically. With these annotations you write no code for admin fields.@Semantics.amount.currencyCode: 'CurrencyCode'onTotalPrice(release-dependent: in older releases the reference was set through the underlying table only; in current releases the CDS annotation is expected for view entities).
━━━━━━━━━━━━━━━━━━━━
▶ STEP 5 — CREATE THE CHILD CDS VIEW ENTITY
New *Data Definition* ZI_RAP_Booking, referenced object ZRAP_BOOKING, template *Define View Entity*.
association to parent ZI_RAP_Travel as _Travel on $projection.ParentUUID = _Travel.TravelUUID— the ON condition joins the child's foreign key to the parent's key. It must use$projectionfields (the aliased names).- The child is not a root, so it uses
define view entity, notdefine root view entity. - Keep the same semantic annotations for amounts and the local ETag field.
━━━━━━━━━━━━━━━━━━━━
▶ CARDINALITY AND ON CONDITIONS
| Declaration | Where | Meaning |
|---|---|---|
| composition [0..*] of ZI_RAP_Booking as _Booking | Root | A Travel owns zero or more Bookings |
| association to parent ZI_RAP_Travel as _Travel on ... | Child | Each Booking has exactly one parent; cardinality is implicit [1..1] |
| association [0..1] to I_Currency as _Currency | Either | Optional lookup association (released API) — *not* part of the BO tree |
Compositions define the BO structure and drive create-by-association, cascade delete, and lock/ETag inheritance. Plain associations are only navigation. Do not model the parent relationship as a normal association: the BDEF in Part 3 will reject it.
━━━━━━━━━━━━━━━━━━━━
▶ STEP 6 — ACTIVATION SEQUENCE (THE PART THAT TRIPS EVERYONE)
The root references the child through the composition, and the child references the root through the parent association. Neither activates alone. Use one of these approaches:
- Mass activation (recommended): create both data definitions (they can be inactive), then press *Activate All* (Ctrl+Shift+F3) and select both. ADT resolves the cycle.
- Two-pass: activate
ZI_RAP_Travelwithout the composition line, activateZI_RAP_Bookingwith its parent association, then add the composition back to the root and activate again.
Full order for this part:
ZRAP_TRAVEL(table) → activateZRAP_BOOKING(table) → activateZI_RAP_Travel+ZI_RAP_Booking→ mass-activate together- Optional: run the data generator class from the code section to insert test rows
- Data Preview (F8) on
ZI_RAP_Travel→ follow_Booking
━━━━━━━━━━━━━━━━━━━━
▶ STEP 7 — SEED TEST DATA
Managed RAP will create rows later through the BO, but you want something to look at now. The small class ZCL_RAP_TRAVEL_DATA_GEN (implements if_oo_adt_classrun) deletes and inserts a few Travel and Booking rows into your own Z tables — never standard SAP tables. Run it with F9 in ADT. In production-bound code you would never insert directly; this is a developer-only seeding utility kept in the development package.
━━━━━━━━━━━━━━━━━━━━
▶ TROUBLESHOOTING
| Symptom | Likely cause | Fix |
|---|---|---|
| "Reference field for CURR field missing" | Amount without currency link | Add @Semantics.amount.currencyCode in table and CDS |
| "Composition target has no association to parent" | Child missing association to parent | Add it with correct ON condition |
| Cycle activation error | Activating root alone | Mass-activate both entities |
| Data Preview shows no bookings | parent_uuid not filled in test data | Fix generator so bookings carry the Travel UUID |
| Alias rename breaks later | Draft table mirrors old alias | Regenerate draft table after renames (Part 3) |
| sysuuid_x16 unknown in ABAP Cloud | Wrong type spelling | Use sysuuid_x16 (data element) — released in ABAP Cloud |
━━━━━━━━━━━━━━━━━━━━
▶ CHECKPOINT
You are ready for Part 3 when:
- [ ]
ZRAP_TRAVELandZRAP_BOOKINGare active with UUID keys, business ID, admin and ETag fields, and linked currency. - [ ]
ZI_RAP_Travelis a root view entity withcomposition [0..*]toZI_RAP_Booking. - [ ]
ZI_RAP_Bookinghasassociation to parentwith an ON condition onParentUUID. - [ ] Data Preview on the root navigates to bookings.
- [ ] You know the names
ZRAP_TRAV_D/ZRAP_BOOK_Dwill be generated from the BDEF.
━━━━━━━━━━━━━━━━━━━━
▶ TRY IT YOURSELF
Add a Description field (abap.char(256)) to the Travel table and CDS, and an optional association _Agency (or any released master-data view available in your release) with [0..1] cardinality. Reactivate, then confirm in Data Preview that the association is navigable but does not appear as a composition.
━━━━━━━━━━━━━━━━━━━━
▶ INTERVIEW TAKEAWAY
"In RAP I start from the tree: a root CDS view entity with a composition to its children, each child with an association to parent. I use UUID technical keys plus a separate business ID, annotate admin and ETag timestamps with @Semantics so the managed framework fills them, link every amount to a currency, and let ADT generate draft tables from the behavior definition so field names stay in sync."
━━━━━━━━━━━━━━━━━━━━
▶ NEXT PART
[Part 3: Managed Behavior, Draft, Locking, ETag and Numbering](/lessons/rap-bootcamp-part-3-managed-behavior-draft) turns this data model into a transactional business object.
Code example
-- ============================================================-- 1) Root table ZRAP_TRAVEL-- ============================================================@EndUserText.label : 'RAP Bootcamp Travel'@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENT@AbapCatalog.deliveryClass : #A@AbapCatalog.dataMaintenance : #RESTRICTEDdefine table zrap_travel { key client : abap.clnt not null; key travel_uuid : sysuuid_x16 not null; travel_id : abap.numc(8); agency_id : abap.numc(6); customer_id : abap.numc(6); begin_date : abap.dats; end_date : abap.dats; @Semantics.amount.currencyCode : 'zrap_travel.currency_code' booking_fee : abap.curr(16,2); @Semantics.amount.currencyCode : 'zrap_travel.currency_code' total_price : abap.curr(16,2); currency_code : abap.cuky; description : abap.char(256); overall_status : abap.char(1); -- O open, A accepted, X rejected 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) Child table ZRAP_BOOKING-- ============================================================@EndUserText.label : 'RAP Bootcamp Booking'@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENT@AbapCatalog.deliveryClass : #A@AbapCatalog.dataMaintenance : #RESTRICTEDdefine table zrap_booking { key client : abap.clnt not null; key booking_uuid : sysuuid_x16 not null; parent_uuid : sysuuid_x16 not null; booking_id : abap.numc(4); booking_date : abap.dats; customer_id : abap.numc(6); carrier_id : abap.char(3); connection_id : abap.numc(4); flight_date : abap.dats; @Semantics.amount.currencyCode : 'zrap_booking.currency_code' flight_price : abap.curr(16,2); currency_code : abap.cuky; booking_status : abap.char(1); -- N new, B booked, X cancelled local_last_changed_at : abp_locinst_lastchange_tstmpl;} -- ============================================================-- 3) Root CDS view entity ZI_RAP_Travel-- ============================================================@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Travel - RAP Bootcamp root'define root view entity ZI_RAP_Travel as select from zrap_travel composition [0..*] of ZI_RAP_Booking as _Booking association [0..1] to I_Currency as _Currency on $projection.CurrencyCode = _Currency.Currency{ key travel_uuid as TravelUUID, travel_id as TravelID, agency_id as AgencyID, customer_id as CustomerID, begin_date as BeginDate, end_date as EndDate, @Semantics.amount.currencyCode: 'CurrencyCode' booking_fee as BookingFee, @Semantics.amount.currencyCode: 'CurrencyCode' total_price as TotalPrice, currency_code as CurrencyCode, description as Description, overall_status as OverallStatus, @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, /* associations */ _Booking, _Currency} -- ============================================================-- 4) Child CDS view entity ZI_RAP_Booking-- ============================================================@AccessControl.authorizationCheck: #CHECK@EndUserText.label: 'Booking - RAP Bootcamp child'define view entity ZI_RAP_Booking as select from zrap_booking association to parent ZI_RAP_Travel as _Travel on $projection.ParentUUID = _Travel.TravelUUID association [0..1] to I_Currency as _Currency on $projection.CurrencyCode = _Currency.Currency{ key booking_uuid as BookingUUID, parent_uuid as ParentUUID, booking_id as BookingID, booking_date as BookingDate, customer_id as CustomerID, carrier_id as CarrierID, connection_id as ConnectionID, flight_date as FlightDate, @Semantics.amount.currencyCode: 'CurrencyCode' flight_price as FlightPrice, currency_code as CurrencyCode, booking_status as BookingStatus, @Semantics.systemDateTime.localInstanceLastChangedAt: true local_last_changed_at as LocalLastChangedAt, /* associations */ _Travel, _Currency} " ============================================================" 5) Developer-only test data generator (own Z tables only)" ============================================================CLASS zcl_rap_travel_data_gen DEFINITION PUBLIC FINAL CREATE PUBLIC. PUBLIC SECTION. INTERFACES if_oo_adt_classrun.ENDCLASS. CLASS zcl_rap_travel_data_gen IMPLEMENTATION. METHOD if_oo_adt_classrun~main. DATA travels TYPE STANDARD TABLE OF zrap_travel. DATA bookings TYPE STANDARD TABLE OF zrap_booking. DELETE FROM zrap_booking. DELETE FROM zrap_travel. GET TIME STAMP FIELD DATA(now). DO 3 TIMES. DATA(travel_uuid) = cl_system_uuid=>create_uuid_x16_static( ). APPEND VALUE #( travel_uuid = travel_uuid travel_id = sy-index agency_id = '070001' customer_id = '000010' begin_date = cl_abap_context_info=>get_system_date( ) + 30 end_date = cl_abap_context_info=>get_system_date( ) + 37 booking_fee = '20.00' currency_code = 'EUR' overall_status = 'O' description = |Bootcamp trip { sy-index }| created_by = cl_abap_context_info=>get_user_technical_name( ) created_at = now last_changed_at = now local_last_changed_at = now ) TO travels. DO 2 TIMES. APPEND VALUE #( booking_uuid = cl_system_uuid=>create_uuid_x16_static( ) parent_uuid = travel_uuid booking_id = sy-index booking_date = cl_abap_context_info=>get_system_date( ) customer_id = '000010' carrier_id = 'LH' connection_id = '0400' flight_date = cl_abap_context_info=>get_system_date( ) + 30 flight_price = '450.00' currency_code = 'EUR' booking_status = 'N' local_last_changed_at = now ) TO bookings. ENDDO. ENDDO. INSERT zrap_travel FROM TABLE @travels. INSERT zrap_booking FROM TABLE @bookings. out->write( |{ lines( travels ) } travels, { lines( bookings ) } bookings created.| ). ENDMETHOD.ENDCLASS.Real project scenario
On a customer project the team modelled "Travel" with a composite semantic key (agency + travel number) and later needed to create travels from an external channel before a number was assigned. Switching to a UUID technical key with a separate business TravelID late in the project meant regenerating draft tables, reworking the projection and retesting the UI. Starting with UUID keys, a separate readable ID, @Semantics admin fields and currency-linked amounts — exactly the model in this part — avoided that rework on the next object.
Common mistakes
- Using a normal association instead of
association to parentin the child — the BDEF later cannot declare the child as composed. - Activating the root alone and fighting the cycle error instead of mass-activating.
- Declaring amounts without a currency reference; activation fails or the UI shows unformatted values.
- Hand-writing draft tables with DB-table field names instead of CDS alias names.
- Using the business ID as the key and then needing early numbering in every create path.
- Inserting test data into standard SAP tables — only seed your own Z tables, and only in development.
- Forgetting
local_last_changed_aton the child, which leaves no field for its local ETag.
Best practices
- Model the tree on paper first: root, children, keys, business IDs, admin fields.
- Use UUID technical keys plus a separate business ID for managed scenarios.
- Annotate admin and ETag timestamps with
@Semanticsso the framework fills them. - Link every amount to a currency in both table and CDS.
- Mass-activate root and child together.
- Generate draft tables from the BDEF quick fix, never by hand.
- Keep naming consistent (
ZRAP_*tables,ZI_RAP_*interface views) across the whole BO.
Interview angle
Expect: "What is the difference between composition and association in RAP?" — composition defines the BO tree (ownership, cascade delete, create-by-association, lock and ETag inheritance); association is navigation only. Also: "Why UUID keys?" — framework-managed early numbering, no number-range dependency, simple draft handling; a readable business ID is still shown to users.
Practice this lesson
Quick check
1. Requests must get gap-free numbers only when actually saved. Which technique fits?
2. Deep create of a travel with bookings fails with "operation not allowed" for bookings. What is missing?
3. A booking must exist only within its travel and be deleted with it. How is this modeled?
Interview questions
1. When do you choose early vs late numbering in RAP?
Answer it in your own words first, then compare.
2. Deep create of a travel with bookings fails in RAP. 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.
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