RAPのアクションから更新系汎用モジュールを呼びたいとき

## はじめに
ABAP Restful Application Programming Model (RAP)のアクションから、更新系の汎用モジュールを呼び出したい場面があります。

しかし、単純にアクションのイベントハンドラから汎用モジュールを呼び出すと、このフェーズで更新が許可されていないため"**BEHAVIOR_ILLEGAL_STATEMENT**"という実行時エラーになります。

## CALL FUNCTION DESTINATION 'NONE'は？
SAP Communityなどでは、そういった場合に`CALL FUNCTION DESTINATION 'NONE'`が使えるという情報を目にします。

https://community.sap.com/t5/technology-q-a/how-to-update-amp-commit-in-the-action-without-using-create-update-and/qaq-p/14046778

ただ、BTP ABAP Environmentで試してみると"**RFC_ILLEGAL_CALL_ON_SAPCP**"という実行時エラーになりました。以下のブログのスレッドでは、「ABAP CloudではRFC宛先はシステム間の連携のみで許可され、`DESTINATION 'NONE'`は使えない」とありました。

https://community.sap.com/t5/enterprise-resource-planning-blogs-by-sap/restricted-abap-and-sap-s-4hana-on-premise/bc-p/13462013

よって、`CALL FUNCTION DESTINATION 'NONE'`はオンプレミス（言語バージョン：Standard ABAP）でのみ使える方法のようです。

## ABAP Cloudではどうするか
クラウド、オンプレ、言語バージョンを問わず使える方法とはどのようなものでしょうか。私が知っている方法は以下のようなものです。

1. アクションの中で、エンティティに「更新が必要である」ことを示すフラグを設定する
1. save_modifiedメソッドで更新系汎用モジュールを呼ぶ

