【RAP】Managed BOでUnmanaged Saveを実装

## はじめに
この記事では、RAP(ABAP RESTful Application Programming Model)のManaged BOを使用した、Unmanaged Saveを実装します。

シリーズの先頭はこちら☞ [【RAP】ADTのRAP Generatorを使ってみる](https://qiita.com/tami/items/a9f3d2d4afb28d53919b#%E7%B6%9A%E7%B7%A8)

### Managed / Unmanaged BOとは
RAPでビジネスオブジェクト(BO)を扱う流れは、保存前までの**Interaction Phase**と、ユーザが保存をトリガした後の**Save Sequence**に分けられます。Interaction Phaseでは、ユーザがBOを呼んでデータを読み込んだり変更をしたりします。BOへの変更はTransactional bufferにためられます。保存がトリガされた後はSave Sequenceによってデータがデータベースに保存されます。

**Managed BO**とは、Interaction PhaseおよびSave Sequenceの処理がフレームワークによって提供されるタイプのBOです。これは新規の開発で使用されます。
これに対し**Unmanaged BO**とは、Interaction Phase、およびSave Sequenceの中の処理を自前で実装するタイプのBOです。これは、再利用できるコードが既にあり、RAPにつなぎたい場合に使用されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7449b41f-d885-bd17-ae2b-e8c541c7d194.png)

### Unmanaged Saveとは
さて、Unmanaged BOで何が一番手間かというと、それはInteraction PhaseでのTransactional bufferとのやり取りだと思います。CRUD処理は再利用できるクラスや汎用モジュールがあるとしても、Transactional bufferはRAP固有の考え方なので既存のコードはない場合がほとんどでしょう。そこで登場したのが**[managed BO with unmanaged save](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/2c2bec28fff6475f922f9f876491826d.html)**（以下、Unmanagd Save）という方法です。
Unmanaged Saveを使うと、Save Sequenceのsave（実際にDBとやりとりする部分）だけを自前で実装し、その他の部分はフレームワークに任せることができます。

## Unmanaged Saveの実装
[RAP Generatorで生成](https://qiita.com/tami/items/a9f3d2d4afb28d53919b)したビジネスオブジェクトをUnmanaged Saveに変えます。
ステップは以下です。

1. CUD用クラスの登録
1. Behavior Definitionの設定
1. Saverクラスの実装

### 1. CUD用クラスの登録
PersonエンティティのCUD（Create, Update, Delete）用クラスを作成します。
事前にメソッドの受け渡しに使うテーブルタイプを更新対象のテーブルと同じ型で登録しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9bba3621-4144-95bd-ee75-a693cee3f1c7.png)

以下がクラスの実装です。受け取ったデータを使ってデータを更新するだけのごくシンプルな処理です。

```abap
CLASS zc_save_person DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    METHODS create
      IMPORTING persons TYPE ztt_person.
    METHODS update
      IMPORTING persons TYPE ztt_person.
    METHODS delete
      IMPORTING persons TYPE ztt_person.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.



CLASS zc_save_person IMPLEMENTATION.
  METHOD create.
    INSERT zperson_m FROM TABLE @persons.
  ENDMETHOD.

  METHOD update.
    UPDATE zperson_m FROM TABLE @persons.
  ENDMETHOD.

  METHOD delete.
    DELETE zperson_m FROM TABLE @persons.
  ENDMETHOD.
ENDCLASS.
```

### 2. Behavior Definitionの設定
Unmanaged Saveに変えるために、Behavior Definitionを以下のように変更します。
保存の方法としての`persistent table ...`をコメントアウトし、代わりに`with unmanaged save`を指定します。

```abap
managed implementation in class zbp_person_m unique;
strict;
with draft;

define behavior for ZI_PERSON_M alias Person
//persistent table zperson_m コメントアウト
with unmanaged save
```
unmanagedの部分に警告が出るのでCtrl + 1でQuick Fixを開き、"Add required method save_modified..."をダブルクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/11f40fae-b110-2655-4db8-7f9a196c013f.png)

Behavior ImplementationクラスのLocal Typesに以下の定義が追加されます。

```abap
CLASS lsc_zi_person_m DEFINITION INHERITING FROM cl_abap_behavior_saver.

  PROTECTED SECTION.

    METHODS save_modified REDEFINITION.

ENDCLASS.

CLASS lsc_zi_person_m IMPLEMENTATION.

  METHOD save_modified.
  ENDMETHOD.

ENDCLASS.
```

