RAPでEvent driven side effectを実装する

## はじめに
この記事は、以下のブログおよびGitリポジトリを参考にRAP (ABAP Restful Application Programming Model) でEvent driven side effectを実装してみた記録です。

https://community.sap.com/t5/technology-blogs-by-sap/how-to-build-a-responsive-ui-for-a-rap-business-object-that-starts-an/ba-p/14027487

https://github.com/SAP-samples/abap-platform-bgpf-appl-log-events-side-effects/blob/main/README.md

## Event driven side effectとは
Side effectとは、Fiori elementsでアクションや項目の入力などの何らかの変化をトリガとして、他の項目またはエンティティをリロードさせるための仕組みです。RAPにはイベントを発生させてそれを外部に連携させる仕組みがあり、イベントをトリガにSide effectを発生させることを**Event driven side effect**と呼びます。バックエンドからのイベントはWebsocketを通じてクライアントに通知されます。（参考：[Event-Driven Side Effects | UI5 Demo Kit](https://sapui5.hana.ondemand.com/#/topic/27c9c3bad6eb4d99bc18a661fdb5e246)）
イベントトリガのSide effectの利点は、バックグラウンド処理など非同期の処理が完了した時点で画面を更新できることです。 


Behavior DefinitionでのSide effectの定義はオンプレミスではS/4HANA 2023から利用可能です。ただし、Event driven side effectは現時点ではクラウド環境のみで利用可能で、on-premiseでは2025から利用可能になるということです。

:::note 
Event driven side effectは以下のバージョンでサポートされます。
- SAPUI5: 1.132~
- OData: V4
:::

## サンプルシナリオ
冒頭のGitリポジトリのシナリオをそのまま使っています。品目の在庫を管理するFioriアプリケーションを作成します。

![RAP Side effects.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/119fe83f-a93e-4eee-890a-6ca816fea95d.png)

処理の流れは以下のようになります。
1. UIから「在庫更新」のアクションを実行
1. バックグラウンド処理を起動
1. バックグラウンドで呼ばれる処理の中でRAP BOを呼び在庫を更新する
1. RAP BO側では、在庫が更新されたことを確認するとイベントを起動する

バックグラウンド処理では、Background Processing Framework (BGPF)という技術が使われています。BGPFについては以下のブログで紹介しています。

https://qiita.com/tami/items/b8a54ec953c7ed9b4824

## 動作
Gitリポジトリを参考に実装してみた画面が以下です。ADTのプレビューから実行しています。

"Calculate Inventory"ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/66a9b142-ff5c-4974-a5eb-144049b5df11.png)

しばらく待つとQuantityが更新され"New data is available for this item"というメッセージが表示されます。バックグラウンド処理の中で5秒のWAITを入れているため、5秒くらいの待ち時間になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/bc6af463-74bc-455c-931f-7dbb53999afe.png)

:::note
Side effectが発生したときにどのような動作をさせるかは、manifest.jsonで設定可能です。デフォルトの動作は"Notification"で、上の例のように通知が表示されます。

```
{ 
     "sap.fe": { 
          "app": { 
               "sideEffectsEventsInteractionType": "Notification/Confirmation/None" 
          }
     } 
}
```
[Event-Driven Side Effects | UI5 Demo Kit](https://sapui5.hana.ondemand.com/#/topic/27c9c3bad6eb4d99bc18a661fdb5e246)
:::

ネットワークタブを見たところなぜかWebSocket接続は確認できず、アクション実行と結果更新用の`$batch`リクエストのみ表示されていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5fa47853-ae1c-4b6b-abad-bf6ca37a2857.png)

## 実装
以下では、RAPでEvent driven side effect実装でのポイントになる箇所を紹介します。全体のコードは冒頭で紹介したGitリポジトリを参照してください。

