Schritt für Schritt zum Managed-RAP-Service mit Draft
Von der Datenbanktabelle zur lauffähigen Fiori-Elements-App — ein vollständiges Managed-RAP-Business-Objekt mit Draft, Validierung, Determinationen und Genehmigen/Ablehnen-Aktionen.
In diesem Artikel bauen wir eine kleine, aber vollständige RAP-Anwendung (ABAP RESTful Application Programming Model): eine Geräteanforderung, in der Mitarbeitende einen Laptop oder Monitor anfordern und eine Führungskraft die Anforderung genehmigt oder ablehnt.
Am Ende steht ein Fiori-Elements-List-Report mit Object Page — inklusive Draft, einer Validierung, zwei Determinationen und zwei Aktionen. Ganz ohne UI5-Code.
Voraussetzungen: S/4HANA 2022 oder neuer (On-Premise oder Private Cloud) oder die SAP BTP ABAP Environment sowie die ABAP Development Tools (Eclipse oder VS Code). Alles hier verwendet ABAP for Cloud Development und ausschließlich freigegebene (released) APIs.
Die Objekte im Überblick
| Objekt | Typ | Zweck |
|---|---|---|
ZEQUIP_REQ |
Datenbanktabelle | Persistente Daten |
ZR_EquipmentRequest |
CDS Root View Entity | Basis-Business-Objekt |
ZR_EquipmentRequest |
Behavior Definition | Transaktionales Verhalten + Draft |
ZBP_R_EquipmentRequest |
Klasse | Behavior Implementation |
ZC_EquipmentRequest |
CDS-Projektion + Behavior | Servicespezifische Sicht |
ZC_EquipmentRequest |
Metadata Extension | UI-Annotationen |
ZUI_EQUIPMENTREQUEST |
Service Definition | Was veröffentlicht wird |
ZUI_EQUIPMENTREQUEST_O4 |
Service Binding | OData V4 – UI |
Die Namenskonvention ist einfach: ZR_ für das Root-Business-Objekt, ZC_ für Projektionen, ZBP_ für Behavior Pools und ZUI_ für UI-Services.
1. Datenbanktabelle
Der Schlüssel ist eine UUID (sysuuid_x16). So kann RAP die Schlüssel selbst vergeben (Managed Numbering). Die lesbare Nummer request_id wird separat beim Sichern gefüllt.
Die letzten fünf Felder sind Verwaltungsfelder. Dank der abp_*-Datenelemente und der @Semantics-Annotationen im nächsten Schritt füllt das Framework sie automatisch — außerdem werden sie für ETag und Draft benötigt.
@EndUserText.label : 'Equipment Request'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zequip_req {
key client : abap.clnt not null;
key request_uuid : sysuuid_x16 not null;
request_id : abap.numc(10);
description : abap.char(80);
equipment_type : abap.char(20);
@Semantics.amount.currencyCode : 'zequip_req.currency_code'
estimated_cost : abap.curr(15,2);
currency_code : abap.cuky;
status : abap.char(1);
requested_by : abp_creation_user;
created_at : abp_creation_tstmpl;
local_last_changed_by : abp_locinst_lastchange_user;
local_last_changed_at : abp_locinst_lastchange_tstmpl;
last_changed_at : abp_lastchange_tstmpl;
}
2. Root-CDS-View-Entity
Die Root-View gibt den Feldern lesbare CamelCase-Namen und kennzeichnet die Verwaltungsfelder mit @Semantics-Annotationen.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Equipment Request - Root'
define root view entity ZR_EquipmentRequest
as select from zequip_req
{
key request_uuid as RequestUUID,
request_id as RequestID,
description as Description,
equipment_type as EquipmentType,
@Semantics.amount.currencyCode: 'CurrencyCode'
estimated_cost as EstimatedCost,
currency_code as CurrencyCode,
status as Status,
@Semantics.user.createdBy: true
requested_by as RequestedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
@Semantics.user.localInstanceLastChangedBy: true
local_last_changed_by as LocalLastChangedBy,
@Semantics.systemDateTime.localInstanceLastChangedAt: true
local_last_changed_at as LocalLastChangedAt,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt
}
3. Behavior Definition
Das ist das Herzstück des Business-Objekts. Rechtsklick auf die Root-View → New Behavior Definition, dann anpassen:
managed implementation in class zbp_r_equipmentrequest unique;
strict ( 2 );
with draft;
define behavior for ZR_EquipmentRequest alias EquipmentRequest
persistent table zequip_req
draft table zequip_req_d
etag master LocalLastChangedAt
lock master total etag LastChangedAt
authorization master ( global )
{
field ( numbering : managed, readonly ) RequestUUID;
field ( readonly ) RequestID, Status, RequestedBy, CreatedAt,
LocalLastChangedBy, LocalLastChangedAt, LastChangedAt;
field ( mandatory ) Description, EquipmentType;
create;
update;
delete;
action ( features : instance ) approve result [1] $self;
action ( features : instance ) reject result [1] $self;
determination setInitialValues on modify { create; }
determination setRequestID on save { create; }
validation validateCost on save { create; field EstimatedCost, CurrencyCode; }
draft action Activate optimized;
draft action Discard;
draft action Edit;
draft action Resume;
draft determine action Prepare { validation validateCost; }
mapping for zequip_req
{
RequestUUID = request_uuid;
RequestID = request_id;
Description = description;
EquipmentType = equipment_type;
EstimatedCost = estimated_cost;
CurrencyCode = currency_code;
Status = status;
RequestedBy = requested_by;
CreatedAt = created_at;
LocalLastChangedBy = local_last_changed_by;
LocalLastChangedAt = local_last_changed_at;
LastChangedAt = last_changed_at;
}
}
Ein paar wichtige Punkte:
strict ( 2 )aktiviert die aktuellsten Strict-Mode-Prüfungen — für Neuentwicklungen immer verwenden.with draft+draft table zequip_req_d: Die Draft-Tabelle existiert noch nicht. Cursor auf den Namen setzen und per Quick Fix (Strg+1) generieren.etag masterschützt vor verlorenen Änderungen;total etagist bei Draft Pflicht.features : instancean den Aktionen bedeutet: Wir entscheiden pro Datensatz, ob die Buttons aktiv sind.draft determine action Prepareführt die Validierung schon im Draft aus — Fehler erscheinen also vor dem Sichern.
4. Behavior Implementation
Per Quick Fix auf dem Klassennamen in der ersten Zeile ZBP_R_EquipmentRequest anlegen. Die globale Klasse bleibt leer:
CLASS zbp_r_equipmentrequest DEFINITION PUBLIC ABSTRACT FINAL
FOR BEHAVIOR OF zr_equipmentrequest.
ENDCLASS.
CLASS zbp_r_equipmentrequest IMPLEMENTATION.
ENDCLASS.
Die gesamte Logik steht im Reiter Local Types der Klasse:
" Local types (tab "Local Types" in ADT)
CLASS lhc_equipmentrequest DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
CONSTANTS:
BEGIN OF c_status,
open TYPE c LENGTH 1 VALUE 'O',
approved TYPE c LENGTH 1 VALUE 'A',
rejected TYPE c LENGTH 1 VALUE 'X',
END OF c_status.
METHODS get_global_authorizations FOR GLOBAL AUTHORIZATION
IMPORTING REQUEST requested_authorizations FOR EquipmentRequest RESULT result.
METHODS get_instance_features FOR INSTANCE FEATURES
IMPORTING keys REQUEST requested_features FOR EquipmentRequest RESULT result.
METHODS approve FOR MODIFY
IMPORTING keys FOR ACTION EquipmentRequest~approve RESULT result.
METHODS reject FOR MODIFY
IMPORTING keys FOR ACTION EquipmentRequest~reject RESULT result.
METHODS setinitialvalues FOR DETERMINE ON MODIFY
IMPORTING keys FOR EquipmentRequest~setInitialValues.
METHODS setrequestid FOR DETERMINE ON SAVE
IMPORTING keys FOR EquipmentRequest~setRequestID.
METHODS validatecost FOR VALIDATE ON SAVE
IMPORTING keys FOR EquipmentRequest~validateCost.
ENDCLASS.
CLASS lhc_equipmentrequest IMPLEMENTATION.
METHOD get_global_authorizations.
" Demo: everyone may create/change/delete.
" In a real app check an authorization object here (AUTHORITY-CHECK).
result = VALUE #( %create = if_abap_behv=>auth-allowed
%update = if_abap_behv=>auth-allowed
%delete = if_abap_behv=>auth-allowed ).
ENDMETHOD.
METHOD get_instance_features.
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
FIELDS ( Status ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
" Approve / reject only while the request is still open
result = VALUE #( FOR req IN requests
LET enabled = COND #( WHEN req-Status = c_status-open
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
IN ( %tky = req-%tky
%action-approve = enabled
%action-reject = enabled ) ).
ENDMETHOD.
METHOD approve.
MODIFY ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
UPDATE FIELDS ( Status )
WITH VALUE #( FOR key IN keys ( %tky = key-%tky Status = c_status-approved ) ).
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(requests).
result = VALUE #( FOR req IN requests ( %tky = req-%tky %param = req ) ).
ENDMETHOD.
METHOD reject.
MODIFY ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
UPDATE FIELDS ( Status )
WITH VALUE #( FOR key IN keys ( %tky = key-%tky Status = c_status-rejected ) ).
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(requests).
result = VALUE #( FOR req IN requests ( %tky = req-%tky %param = req ) ).
ENDMETHOD.
METHOD setinitialvalues.
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
FIELDS ( Status CurrencyCode ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DELETE requests WHERE Status IS NOT INITIAL.
CHECK requests IS NOT INITIAL.
MODIFY ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
UPDATE FIELDS ( Status CurrencyCode )
WITH VALUE #( FOR req IN requests
( %tky = req-%tky
Status = c_status-open
CurrencyCode = COND #( WHEN req-CurrencyCode IS INITIAL
THEN 'EUR'
ELSE req-CurrencyCode ) ) ).
ENDMETHOD.
METHOD setrequestid.
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
FIELDS ( RequestID ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
DELETE requests WHERE RequestID IS NOT INITIAL.
CHECK requests IS NOT INITIAL.
" Demo only: MAX + 1. Use a number range object in production.
SELECT SINGLE FROM zequip_req
FIELDS MAX( request_id )
INTO @DATA(max_id).
MODIFY ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
UPDATE FIELDS ( RequestID )
WITH VALUE #( FOR req IN requests INDEX INTO i
( %tky = req-%tky RequestID = max_id + i ) ).
ENDMETHOD.
METHOD validatecost.
READ ENTITIES OF zr_equipmentrequest IN LOCAL MODE
ENTITY EquipmentRequest
FIELDS ( EstimatedCost CurrencyCode ) WITH CORRESPONDING #( keys )
RESULT DATA(requests).
LOOP AT requests INTO DATA(req).
" Clear old messages of this validation (draft state messages)
APPEND VALUE #( %tky = req-%tky %state_area = 'VALIDATE_COST' )
TO reported-equipmentrequest.
IF req-EstimatedCost <= 0.
APPEND VALUE #( %tky = req-%tky ) TO failed-equipmentrequest.
APPEND VALUE #( %tky = req-%tky
%state_area = 'VALIDATE_COST'
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Estimated cost must be greater than zero' )
%element-EstimatedCost = if_abap_behv=>mk-on )
TO reported-equipmentrequest.
ENDIF.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
Was die Methoden tun:
get_instance_features– Approve und Reject sind nur im Status Offen aktiv.approve/reject– ändern den Status per EML (MODIFY ENTITIES ... IN LOCAL MODE) und liefern den geänderten Datensatz zurück, damit die Oberfläche sofort aktualisiert wird.setinitialvalues– beim Anlegen: Status Offen und Standardwährung.setrequestid– beim Sichern: vergibt die nächste Nummer. MAX + 1 reicht für eine Demo, produktiv sollte ein Nummernkreisobjekt verwendet werden.validatecost– lehnt Kosten ≤ 0 ab und hängt die Meldung an das FeldEstimatedCost.
IN LOCAL MODEüberspringt Berechtigungs- und Feature-Control-Prüfungen. Deshalb können wirStatusändern, obwohl das Feld für Benutzer schreibgeschützt ist.
5. Projektionsschicht
Die Projektion legt fest, was ein bestimmter Service sieht. Für transaktionale Projektionen ist provider contract transactional_query Pflicht.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Equipment Request - Projection'
@Metadata.allowExtensions: true
@Search.searchable: true
define root view entity ZC_EquipmentRequest
provider contract transactional_query
as projection on ZR_EquipmentRequest
{
key RequestUUID,
@Search.defaultSearchElement: true
RequestID,
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
Description,
EquipmentType,
EstimatedCost,
@Consumption.valueHelpDefinition: [{ entity: { name: 'I_CurrencyStdVH', element: 'Currency' } }]
CurrencyCode,
Status,
RequestedBy,
CreatedAt,
LocalLastChangedAt,
LastChangedAt
}
Das Projektions-Behavior übernimmt einfach, was das Basisobjekt anbietet:
projection;
strict ( 2 );
use draft;
define behavior for ZC_EquipmentRequest alias EquipmentRequest
use etag
{
use create;
use update;
use delete;
use action approve;
use action reject;
use action Activate;
use action Discard;
use action Edit;
use action Resume;
use action Prepare;
}
6. UI-Annotationen in einer Metadata Extension
UI-Annotationen gehören nicht in die CDS-View, sondern in eine Metadata Extension. So bleibt die View schlank, und die UI-Schicht lässt sich ändern, ohne das Datenmodell anzufassen.
@Metadata.layer: #CUSTOMER
@UI.headerInfo: { typeName: 'Equipment Request',
typeNamePlural: 'Equipment Requests',
title: { type: #STANDARD, value: 'Description' },
description: { value: 'RequestID' } }
annotate entity ZC_EquipmentRequest with
{
@UI.facet: [{ id: 'General', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE,
label: 'General Information', position: 10 }]
@UI.hidden: true
RequestUUID;
@UI: { lineItem: [{ position: 10 }], identification: [{ position: 10 }],
selectionField: [{ position: 10 }] }
RequestID;
@UI: { lineItem: [{ position: 20 }], identification: [{ position: 20 }] }
Description;
@UI: { lineItem: [{ position: 30 }], identification: [{ position: 30 }],
selectionField: [{ position: 20 }] }
EquipmentType;
@UI: { lineItem: [{ position: 40 }], identification: [{ position: 40 }] }
EstimatedCost;
@UI: { lineItem: [{ position: 50 },
{ type: #FOR_ACTION, dataAction: 'approve', label: 'Approve' },
{ type: #FOR_ACTION, dataAction: 'reject', label: 'Reject' }],
identification: [{ position: 50 },
{ type: #FOR_ACTION, dataAction: 'approve', label: 'Approve' },
{ type: #FOR_ACTION, dataAction: 'reject', label: 'Reject' }],
selectionField: [{ position: 30 }] }
Status;
@UI: { lineItem: [{ position: 60 }], identification: [{ position: 60 }] }
RequestedBy;
}
7. Service Definition und Binding
@EndUserText.label: 'Equipment Request - UI Service'
define service ZUI_EQUIPMENTREQUEST {
expose ZC_EquipmentRequest as EquipmentRequest;
}
Jetzt ein Service Binding ZUI_EQUIPMENTREQUEST_O4 vom Typ OData V4 – UI anlegen, aktivieren und auf Publish klicken. Die Entität EquipmentRequest auswählen und Preview wählen — die Fiori-Elements-App öffnet sich im Browser.
8. Ausprobieren
- Auf Create klicken. Ein Draft entsteht, Status ist bereits Offen.
- Kosten auf 0 lassen und erneut Create drücken → die Validierungsmeldung erscheint am Feld.
- Kosten eingeben und sichern. Die Anforderung erhält ihre Nummer (
RequestID). - Zeile markieren und Approve klicken. Der Status ändert sich, beide Buttons werden inaktiv.
Wie geht es weiter?
- MAX + 1 durch ein Nummernkreisobjekt ersetzen.
- Eine Zugriffskontrolle (DCL) und eine echte Berechtigungsprüfung ergänzen.
- Texte in eine Nachrichtenklasse statt
new_message_with_textauslagern. - ABAP-Unit-Tests mit EML und den RAP-Test-Doubles schreiben.
- Vor dem Transport ATC mit der Cloud-Readiness-Variante ausführen.
Der vollständige Quellcode steht in den Codeblöcken oben — einfach der Reihe nach kopieren und aktivieren. Fragen oder Ideen? Ich freue mich über Feedback.
Kommentare