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.
Explanation
📌 KEY POINTS
✅ Consumers use projections (ZC_), never the base BO directly.
✅ One projection per consumer: UI with draft, API without draft.
✅ Test with EML + COMMIT ENTITIES; validations run only at commit.
⚠️ Read SAP data through released CDS views (clean core).
▶ WHERE WE ARE
You now have a complete business object: ZR_PurchReq with items, numbering, locking, authorization, workflow and draft. This last part covers what separates a working demo from a production object: a stable projection layer for consumers, automated tests, performance and clean-core rules.
━━━━━━━━━━━━━━━━━━━━
▶ 1. THE PROJECTION LAYER — WHY IT EXISTS
Never expose ZR_ views directly in a service. Put a projection layer (ZC_ views) between the business object and its consumers:
- Base BO (ZR_) — all fields and all behavior. Reused by every consumer.
- Projection (ZC_) — what one specific app or API needs: a subset of fields, a subset of operations and actions, UI annotations, value helps, and search.
Several projections can sit on one BO — for example ZC_PurchReq for the Fiori app and ZA_PurchReq for an API that only allows reads and approvals. Each projection changes without touching the core behavior.
━━━━━━━━━━━━━━━━━━━━
▶ 2. BUILD THE PROJECTION — STEP BY STEP
- Projection root view: "define root view entity ZC_PurchReq provider contract transactional_query as projection on ZR_PurchReq". The provider contract transactional_query marks it as a projection for transactional services.
- Redirect compositions: in the root, "_Item : redirected to composition child ZC_PurchReqItem"; in the child, "_Request : redirected to parent ZC_PurchReq". Without redirection the projection would point back to the base views.
- Add @Metadata.allowExtensions: true so UI annotations can live in a separate metadata extension (Service Bindings Part 6).
- Projection behavior definition: right-click ZC_PurchReq, New Behavior Definition. It starts with "projection;" and "use draft;" and lists only the operations this app may use: "use create; use update; use action approve;" and so on. Anything not listed is not available to this consumer, even though the base BO supports it.
- Hand over to the service layer: a service definition exposes ZC_PurchReq and ZC_PurchReqItem, and a service binding publishes it as OData V4. This is covered step by step in the Service Bindings series, which uses exactly these objects.
━━━━━━━━━━━━━━━━━━━━
▶ 3. TESTING WITH EML AND ABAP UNIT
Test the behavior, not the UI. An ABAP Unit test class calls the BO with EML exactly as a service would:
- Arrange: build input with %cid.
- Act: MODIFY ENTITIES, and COMMIT ENTITIES RESPONSE OF zr_purchreq when you need the save sequence (validations, on-save determinations).
- Assert: check FAILED, REPORTED and the data returned by READ ENTITIES.
- Clean up: ROLLBACK ENTITIES in teardown.
Good first tests: submit without items fails; approve is disabled for status New; the total equals the sum of items; a request without description cannot be saved.
Tests that let the save succeed will write to the database. To isolate them, use the SQL test double framework (cl_osql_test_environment) for the tables, or the RAP BO test doubles on releases that provide them.
━━━━━━━━━━━━━━━━━━━━
▶ 4. PERFORMANCE RULES FOR HANDLERS
Every handler receives a table of keys — possibly hundreds from one mass action or API call. The three rules:
- One READ ENTITIES for all keys, never one per key in a loop.
- One MODIFY ENTITIES with a table, never one per key.
- No SELECT inside a loop. If you must read other data, read it once for all keys (with a join or with FOR ALL ENTRIES after checking the driver table is not empty).
Also keep determination triggers narrow (specific fields). A determination that triggers on every field change and modifies fields can cause cascades of further determinations.
For read performance in the UI, keep the projection lean, avoid calculated fields that need expensive joins in list reports, and check the generated SQL with the SQL trace (ST05) or the ABAP profiler.
━━━━━━━━━━━━━━━━━━━━
▶ 5. CLEAN-CORE BOUNDARIES
If you develop in ABAP Cloud (BTP ABAP environment, or the ABAP Cloud language version in S/4HANA):
- Use only released SAP APIs and CDS views (release contract C1). The syntax check enforces this.
- To reuse SAP standard data, read released CDS views (e.g. I_ views marked released), not SAP tables.
- To change SAP standard objects, call their released RAP business objects or released APIs via EML — never update SAP tables directly, never call unreleased function modules.
- Your own custom tables (like ZPR_HEAD) may be written by your own managed BO — that is exactly what managed RAP is for.
If a legacy, unreleased API must be used, wrap it in classic ABAP and release the wrapper for cloud use. The Unmanaged RAP series shows how such APIs are integrated.
━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU HAVE AFTER THE SERIES
A production-shaped managed RAP business object with a projection layer, tests and clean-core design. Next: the Service Bindings series exposes ZC_PurchReq as an OData V4 service and a Fiori Elements app. Then compare with the Unmanaged RAP series to see when you would not let the framework own persistence.
Code example
// ===== Projection root view ZC_PurchReq =====@AccessControl.authorizationCheck: #NOT_REQUIRED@Metadata.allowExtensions: true@EndUserText.label: 'Purchase Request - UI projection'define root view entity ZC_PurchReq provider contract transactional_query as projection on ZR_PurchReq{ key RequestUUID, RequestID, Description, Requester, Status, TotalAmount, CurrencyCode, LocalLastChangedAt, _Item : redirected to composition child ZC_PurchReqItem} // ===== Projection child view ZC_PurchReqItem =====@AccessControl.authorizationCheck: #NOT_REQUIRED@Metadata.allowExtensions: truedefine view entity ZC_PurchReqItem as projection on ZR_PurchReqItem{ key ItemUUID, RequestUUID, ItemNo, ItemText, Quantity, Unit, Price, CurrencyCode, LocalLastChangedAt, _Request : redirected to parent ZC_PurchReq} // ===== Projection behavior definition ZC_PurchReq =====projection;strict ( 2 );use draft; define behavior for ZC_PurchReq alias PurchaseRequest{ use create; use update; use delete; use action submit; use action approve; use action reject; use action Edit; use action Activate; use action Discard; use action Resume; use action Prepare; use association _Item { create; with draft; }} define behavior for ZC_PurchReqItem alias Item{ use update; use delete; use association _Request { with draft; }} " ===== ABAP Unit test (test class of ZBP_R_PURCHREQ) =====CLASS ltc_purchreq DEFINITION FINAL FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. PRIVATE SECTION. METHODS submit_without_items_fails FOR TESTING. METHODS teardown.ENDCLASS. CLASS ltc_purchreq IMPLEMENTATION. METHOD submit_without_items_fails. " Arrange + Act: create a request with no items and try to submit it MODIFY ENTITIES OF zr_purchreq ENTITY PurchaseRequest CREATE FIELDS ( Description CurrencyCode ) WITH VALUE #( ( %cid = 'T1' Description = 'Unit test' CurrencyCode = 'INR' ) ) ENTITY PurchaseRequest EXECUTE submit FROM VALUE #( ( %cid_ref = 'T1' ) ) FAILED DATA(failed) REPORTED DATA(reported). " Assert cl_abap_unit_assert=>assert_not_initial( act = failed-purchaserequest msg = 'Submit without items must be rejected' ). cl_abap_unit_assert=>assert_not_initial( act = reported-purchaserequest msg = 'User must get a message explaining why' ). ENDMETHOD. METHOD teardown. ROLLBACK ENTITIES. " discard the transactional buffer - nothing was committed ENDMETHOD. ENDCLASS. " ===== Performance: WRONG vs RIGHT =====" WRONG - one database round trip per keyLOOP AT keys INTO DATA(key). SELECT SINGLE * FROM zpr_head WHERE request_uuid = @key-RequestUUID INTO @DATA(row).ENDLOOP. " RIGHT - one read for the whole set, through the BOREAD ENTITIES OF zr_purchreq IN LOCAL MODE ENTITY PurchaseRequest ALL FIELDS WITH CORRESPONDING #( keys ) RESULT DATA(requests).Real project scenario
A managed RAP object worked well in testing with one request at a time. After go-live, an integration pushed 2,000 approvals in one OData batch, and the call timed out. The cause was a helper method in the approve action that selected the approver's cost-center data with SELECT SINGLE inside a loop over keys, plus a determination on the item that triggered on every field and re-read all items each time.
The fix followed the three handler rules: read cost-center data once for all keys, narrow the determination trigger to Quantity and Price, and replace per-key MODIFY calls with one set-based MODIFY. Response time for the batch fell from over two minutes to a few seconds.
The team also added an ABAP Unit test for the approval path with 500 keys so the regression could not return unnoticed.
Common mistakes
- Exposing ZR_ base views directly in a service, so every UI change touches the core business object.
- Forgetting "redirected to composition child / parent" in the projection, which breaks navigation and draft.
- Listing every base operation with "use" in every projection, instead of exposing only what that consumer needs.
- Testing only through the Fiori preview — slow, manual and blind to bulk behavior.
- Tests that commit real data and are not isolated, leaving junk records in the development system.
- SELECT or MODIFY inside loops over keys in handlers.
- Broad determination triggers that cause cascades of re-execution.
- Calling unreleased SAP APIs or updating SAP standard tables from ABAP Cloud code.
Best practices
- One base BO (ZR_), one projection per consumer (ZC_ for UI, a separate one for APIs).
- Keep UI annotations in metadata extensions on the projection, never on the base BO.
- Write EML-based ABAP Unit tests for every validation, action and feature-control rule; run them in ATC/CI.
- Isolate database-writing tests with test doubles.
- Design every handler for sets of keys; test with large inputs, not one record.
- In ABAP Cloud, use only released APIs; wrap legacy APIs in released classic-ABAP wrappers.
- Document the status model and which behavior element enforces each rule — it makes support and change requests much faster.
Interview angle
Senior-level questions: "Why do you need a projection layer?", "How do you test a RAP business object?", "What are typical RAP performance problems?", "What does clean core mean for RAP development?"
Strong answer: the projection layer decouples consumers from the core BO, so each UI or API gets only the fields, operations and annotations it needs, and multiple projections can share one BO. Tests use EML in ABAP Unit, including COMMIT ENTITIES RESPONSE for the save sequence and ROLLBACK ENTITIES for cleanup, with test doubles for isolation. Performance problems usually come from per-key reads and modifies or broad determination triggers. Clean core means released APIs only, no direct changes to SAP tables, and legacy access wrapped behind released interfaces. Concrete numbers from a real incident make this answer stand out.
Practice this lesson
Quick check
1. Which statement about a projection behavior definition (projection; use create; use action approve; ...) is correct?
2. A RAP action is fast for one instance but times out for 2,000 instances in one request. What is the most likely cause?
3. In managed RAP, where are create and update changes held during the interaction phase, before the user saves?
Interview questions
1. A mass approval of 2,000 purchase requests through OData times out, although single approvals are fast. How do you analyze and fix it?
Answer it in your own words first, then compare.
2. You inherit a managed RAP business object before a go-live. What would you review, and which findings would block production?
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.