RAP Basics
Architect / Cross-trackbeginner

RAP Build-Along Part 2: Persistence Tables, Root and Child CDS Views

Key takeaway

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:

  1. 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.
  2. Business ID: a readable travel_id (for example 00000042) shown to users. It is *not* the key; Part 3 and Part 4 show how to fill it.
  3. Parent foreign key: ZRAP_BOOKING-parent_uuid stores the Travel UUID. The child's own key is booking_uuid.
  4. Admin fields: created by/at, last changed by/at, and a *local* last-changed timestamp. These feed ETag and audit in Part 3.
  5. 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.clnt is 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 : #A marks application data.
  • @Semantics.amount.currencyCode on the amount field links the amount to its currency field. Without it, activation reports an error for abap.curr fields.
  • local_last_changed_at changes on *every* save of that row; last_changed_at is 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:

  1. Write the behavior definition in Part 3 with with draft; and draft table zrap_trav_d.
  2. 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 entity marks 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 _Booking declares 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, ...lastChangedAt tell the managed framework which fields it should fill automatically. With these annotations you write no code for admin fields.
  • @Semantics.amount.currencyCode: 'CurrencyCode' on TotalPrice (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 $projection fields (the aliased names).
  • The child is not a root, so it uses define view entity, not define 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:

  1. 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.
  2. Two-pass: activate ZI_RAP_Travel without the composition line, activate ZI_RAP_Booking with its parent association, then add the composition back to the root and activate again.

Full order for this part:

  1. ZRAP_TRAVEL (table) → activate
  2. ZRAP_BOOKING (table) → activate
  3. ZI_RAP_Travel + ZI_RAP_Booking → mass-activate together
  4. Optional: run the data generator class from the code section to insert test rows
  5. 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_TRAVEL and ZRAP_BOOKING are active with UUID keys, business ID, admin and ETag fields, and linked currency.
  • [ ] ZI_RAP_Travel is a root view entity with composition [0..*] to ZI_RAP_Booking.
  • [ ] ZI_RAP_Booking has association to parent with an ON condition on ParentUUID.
  • [ ] Data Preview on the root navigates to bookings.
  • [ ] You know the names ZRAP_TRAV_D / ZRAP_BOOK_D will 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

ABAP Code
-- ============================================================-- 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 parent in 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_at on 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 @Semantics so 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.

Practice more with Interview AI →

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

Interview questions on this topic

Related SAP topics