はじめに
RAPでイベントを発生させ、それをEvent Meshで利用するということがBTP ABAP Environmentのリリース2208から可能になっています。さらに、昨年のDevtoberfestの動画では、発生させたイベントをABAPシステム内で消費することができるようになったことが紹介されていました。
本記事では、RAPで発生させたイベントをABAPシステム内でローカルに利用する方法について検証しました。
シナリオ
銀行口座を模した簡単なRAP BOを作成します。RAP BOには「預け入れ」「引き出し」のアクションがあります。アクションが実行されると残高が更新され、これをトリガとしてイベントが発生します。
イベントのコンシューマとして、口座のランクを管理するグローバルクラスを作成します。残高更新のイベントを受け取ると、残高に応じたランクを口座に設定します。
実装
RAP BO(イベントプロデューサー)
テーブル定義
@EndUserText.label : 'Bank Account'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zbankaccount {
key client : abap.clnt not null;
key accountid : zde_account_id not null;
@Semantics.amount.currencyCode : 'zbankaccount.currency'
balance : abap.curr(13,2);
currency : abap.cuky;
created_by : abp_creation_user;
created_at : abp_creation_tstmpl;
last_changed_by : abp_locinst_lastchange_user;
local_last_changed_at : abp_locinst_lastchange_tstmpl;
last_changed_at : abp_lastchange_tstmpl;
}
Behavior Definition
with additional save
を指定し、保存後の追加の処理でイベントを発生させられるようにします。また、event <イベント名> parameter <Abstract Entity>
でイベントを定義します。
managed implementation in class ZBP_R_BankAccountTP unique;
strict ( 2 );
with draft;
define behavior for ZR_BankAccountTP alias BankAccount
persistent table ZBANKACCOUNT
draft table ZBANKACCOUNT00D
etag master LocalLastChangedAt
lock master total etag LastChangedAt
authorization master( global )
late numbering
with additional save
{
field ( readonly )
CreatedBy,
CreatedAt,
LastChangedBy,
LocalLastChangedAt,
LastChangedAt,
AccountID,
Balance;
field ( readonly : update )
Currency;
create;
update;
delete;
draft action Edit;
draft action Activate;
draft action Discard;
draft action Resume;
draft determine action Prepare;
action deposit parameter za_amount result [1] $self;
action withdraw parameter za_amount result [1] $self;
event balanceChanged parameter ZA_BALANCECHANGED;
mapping for ZBANKACCOUNT
{
AccountID = ACCOUNTID;
Balance = BAlANCE;
Currency = CURRENCY;
CreatedBy = CREATED_BY;
CreatedAt = CREATED_AT;
LastChangedBy = LAST_CHANGED_BY;
LocalLastChangedAt = LOCAL_LAST_CHANGED_AT;
LastChangedAt = LAST_CHANGED_AT;
}
}
イベントパラメータとして使うAbstract Entityの定義は以下です。イベントパラメータにエンティティのキーは持っているので、ここにはキー以外で追加したい項目を指定します。今回は特にないためdummy
で定義しています。
@EndUserText.label: 'Parameters for Balance Changed Event'
define abstract entity ZA_BALANCECHANGED
{
dummy: abap.char( 1 );
}
Behavior Implementation
save_modified
メソッドで、残高に変更があったらイベントを発生させます。FROM VALUE #( ( %key = bankaccount-%key ) )
でイベントに渡すパラメータを設定しています。ここではkeyのみ指定しています。
METHOD save_modified.
LOOP AT update-bankaccount INTO DATA(bankaccount).
IF bankaccount-%control-Balance = if_abap_behv=>mk-on.
RAISE ENTITY EVENT ZR_BankAccountTP~balanceChanged
FROM VALUE #( ( %key = bankaccount-%key ) ).
ENDIF.
ENDLOOP.
ENDMETHOD.
Behavior Implementation全体のソースは以下にあります。
https://github.com/miyasuta/rap-local-event/blob/main/src/zbp_r_bankaccounttp.clas.locals_imp.abap
口座ランク管理クラス(イベントコンシュマー)
クラス定義
イベントコンシュマーのクラスは抽象クラスとして定義します。実装はLocal Typeで行います。
CLASS zcl_account_status_manager DEFINITION
PUBLIC
ABSTRACT
FINAL
FOR EVENTS OF ZR_BankAccountTP.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_account_status_manager IMPLEMENTATION.
ENDCLASS.
DB更新など、modifyフェーズで許可されていないオペレーションを実行する場合、cl_abap_tx=>save( )
のあとに書かないと実行時エラーが発生します。
Checking Transactional Consistency with the Controlled SAP LUW
CLASS lcl_abap_behv_event_handler DEFINITION INHERITING FROM cl_abap_behavior_event_handler.
PRIVATE SECTION.
METHODS update_rank_with_balance FOR ENTITY EVENT eventparam FOR BankAccount~balanceChanged.
ENDCLASS.
CLASS lcl_abap_behv_event_handler IMPLEMENTATION.
METHOD update_rank_with_balance.
DATA rank TYPE zaccountrank.
DATA rank_table TYPE STANDARD TABLE OF zaccountrank.
"enter save phase
cl_abap_tx=>save( ).
CHECK eventparam IS NOT INITIAL.
"Get balance
SELECT accountid, balanceinjpy
FROM ZI_BankAccount
FOR ALL ENTRIES IN @eventparam
WHERE AccountID = @eventparam-AccountID
INTO TABLE @DATA(accounts).
"Set rank according to the balance
LOOP AT accounts INTO DATA(account).
rank = VALUE #(
accountid = account-Accountid
rank = COND #( WHEN account-BalanceInJpy < 100 THEN 'A' "日本円は1/100で表現
WHEN account-BalanceInJpy < 1000 THEN 'B'
ELSE 'C' )
last_changed_by = cl_abap_context_info=>get_user_technical_name( ) ).
GET TIME STAMP FIELD rank-last_changed_at.
APPEND rank TO rank_table.
ENDLOOP.
"Update rank
MODIFY zaccountrank FROM TABLE @rank_table.
ENDMETHOD.
ENDCLASS.
テーブル定義
@EndUserText.label : 'Account Rank'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zaccountrank {
key client : abap.clnt not null;
key accountid : zde_account_id not null;
rank : abap.char(1);
last_changed_by : abp_locinst_lastchange_user;
last_changed_at : abp_lastchange_tstmpl;
}
CDS View定義
JPY換算した口座残高をもとにランクを判断するため、通貨換算を入れています。
@AbapCatalog.viewEnhancementCategory: [#NONE]
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Bank Account'
@Metadata.ignorePropagatedAnnotations: true
@ObjectModel.usageType:{
serviceQuality: #X,
sizeCategory: #S,
dataClass: #MIXED
}
define view entity ZI_BankAccount
as select from zbankaccount
{
key accountid as Accountid,
@Semantics.amount.currencyCode: 'Currency'
balance as Balance,
currency as Currency,
abap.cuky'JPY' as ReferenceCurrency,
@Semantics.amount.currencyCode: 'ReferenceCurrency'
currency_conversion( amount => balance,
source_currency => currency,
target_currency => abap.cuky'JPY',
exchange_rate_date => $session.system_date ) as BalanceInJpy,
created_by as CreatedBy,
created_at as CreatedAt,
last_changed_by as LastChangedBy,
local_last_changed_at as LocalLastChangedAt,
last_changed_at as LastChangedAt
}
動作確認
ローカルイベントの使いどころはあるか?
イベントの特徴は、イベント発生元がイベントの消費者を意識することがないところにあります。さらに、1つのイベントに対して複数のコンシュマーを定義することも可能です。
よって発生元としては「このイベントに興味があるプロセスが(今はなくても)あるかもしれない」という気持ちで定義しておき、コンシュマー側は必要になったタイミングで利用を開始する…という流れかと思います。
ただ、ABAPの世界で「イベントドリブンアーキテクチャ」というものが一般的ではないためかもしれませんが、上記のようなケースは実際にはあまりないかもしれません。通常はイベントの発生元が責任を持って後続処理を呼び出すと考えられます。
そんなわけで、今のところローカルイベントの使いどころが思いつきません。「こんな使い方があるんじゃないか」というご意見がありましたらコメントいただけると幸いです。