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.
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.
@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.
@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:
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 masterkayıp güncellemelere karşı korur; draft içintotal etagzorunludur.- 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:
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:
" 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ıEstimatedCostalanına bağlar.
IN LOCAL MODEyetki ve feature control kontrollerini atlar. Bu yüzden kullanıcıya salt okunur olanStatusalanını değiştirebiliyoruz.
5. Projection katmanı
Projection, belirli bir servisin neyi göreceğini belirler. İşlemsel projection'larda provider contract transactional_query zorunludur.
@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:
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.
@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
@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.
8. Deneyelim
- Create'e tıklayın. Bir draft oluşur ve Status zaten Açıktır.
- Tutarı 0 bırakıp tekrar Create'e basın → validation mesajı alanın üzerinde görünür.
- Bir tutar girip kaydedin. Talep numarasını (
RequestID) alır. - Satırı seçip Approve'a tıklayın. Durum değişir, iki buton da pasifleşir.
Sırada ne var
- MAX + 1 yerine number range nesnesi kullanmak.
- Access control (DCL) ve gerçek bir yetki kontrolü eklemek.
new_message_with_textyerine 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