【ABAP】RAPで発生させたローカルイベントをABAPクラスで受け取る

## はじめに
RAPでイベントを発生させ、それをEvent Meshで利用するということがBTP ABAP Environmentのリリース2208から可能になっています。さらに、昨年のDevtoberfestの[動画](https://www.youtube.com/watch?v=yGWn8EKVpN0&t=1s)では、発生させたイベントをABAPシステム内で消費することができるようになったことが紹介されていました。
本記事では、RAPで発生させたイベントをABAPシステム内でローカルに利用する方法について検証しました。

## シナリオ
銀行口座を模した簡単なRAP BOを作成します。RAP BOには「預け入れ」「引き出し」のアクションがあります。アクションが実行されると残高が更新され、これをトリガとしてイベントが発生します。

イベントのコンシューマとして、口座のランクを管理するグローバルクラスを作成します。残高更新のイベントを受け取ると、残高に応じたランクを口座に設定します。

## 実装
### RAP BO（イベントプロデューサー）
#### テーブル定義
```abap
@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>`でイベントを定義します。
```abap
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`で定義しています。
```abap
@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のみ指定しています。
```abap
  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で行います。

```abap
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.
```

::: note warn
DB更新など、modifyフェーズで許可されていないオペレーションを実行する場合、`cl_abap_tx=>save( )`のあとに書かないと実行時エラーが発生します。
[Checking Transactional Consistency with the Controlled SAP LUW](https://help.sap.com/docs/abap-cloud/abap-concepts/checking-transactional-consistency-with-controlled-sap-luw)
:::
```abap
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.
```
#### テーブル定義
```abap
@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換算した口座残高をもとにランクを判断するため、通貨換算を入れています。
```abap
@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
}
```

## 動作確認
新規の口座を作成し、預入を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/fb4850cc-96d6-6078-05f3-282cc89e6592.png)

ランクテーブルにランクが設定されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/61cb99d6-0b94-0c92-1060-d24910fe5524.png)

## ローカルイベントの使いどころはあるか？
イベントの特徴は、イベント発生元がイベントの消費者を意識することがないところにあります。さらに、1つのイベントに対して複数のコンシュマーを定義することも可能です。
よって発生元としては「このイベントに興味があるプロセスが（今はなくても）あるかもしれない」という気持ちで定義しておき、コンシュマー側は必要になったタイミングで利用を開始する…という流れかと思います。
ただ、ABAPの世界で「イベントドリブンアーキテクチャ」というものが一般的ではないためかもしれませんが、上記のようなケースは実際にはあまりないかもしれません。通常はイベントの発生元が責任を持って後続処理を呼び出すと考えられます。
そんなわけで、今のところローカルイベントの使いどころが思いつきません。「こんな使い方があるんじゃないか」というご意見がありましたらコメントいただけると幸いです。

## 参考
- [Asynchronous code execution using ABAP Cloud | Blog](https://community.sap.com/t5/enterprise-resource-planning-blogs-by-sap/asynchronous-code-execution-using-abap-cloud/ba-p/13572520)
- [Business Event Consumption | Help Portal](https://help.sap.com/docs/abap-cloud/abap-rap/business-event-consumption)
- [Checking Transactional Consistency with the Controlled SAP LUW | Help Portal](https://help.sap.com/docs/abap-cloud/abap-concepts/checking-transactional-consistency-with-controlled-sap-luw)
- [What’s New in the ABAP RESTful Application Programming Model (RAP)? | Youtube](https://www.youtube.com/watch?v=yGWn8EKVpN0&t=1s)