← Tüm yazılar

Adım Adım Draft'lı Managed RAP Servisi Oluşturma

Veritabanı tablosundan çalışan bir Fiori Elements uygulamasına — draft, validation, determination ve onay/ret aksiyonlarıyla eksiksiz bir managed RAP iş nesnesi.

RAPCDSABAP CloudFiori ElementsOData V4

Bu yazıda küçük ama eksiksiz bir RAP (ABAP RESTful Application Programming Model) uygulaması geliştireceğiz: çalışanların dizüstü bilgisayar ya da monitör talep ettiği, yöneticinin de bu talebi onaylayıp reddettiği bir Ekipman Talebi uygulaması.

Sonunda elimizde draft desteği, bir validation, iki determination ve iki aksiyon içeren bir Fiori Elements list report + object page olacak. Tek satır UI5 kodu yazmadan.

Ön koşullar: S/4HANA 2022 veya üzeri (on-premise ya da private cloud) veya SAP BTP ABAP ortamı ve ABAP Development Tools (Eclipse ya da VS Code). Buradaki her şey ABAP for Cloud Development dil sürümüyle ve yalnızca released API'lerle yazıldı.

Oluşturacağımız nesneler

Nesne Tür Amaç
ZEQUIP_REQ Veritabanı tablosu Kalıcı veri
ZR_EquipmentRequest CDS root view entity Temel iş nesnesi
ZR_EquipmentRequest Behavior definition İşlem davranışı + draft
ZBP_R_EquipmentRequest Sınıf Behavior implementation
ZC_EquipmentRequest CDS projection + behavior Servise özel görünüm
ZC_EquipmentRequest Metadata extension UI annotation'ları
ZUI_EQUIPMENTREQUEST Service definition Dışarı açılacaklar
ZUI_EQUIPMENTREQUEST_O4 Service binding OData V4 – UI

İsimlendirme basit bir kurala dayanıyor: root iş nesnesi için ZR_, projection için ZC_, behavior pool için ZBP_, UI servisleri için ZUI_.

1. Veritabanı tablosu

Anahtar alan bir UUID (sysuuid_x16); böylece RAP anahtarları kendisi atayabiliyor (managed numbering). İnsanların okuyacağı numara request_id ise kayıt sırasında ayrıca dolduruluyor.

Son beş alan yönetim alanları. abp_* veri elemanları ve bir sonraki adımdaki @Semantics annotation'ları sayesinde bu alanları framework dolduruyor; ETag ve draft için de gerekliler.

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

Root view, tablo alanlarına okunaklı CamelCase isimler veriyor ve yönetim alanlarını @Semantics annotation'larıyla işaretliyor.

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

İş nesnesinin kalbi burası. Root view'a sağ tıklayıp New Behavior Definition deyin, sonra şöyle düzenleyin:

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;
  }
}

Dikkat edilecek birkaç nokta:

  • strict ( 2 ) en güncel strict mode kontrollerini açar; yeni geliştirmede hep kullanın.
  • with draft + draft table zequip_req_d: draft tablosu henüz yok. İmleci adının üzerine getirip quick fix (Ctrl+1) ile oluşturun.
  • etag master kayıp güncellemelere karşı korur; draft için total etag zorunludur.
  • Aksiyonlardaki features : instance, butonların kayıt bazında açık/kapalı olacağına bizim karar vereceğimiz anlamına gelir.
  • draft determine action Prepare, validation'ı kullanıcı daha draft'tayken çalıştırır; hatalar Kaydet'ten önce görünür.

4. Behavior implementation

İlk satırdaki sınıf adının üzerinde quick fix ile ZBP_R_EquipmentRequest sınıfını oluşturun. Global sınıf boş kalır:

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.

Tüm mantık sınıfın Local Types sekmesinde:

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.

Metotlar ne yapıyor:

  • get_instance_features – Approve ve Reject yalnızca durum Açık iken etkin.
  • approve / reject – durumu EML ile (MODIFY ENTITIES ... IN LOCAL MODE) günceller ve değişen kaydı döner; ekran hemen yenilenir.
  • setinitialvalues – oluşturma anında çalışır; durumu Açık yapar, varsayılan para birimini atar.
  • setrequestid – kayıtta çalışır; sıradaki numarayı verir. Demo için MAX + 1 yeterli, ama gerçek sistemde number range nesnesi kullanın.
  • validatecost – sıfır veya negatif tutarı reddeder, mesajı EstimatedCost alanına bağlar.

IN LOCAL MODE yetki ve feature control kontrollerini atlar. Bu yüzden kullanıcıya salt okunur olan Status alanını değiştirebiliyoruz.

5. Projection katmanı

Projection, belirli bir servisin neyi göreceğini belirler. İşlemsel projection'larda provider contract transactional_query zorunludur.

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
}

Projection behavior, temel iş nesnesinin sunduklarını yeniden kullanır:

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 annotation'ları metadata extension'da

UI annotation'larını CDS view'a değil metadata extension'a yazın. View temiz kalır, UI katmanı da veri modeline dokunmadan değiştirilebilir.

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 ve binding

zui_equipmentrequest.srvd
@EndUserText.label: 'Equipment Request - UI Service'
define service ZUI_EQUIPMENTREQUEST {
  expose ZC_EquipmentRequest as EquipmentRequest;
}

Şimdi OData V4 – UI türünde ZUI_EQUIPMENTREQUEST_O4 adlı bir service binding oluşturun, aktive edin ve Publish'e basın. EquipmentRequest entity'sini seçip Preview'a tıklayın; Fiori Elements uygulaması tarayıcıda açılır.

Fiori Elements önizlemesinin oluşturduğu list report
Fiori Elements önizlemesinin oluşturduğu list report

8. Deneyelim

  1. Create'e tıklayın. Bir draft oluşur ve Status zaten Açıktır.
  2. Tutarı 0 bırakıp tekrar Create'e basın → validation mesajı alanın üzerinde görünür.
  3. Bir tutar girip kaydedin. Talep numarasını (RequestID) alır.
  4. Satırı seçip Approve'a tıklayın. Durum değişir, iki buton da pasifleşir.
Approve ve Reject aksiyonlarıyla object page
Approve ve Reject aksiyonlarıyla object page

Sırada ne var

  • MAX + 1 yerine number range nesnesi kullanmak.
  • Access control (DCL) ve gerçek bir yetki kontrolü eklemek.
  • new_message_with_text yerine metinleri bir mesaj sınıfına taşımak.
  • EML ve RAP test double'larıyla ABAP Unit testleri yazmak.
  • Transport öncesi cloud readiness varyantıyla ATC çalıştırmak.

Kaynak kodun tamamı yukarıdaki kod bloklarında; sırayla kopyalayıp aktive etmeniz yeterli. Soru ya da fikirleriniz varsa memnuniyetle dinlerim.

Yorumlar

Yorum yazın