### 3. Saverクラスの実装
Saver クラスのメソッド`save_modified`を実装します。処理の大枠は以下のようになります。

```abap
  METHOD save_modified.
    IF create-person IS NOT INITIAL.
      "登録処理
    ENDIF.

    IF update-person IS NOT INITIAL.
      "更新処理
    ENDIF.

    IF delete-person IS NOT INITIAL.
      "削除処理
    ENDIF.

  ENDMETHOD.
```

#### 3.1. 登録処理
登録時は、DB登録用の構造に`create-person`で入ってきた構造をマッピングし、create用のメソッドに渡します。
このケースのように、DBの項目名とCDS Viewの項目名が違う場合はマッピングが必要になります。

```abap
    DATA person_db TYPE STANDARD TABLE OF zperson_m.
    DATA(lo_save_person) = NEW zc_save_person(  ).

    IF create-person IS NOT INITIAL.
      "項目マッピング
      person_db = CORRESPONDING #( create-person mapping
                                     person_uuid = PersonUUID
                                     first_name  = FirstName
                                     last_name   = LastName
                                     email       = Email
                                     birthday    = Birthday
                                     status      = Status
                                     created_by  = CreatedBy
                                     created_at  = CreatedAt
                                     last_changed_by       = LastChangedBy
                                     local_last_changed_at = LocalLastChangedAt
                                     last_changed_at       = LastChangedAt ).

      lo_save_person->create( person_db ).

    ENDIF.
```

#### 3.2. 更新処理
更新時はupdate-person-%controlの構造で、更新された項目に01が設定されます。以下はデバッグで見たupdate-personの中身です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/99570778-c32d-74b0-a4bc-d4c6f9ba55b0.png)
処理の流れは以下のようになります。
①update-personからキーを取得
②データベースから変更前の全項目を取得
③各項目について%controlの値をチェックし、更新されていたらupdate-entityの項目で上書き
④update用メソッドを呼ぶ

上の画面の通り、update-personには更新された項目も更新されていない項目も入ってきているので、%controlと比較せず全項目update-personから上書きしてもよいのではとも思いますが、[Help](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/bb7122a430f94f59b36081414ffd8a95.html)のサンプルソースのやり方に従いました。

```abap
    IF update-person IS NOT INITIAL.
      person_db = CORRESPONDING #( update-person mapping person_uuid = PersonUUID ).

      " データベースから全項目を取得
      SELECT * FROM zperson_m
        FOR ALL ENTRIES IN @person_db
        WHERE person_uuid = @person_db-person_uuid
        INTO TABLE @person_db.

      "変更された項目で上書きする
      LOOP AT update-person ASSIGNING FIELD-SYMBOL(<u_person>).
        ASSIGN person_db[ person_uuid = <u_person>-PersonUUID ]
        TO FIELD-SYMBOL(<person_db>).

        IF <u_person>-%control-FirstName = if_abap_behv=>mk-on.
          <person_db>-first_name = <u_person>-FirstName.
        ENDIF.

        IF <u_person>-%control-LastName = if_abap_behv=>mk-on.
          <person_db>-last_name = <u_person>-LastName.
        ENDIF.

        IF <u_person>-%control-Email = if_abap_behv=>mk-on.
          <person_db>-email = <u_person>-Email.
        ENDIF.

        IF <u_person>-%control-Birthday = if_abap_behv=>mk-on.
          <person_db>-birthday = <u_person>-Birthday.
        ENDIF.

        IF <u_person>-%control-Status = if_abap_behv=>mk-on.
          <person_db>-status = <u_person>-Status.
        ENDIF.

        IF <u_person>-%control-LastChangedBy = if_abap_behv=>mk-on.
          <person_db>-last_changed_by = <u_person>-LastChangedBy.
        ENDIF.

        IF <u_person>-%control-LocalLastChangedAt = if_abap_behv=>mk-on.
          <person_db>-local_last_changed_at = <u_person>-LocalLastChangedAt.
        ENDIF.

        IF <u_person>-%control-LastChangedAt = if_abap_behv=>mk-on.
          <person_db>-last_changed_at = <u_person>-LastChangedAt.
        ENDIF.
      ENDLOOP.

      lo_save_person->update( person_db ).

    ENDIF.
```

#### 3.3. 削除処理
削除処理はシンプルに、delete-personからキーのみをマッピングし、delete用のメソッドに渡します。

```abap
    IF delete-person IS NOT INITIAL.
      "項目マッピング（keyのみ）
      person_db = CORRESPONDING #( delete-person mapping person_uuid = PersonUUID ).

      lo_save_person->delete( person_db ).
    ENDIF.
```