### Behavior Definition
[ソース](https://github.com/SAP-samples/abap-platform-bgpf-appl-log-events-side-effects/blob/main/src/zbgpfr_inventorytp_006.bdef.asbdef)

**Unmanaged saveの利用**
BGPFの処理は`save_modified`メソッドから実行する必要があるため、Unmanaged saveを利用します。

```abap
managed with unmanaged save implementation in class ZBGPFBP_R_InventoryTP_006 unique;
```

**アクションの定義**
```abap
  action reCalculateInventory;
```

**イベントの定義**
```abap
  event QuantityUpdated for side effects;
```

**Side effectの定義**
`QuantityUpdated`のイベントが発生したらQuantityを更新するというSide effectです。
```
  side effects
  {
    event QuantityUpdated affects field ( Quantity );
  }
```

### Behavior Projection
[ソース](https://github.com/SAP-samples/abap-platform-bgpf-appl-log-events-side-effects/blob/main/src/zbgpfc_inventorytp_006.bdef.asbdef)

**Side effectを使用**
```abap
projection;
strict ( 2 );
use draft;
use side effects;
```

**アクションとイベントを利用**
```abap
  use action reCalculateInventory;

  use event QuantityUpdated;
```

### Behavior Implementation
[ソース](https://github.com/SAP-samples/abap-platform-bgpf-appl-log-events-side-effects/blob/main/src/zbgpfbp_r_inventorytp_006.clas.locals_imp.abap)

**アクション**
`BgpfStatus`の項目を設定することにより、後続処理（save_modified）にバックグラウンド処理が必要なことを通知します。
```abap
  METHOD reCalculateInventory.

    READ ENTITIES OF ZBGPFR_InventoryTP_006 IN LOCAL MODE
         ENTITY Inventory
         FIELDS ( BgpfStatus )
         WITH CORRESPONDING #( keys )
       RESULT DATA(inventories).

    LOOP AT inventories ASSIGNING FIELD-SYMBOL(<inventory>) .
      CASE <inventory>-BgpfStatus.
        WHEN zbgpfcl_calc_inventory_006=>bgpf_state-unknown .
          <inventory>-BgpfStatus = zbgpfcl_calc_inventory_006=>bgpf_state-started_from_bo.
        WHEN zbgpfcl_calc_inventory_006=>bgpf_state-successful .
          <inventory>-BgpfStatus = zbgpfcl_calc_inventory_006=>bgpf_state-started_from_bo.
        WHEN OTHERS.
          "do nothing
      ENDCASE.
    ENDLOOP.

    MODIFY ENTITIES OF ZBGPFR_InventoryTP_006 IN LOCAL MODE
      ENTITY Inventory
        UPDATE FIELDS ( BgpfStatus )
        WITH CORRESPONDING #( inventories ).
  ENDMETHOD.
```

**save_modifiedメソッド**
アクションで`BgpfStatus`が設定された場合、BGPFによるバックグラウンド処理を起動します。バックグラウンド処理ではEMLを使用してRAP BOを更新するので、その際にも`save_modified`が呼ばれます。そこで数量が更新されていれば`QuantityUpdated`のイベントを発生させます。
```abap
  METHOD save_modified.

    DATA : inventories         TYPE STANDARD TABLE OF zbgpf_inven_006,
           inventory           TYPE                   zbgpf_inven_006,
           events_to_be_raised TYPE TABLE FOR EVENT ZBGPFR_InventoryTP_006~QuantityUpdated.

    DATA update_inventory_2 TYPE STRUCTURE FOR CHANGE zbgpfr_inventorytp_006\\inventory.
    DATA update_inventories_2 TYPE TABLE FOR CHANGE ZBGPFR_InventoryTP_006\\Inventory.


    IF update-inventory IS NOT INITIAL.
      LOOP AT update-inventory ASSIGNING FIELD-SYMBOL(<update_inventory>).

        "check if a process via bgpf shall be started
        IF     <update_inventory>-BgpfStatus          = zbgpfcl_calc_inventory_006=>bgpf_state-started_from_bo
           AND <update_inventory>-%control-BgpfStatus = if_abap_behv=>mk-on.


          TRY.
              "BGPFでバックグラウンド処理を開始
              DATA(bgpf_process_name) = zbgpfcl_calc_inventory_006=>run_via_bgpf( i_rap_bo_entity_key = <update_inventory>-%key ).

              update_inventory_2-%control-BgpgProcessName = if_abap_behv=>mk-on.
              update_inventory_2-bgpgprocessname = bgpf_process_name.
              update_inventory_2-%key = <update_inventory>-%key.
              APPEND update_inventory_2 TO update_inventories_2.

            CATCH cx_bgmc INTO DATA(bgpf_exception).
              "handle exception

              update_inventory_2-%control-remark = if_abap_behv=>mk-on.
              update_inventory_2-remark = bgpf_exception->get_text(  ).
              update_inventory_2-%key = <update_inventory>-%key.
              APPEND update_inventory_2 TO update_inventories_2.

          ENDTRY.

        ENDIF.

        "BGPFによって数量が更新されたらイベントを発生させる
        IF <update_inventory>-%control-Quantity = if_abap_behv=>mk-on.

          CLEAR events_to_be_raised.
          APPEND INITIAL LINE TO events_to_be_raised.
          events_to_be_raised[ 1 ] = CORRESPONDING #( <update_inventory> ).

          RAISE ENTITY EVENT zbgpfr_inventorytp_006~QuantityUpdated FROM events_to_be_raised.

        ENDIF.
      ENDLOOP.

    ENDIF.

    "以下、テーブル更新のための処理

  ENDMETHOD.
```

### BGPF用のクラス
[ソース](https://github.com/SAP-samples/abap-platform-bgpf-appl-log-events-side-effects/blob/main/src/zbgpfcl_calc_inventory_006.clas.abap)

**run_via_bgpf**
`save_modied`から呼ばれる処理です。バックグラウンド処理を起動します。
```abap
  METHOD run_via_bgpf.
    TRY.
        DATA(process_monitor) = cl_bgmc_process_factory=>get_default( )->create(
                                              )->set_name( |Calculate inventory data|
                                              )->set_operation(  NEW zbgpfcl_calc_inventory_006( i_rap_bo_entity_key = i_rap_bo_entity_key )
                                              )->save_for_execution( ).

        r_process_monitor_string = process_monitor->to_string( ).

      CATCH cx_bgmc INTO DATA(lx_bgmc).



    ENDTRY.
  ENDMETHOD.
  ```

**if_bgmc_op_single~execute**
バックグラウンドで実行される処理です。処理対象となった製品の在庫を+10してBOを更新します。

```abap
  METHOD if_bgmc_op_single~execute.

    DATA update TYPE TABLE FOR UPDATE ZBGPFR_InventoryTP_006\\Inventory.
    DATA update_line TYPE STRUCTURE FOR UPDATE ZBGPFR_InventoryTP_006\\Inventory .

    WAIT UP TO  wait_time_in_seconds SECONDS.

    READ ENTITIES OF ZBGPFR_InventoryTP_006
             ENTITY Inventory
             ALL FIELDS
             WITH VALUE #( ( %is_draft = if_abap_behv=>mk-off
                             %key-uuid = inventory_uuid
                            )  )
             RESULT DATA(entities)
             FAILED DATA(failed).

    IF entities IS NOT INITIAL.

      LOOP AT entities INTO DATA(entity).

        update_line-%is_draft = if_abap_behv=>mk-off.
        update_line-uuid = entity-uuid.
        update_line-Quantity = entity-Quantity + 10.

        APPEND update_line TO update.

      ENDLOOP.

      MODIFY ENTITIES OF ZBGPFR_InventoryTP_006
             ENTITY Inventory
               UPDATE FIELDS ( Quantity )
               WITH update
               REPORTED DATA(reported_ready)
               FAILED DATA(failed_ready).

    ENDIF.
  ENDMETHOD.
  ```

## まとめ
非同期な処理の結果を画面に返したいとき、Event driven side effectを使うことができます。BGPFを使えばバックグラウンドの処理も比較的簡単に実装できます。画面が非同期に更新されると「おおっ」となって楽しいのでもしよければ試してみてください。