← Alle Artikel

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.

RAPCDSABAP CloudFiori ElementsOData V4

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.

zequip_req.tabl
@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.

zr_equipmentrequest.ddls
@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:

zr_equipmentrequest.bdef
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 master schützt vor verlorenen Änderungen; total etag ist bei Draft Pflicht.
  • features : instance an den Aktionen bedeutet: Wir entscheiden pro Datensatz, ob die Buttons aktiv sind.
  • draft determine action Prepare fü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:

zbp_r_equipmentrequest.global.abap
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:

zbp_r_equipmentrequest.clas.abap
" 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 Feld EstimatedCost.

IN LOCAL MODE überspringt Berechtigungs- und Feature-Control-Prüfungen. Deshalb können wir Status ä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.

zc_equipmentrequest.ddls
@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:

zc_equipmentrequest.bdef
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.

zc_equipmentrequest.ddlx
@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

zui_equipmentrequest.srvd
@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.

Der von der Fiori-Elements-Vorschau erzeugte List Report
Der von der Fiori-Elements-Vorschau erzeugte List Report

8. Ausprobieren

  1. Auf Create klicken. Ein Draft entsteht, Status ist bereits Offen.
  2. Kosten auf 0 lassen und erneut Create drücken → die Validierungsmeldung erscheint am Feld.
  3. Kosten eingeben und sichern. Die Anforderung erhält ihre Nummer (RequestID).
  4. Zeile markieren und Approve klicken. Der Status ändert sich, beide Buttons werden inaktiv.
Die Object Page mit den Aktionen Approve und Reject
Die Object Page mit den Aktionen Approve und Reject

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_text auslagern.
  • 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

Kommentar schreiben