## 動作確認
### CUD処理
登録、更新、削除は以前と同じように動きます。以下は更新前後でのテーブルの内容を確認した結果です。

更新前
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/103e2d44-b369-e2de-c463-4e4f9cb45e99.png)

Last Nameを更新した後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/83c3cf1f-c716-d800-f240-e3753252af83.png)

変更した項目と、タイムスタンプ項目が更新されました。

### アクション
アクションを使って項目（ステータス）を更新したあとはどうでしょうか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4df01789-19cb-bbb3-2e36-cf0d32f332dc.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/50b88ce8-83f6-1ac9-964c-ec61b7e6df66.png)
アクションで更新した項目がDBに反映され、タイムスタンプも更新されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d66e4c0c-4647-b561-5b91-35005a1b2ee5.png)

## エラーのときは？
保存処理でエラーが起こった場合はどうすればよいでしょうか。なお、Unmanaged SaveでもValidationで定義したロジックは動くので、ここでのエラーはDBが更新できないなど特殊なケースに限られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/abeaaf9e-0951-59f3-0133-10a9007421d4.png)
[SAP Help: Integrating Unmanaged Save in Managed Business Objects](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/2c2bec28fff6475f922f9f876491826d.html)より引用

Saverクラスのスーパークラスである`cl_abap_behavior_saver`を見てみると、メソッド`save_modified`はreportedとfailedをchangingパラメータに持っているので[Validation](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/5cde3ef8a6234c37a304afbd4f22c092.html?q=validation)と同じように処理すればよさそうです。

```abap
  methods SAVE_MODIFIED
    importing
      !CREATE type DATA
      !UPDATE type DATA
      !DELETE type DATA
    changing
      !REPORTED type DATA optional
      !FAILED type DATA optional .
```

ところが、コンパイラで「"failed"というパラメータがない」というエラーになってしまいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/618309b3-a317-0bb3-e190-298d1062fb64.png)

そこでfailedの設定はあきらめ、reportedだけ設定します。

```abap
    IF update-person IS NOT INITIAL.
      "エラーにする
*        "Set failed keys
*        APPEND VALUE #( %is_draft = if_abap_behv=>mk-off
*                        personuuid = update-person[ 1 ]-PersonUUID )
*               TO failed-person.    
               
      "Set message
      APPEND VALUE #( %key = update-person[ 1 ]-%key
                      %is_draft = if_abap_behv=>mk-off
                      personuuid = update-person[ 1 ]-PersonUUID
                      %element-FirstName = if_abap_behv=>mk-on
                      %msg = new_message(
                               id       = 'ZRAP_MSG_YASU2122_2'
                               number   = 002
                               severity = if_abap_behv_message=>severity-error
                             ) )
             TO reported-person.

      RETURN.
```
実行するとエラーメッセージは出ますが、保存が成功したようなMessage Toastも出るという状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/fd7efcd8-6bf1-41ec-7559-fca6a718f563.png)

ここまで試した限りでは、Saverクラスの中でカスタムのエラーを設定するのは難しいという結論です。もともとビジネスロジックによるチェックはValidationで行うべきものなので、Saverクラスの中でエラーを出すことは基本的にないのかもしれません。

しかし、そうだとすると既存の保存ロジックって本当に使い回せるの？という疑問がわきます。既存ロジック（たとえば汎用モジュール）には、チェック処理も入っているものが多いのではないでしょうか。
シンプルにDBに保存するだけの既存ロジックがある場合にしかUnmanaged Saveを使えないのか、あるいはチェックの部分だけ切り出してValidationで事前にかけておくのか（保存時にもチェックはかかるがそこは100%パスする状態にしておく）。ちょっとモヤっとしてこの回は終わります。

2022/1/11更新
SAP Communityに[質問](https://answers.sap.com/questions/13560339/rap-managed-bo-with-unmanaged-save-parameter-faile.html)したところ、以下の回答をいただきました。

[Naveen Kumar Chikkanna](https://people.sap.com/chikkanna)

- `save_modified`は失敗してはならない
- トランザクションを止める必要があるときはショートダンプを発生させるしかない

[Andre Fischer](https://people.sap.com/andre.fischer)

- 入力されたデータが正しいかどうかはVaidationまたはCheckBeforeSaveを使い、ショートダンプの発生を防止する必要がある
- REPORTEDはInfo/Warningメッセージを返すために使う