この方法はSAPの[Andre Fisher氏](https://community.sap.com/t5/user/viewprofilepage/user-id/55)が作成した以下のリポジトリでも使用されています（汎用モジュールの呼び出しではなく、BGPFの実行ですが）。

https://github.com/SAP-samples/abap-platform-application-jobs

※save_modifiedメソッドを利用するには[Unmanaged Save](https://help.sap.com/docs/abap-cloud/abap-rap/unmanaged-save)または[Additional Save](https://help.sap.com/docs/abap-cloud/abap-rap/additional-save)を使用します。CRUD処理をフレームワークに任せる場合はAdditional Saveを使用してください。

## サンプルシナリオ
本のオーダーと在庫を管理するアプリケーションを作成します。
オーダーを入力した後、"sendOrder"というアクションを実行すると本の在庫が更新されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/fcd959a0-695a-4d27-951a-2531a7a27ad8.png)

以下では、「アクションから更新系汎用モジュールを呼び出す」という箇所に絞って実装を紹介します。

### CDS View
`UpdateRequired`という項目が、save_modifiedに更新があることを伝える役割をします。この項目はDBに保持しません。

```abap
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Book Order'
@Metadata.ignorePropagatedAnnotations: true
define root view entity ZI_BookOrder as select from zbook_order
{
    key order_id as OrderId,
    book_id as BookId,
    book_title as BookTitle,
    quantity as Quantity,
    order_date as OrderDate,
    creation_date as CreationDate,
    abap.char'' as UpdateRequired
}
```
### Behavior Definition
ここでのポイントは、`with unmanaged save`とすることでsave_modifiedメソッドを実装できるようにすることです。

※今回は登録処理も汎用モジュールで実施したためUnmanaged Saveを使用していますが、CRUD処理をフレームワークに任せる場合は`with additional save`としてください。

```abap
managed implementation in class zbp_i_bookorder unique;
strict ( 2 );
with draft;

define behavior for ZI_BookOrder //alias <alias_name>
lock master
total etag CreationDate
authorization master ( instance )
with unmanaged save
late numbering
draft table zbook_order_d
//etag master <field_name>
{
  create;
  update;
  delete;

  draft action Edit;
  draft action Activate optimized;
  draft action Discard;
  draft action Resume;
  draft determine action Prepare;

  action sendOrder;

  field ( readonly )
    OrderId,
    CreationDate,
    BookTitle;
}
```

### Behavior Implementation
#### アクションの実装
オーダー送信のアクションでは、はじめに在庫更新の汎用モジュールを呼んで在庫のチェックをします。save_modifiedメソッドではエラーを返せない（※）ため、アクションで事前チェックをしています。

※Saverクラスの継承元をデフォルトの`cl_abap_behavior_saver`から`cl_abap_behavior_saver_failed`に変えるとエラーを返すことができます（以下のブログ参照）。
https://community.sap.com/t5/technology-blogs-by-sap/exposing-bapi-as-odata-api-using-rap-facade/ba-p/13571926

```abap
  METHOD sendOrder.
    DATA modify_order TYPE TABLE FOR UPDATE ZI_BookOrder.

    READ ENTITIES OF ZI_BookOrder IN LOCAL MODE
      ENTITY ZI_BookOrder
      FIELDS ( BookId Quantity )
      WITH CORRESPONDING #( keys )
      RESULT DATA(orders).

    " check stock
    LOOP AT orders INTO DATA(order).
      CALL FUNCTION 'Z_F_UPDATE_BOOKSTOCK'
        EXPORTING
          i_book_id          = order-BookId
          i_quantity         = order-Quantity
          i_check_only       = abap_true
        EXCEPTIONS
          insufficient_stock = 1
          OTHERS             = 2.
      IF sy-subrc <> 0.
        APPEND VALUE #( %tky = order-%tky ) TO failed-zi_bookorder.
        APPEND VALUE #( %tky = order-%tky
                                           %msg = new_message(
                                                    id       = 'ZBOOK_ORDER'
                                                    number   = 001
                                                    severity = if_abap_behv_message=>severity-error
                                                    v1       = order-BookId
                                                  ) ) TO reported-zi_bookorder.
      ELSE.
        APPEND VALUE #( %tky = order-%tky
                        UpdateRequired = abap_true ) TO modify_order.

        APPEND VALUE #( %tky = order-%tky
                                           %msg = new_message(
                                                    id       = 'ZBOOK_ORDER'
                                                    number   = 002
                                                    severity = if_abap_behv_message=>severity-success
                                                    v1       = order-OrderId
                                                  ) ) TO reported-zi_bookorder.
      ENDIF.
    ENDLOOP.

    "notify the save_modified method that update is required
    MODIFY ENTITIES OF ZI_BookOrder IN LOCAL MODE
      ENTITY ZI_BookOrder
      UPDATE FIELDS ( UpdateRequired )
      WITH modify_order.

  ENDMETHOD.
  ```

#### save_modifiedの実装
アクションの中で`UpdateRequired`の項目を更新したため、save_modifiedメソッドの`update`パラメータに更新したレコードが入ってきます。`UpdateRequired`にフラグが立っていれば在庫更新の汎用モジュールを呼び出します。

```abap
  METHOD save_modified.
      " get book id
      READ ENTITIES OF ZI_BookOrder IN LOCAL MODE
        ENTITY ZI_BookOrder
        FIELDS ( BookId UpdateRequired )
        WITH VALUE #( for updated in update-zi_bookorder
                      ( %key = updated-%key ) )
        RESULT DATA(orders).

    LOOP AT orders INTO DATA(order).
      IF order-UpdateRequired = abap_true.
        CALL FUNCTION 'Z_F_UPDATE_BOOKSTOCK'
          EXPORTING
            i_book_id          = order-BookId
            i_quantity         = order-Quantity
            i_check_only       = abap_false
          EXCEPTIONS
            insufficient_stock = 1
            OTHERS             = 2.
        IF sy-subrc <> 0.
          "error should not happen at this point
        ENDIF.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.
```
### 在庫更新の汎用モジュール
以下が在庫更新の汎用モジュールの定義です。重要なことは、この中でコミットしないことです。コミットはRAPのフレームワークが行います。本来はテーブルロックをおこなうべきですが、ここでは省略しています。

```abap
FUNCTION z_f_update_bookstock
  IMPORTING
    VALUE(i_book_id) TYPE int4
    VALUE(i_quantity) TYPE int4
    VALUE(i_check_only) TYPE abap_boolean OPTIONAL
  EXCEPTIONS
    insufficient_stock.



  "Check stock
  "In a real life scenario, table lock is required
  SELECT SINGLE stock FROM zbook_stock
   WHERE book_id = @i_book_id
   INTO @DATA(current_stock).

  IF current_stock < i_quantity.
    RAISE insufficient_stock.
  ENDIF.

  IF i_check_only = abap_true.
    RETURN.
  ENDIF.

  DATA(new_stock) = current_stock - i_quantity.
  DATA(updated_at) = cl_abap_context_info=>get_system_date( ).

  UPDATE zbook_stock
    SET stock = @new_stock,
        updated_at = @updated_at
    WHERE book_id = @i_book_id.

ENDFUNCTION.
```

### 動作確認
Bookd ID"1"の在庫は98です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7fd479aa-60ba-42f9-9ba8-a9bc5f31b969.png)


"Send Order"アクションを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/079f72ee-3de9-4f9d-9dcd-32c2379e8e16.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/61657fa8-49db-4e64-8000-6d36c0d54ef0.png)

在庫が98になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/579b6444-125e-418f-8e2b-895529c8429f.png)

## おわりに
このパターンは汎用モジュールの呼び出しに限らず、アクションから以下のような処理を行いたいときに使うことができます。

- DB更新
- メール送信
- リモートサービスへの接続
- BGPFの呼び出し
- アプリケーションジョブのスケジュール