Core data services (CDS) are the backbone of the ABAP RESTful Application Programming Model (RAP). They define our data models, push logic down to the SAP HANA database, and cleanly express relationships. When data lives in standard tables, a standard CDS view stack (interface/projection views) combined with standard managed query capabilities works beautifully.
But what if your data does not live in a standard table? What if it requires a web service API call, complex procedural computation, or access to Clean-Core-restricted objects via legacy function modules?
This is where standard CDS views fall short, and where Custom CDS Entities combined with Unmanaged Queries come to the rescue.
When Standard CDS Gets Complex (And Why We Go Custom)
We all love standard CDS views. They give us strong typing, annotations, reusability, direct OData exposure, and high-performance push-down directly to HANA.
But standard views always expect a persisted table or database view as their source. When you try to force complex procedural calculations or multi-source merges into a SQL-based CDS view, you often end up with real database development nightmares:
| |
Instead of building 200-line CASE WHEN cascades, a Custom CDS Entity lets you delegate core data retrieval and processing to a dedicated ABAP class implementing the query provider interface. This gives you:
- No static DB source requirement: Data can be generated dynamically at runtime or fetched from external APIs.
- Freestyle calculations: Utilize full ABAP power (loops, classes, RTTI, internal tables, and legacy modules) to build results.
- Flexible integrations: Bring in Clean Core-compliant API data or wrap external microservice endpoints under a single OData structure.
The Architecture of Custom CDS Entities
A standard RAP path builds on interface views and database projection views before reaching the service definition. By contrast, a Custom CDS Entity bypasses the standard view and projection stack entirely. Its structure maps directly to an ABAP class, which is then handled as a top-level entity by the RAP service:
flowchart LR
subgraph Standard Path ["Standard CDS Path"]
DB[(DB Table)] --> I[Interface View
I_*]
I --> STD[Standard CDS View]
STD --> P[Projection View
R_* / C_*]
end
subgraph Custom Path ["Custom Entity Path"]
SRC1[(DB Table)] -.-> CLS[ABAP Class IF_RAP_QUERY_PROVIDER]
SRC2[External API] -.-> CLS
SRC3[Procedural Logic] -.-> CLS
CLS --> CUST[Custom CDS Entity]
end
P --> SD[Service Definition]
CUST --> SD
SD --> SB[Service Binding]
SB --> UI[Fiori UI]
style CUST fill:#d5f5e3,stroke:#27ae60,stroke-width:2px
style CLS fill:#d5f5e3,stroke:#27ae60,stroke-width:2pxBecause custom entities are not statically bound to database objects, you cannot write standard projection views on top of them (e.g., select from ZC_MyCustomEntity is invalid). If you need an annotation layer or alternative mapping, you must define another custom entity or layer annotations inside the service or metadata extensions.
Defining the Custom Entity
Developing a custom entity starts in Core Data Services. We use the keyword define root custom entity and annotate it with @ObjectModel.query.implementedBy to reference our query provider class:
| |
Notice that there is no select from clause. The custom entity does not reference database structures; it merely specifies the structural contract (fields, key declarations, and types) that your ABAP code will fulfill.
Implementing IF_RAP_QUERY_PROVIDER
The specified provider class must implement the IF_RAP_QUERY_PROVIDER interface. This interface contains a single method: select.
| |
set_data(...) should only receive the current page slice, while set_total_number_of_records(...) must receive the size of the entire filtered dataset (e.g., via a separate SELECT COUNT(*) on the DB).Watch Out: The Framework Holds You Accountable
The RAP framework tracks which configuration values and query filters you request inside select. If the OData request submits a filter or paging instruction, and your provider class completes execution without ever fetching them, the system will throw a runtime dump instead of returning silently incomplete results.
“You returned data, but you never checked what was requested - how could this possibly be right?”
— The RAP framework (effectively)
To avoid dumps, retrieve and process these values from the io_request object:
| Request Option | How to Retrieve |
|---|---|
$filter | io_request->get_filter() |
$orderby | io_request->get_sort_elements() |
$top / $skip | io_request->get_paging() |
$select | io_request->get_requested_elements() |
$search | io_request->get_search_expression() |
Handling Pagination (Paging)
| |
Free-Text Search ($search)
When a Fiori elements table triggers a search query via the global search field, it passes a $search flat string. To support this on your Custom Entity, you must denote the entity as searchable and annotate matching default elements:
| |
In your select implementation, gather the raw search terms using get_search_expression():
| |
Smart Filtering: Range Tables vs SQL Where String
When processing filters, you have two primary options:
1. The Dynamic SQL String Shortcut
If your custom entity fields map 1:1 to database column names, you can request a pre-constructed dynamic WHERE string straight from the framework:
| |
2. Standard Range Table Parsing
If your field names differ or if you are not querying a standard database table, fetch filters as range tables:
| |
Pro Tip: Type-Safe Filter Extraction with RTTI
Looping manually over filter ranges gets verbose very quickly. We can write a dynamic, type-safe extractor using Run-Time Type Identification (RTTI) and field symbol assignments.
In your query provider, define a private key/filter structure matching your entity’s properties:
| |
Next, implement a generic loop that utilizes RTTI to dynamically map ranges onto your typed structure components:
| |
This structural assignment means that adding a filter or key to your retrieval method simply requires adding a line to ty_filter. The extractor maps it automatically, bringing full autocomplete power directly to your IDE!
Custom Entities with Parameters
Parameters are mandatory inputs supplied by the caller (such as key dates, target currencies, or language parameters) that govern how custom logic behaves:
| |
In your provider, read them with get_parameters():
| |
Navigation & Associations: The Runtime Process
When linking custom entities together via associations (e.g., linking ZC_DemoProduct to a child list of ZC_DemoReview), keep in mind that the framework does not merge queries.
Drilling down into or expanding navigation paths triggers separate, independent calls to the target query providers:
sequenceDiagram
autonumber
participant UI as Fiori UI
participant FW as RAP Runtime
participant PQ as ZCL_PRODUCT_QUERY (Parent)
participant RQ as ZCL_REVIEW_QUERY (Child)
UI->>FW: GET Products (List)
FW->>PQ: select() - read standard products
PQ-->>FW: return product table
FW-->>UI: render products
Note over UI, RQ: User expands/selects a product to load reviews
UI->>FW: GET Products('P-001')/_Reviews
FW->>RQ: select() - with filter ProductId = 'P-001'
RQ-->>FW: return filtered reviews
FW-->>UI: render child tableIn the child table’s provider class, the parent key is made available via filter constraints. Treat child navigation lookups exactly like range-filtered list requests:
| |
Supporting Behavior: Unmanaged Create, Update, & Delete (CUD)
To make custom entities write-capable under RAP, define an unmanaged behavior definition pointing to an unmanaged behavior pool:
| |
In your behavior execution class, implement the required mutation logic:
| |
Summary: When to Pick Which CDS Type?
While Custom CDS Views provide complete freedom, they demand extra manual implementation. Use wisely by assessing your use case against this guide:
| Metric / Scenario | Standard CDS View | Custom CDS View |
|---|---|---|
| Data Source | Pure DB Table or standard DB Views | External API, calculation, Clean-Core proxy |
| Logic Layer | Push-down SQL, HANA engine | Procedural ABAP, class calls, function modules |
| Annotations | Flexible projection layers on top | Extended structures or direct exposure only |
| Effort | Low (Declarative code) | Medium/High (Manual pagination, filters, and CUD) |
| Analytics/KPIs | Automated aggregation engine | Custom manual aggregations required |
Custom CDS entities represent a powerful tool inside the modern ABAP Clean Core toolbox. By transferring data retrieval and processing logic to custom query classes, we gain full developer control over RAP services while maintaining strict platform boundaries.
