【RAP】Determination, Validationを追加する

##はじめに
この記事ではRAP(ABAP RESTful Application Programming Model)のDetermination, Validationを実装します。

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

**用語の説明**

|用語  |説明  |ヘルプ（さらに詳しく） |
|---|---|---|
|Determination  |ビジネスオブジェクトの項目に値を代入するためのもの  |[link](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/6edb0438d3e14d18b3c403c406fbe209.html)|
|Validation  |ビジネスオブジェクトの整合性をチェックするためのもの  |[link](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/171e26c36cca42699976887b4c8a83bf.html)|

##シナリオ
[前回](https://qiita.com/tami/items/a9f3d2d4afb28d53919b)の記事で生成したビジネスオブジェクトに、以下の機能を追加します。

1. Determinationを使ってEmailを自動設定
1. Validationを使って必須チェック


今回設定するオブジェクトの関係は以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f2832eeb-860f-4193-16d9-7742df41a1ba.png)

###1. Determinationを使ってEmailを自動設定
FirstName, LastNameに入力された値を使ってEmailを自動設定します。

####1.1. Behavior Definitionの設定
Behavior Definitionに以下のコードを追加します。

```abap
{
  field ( readonly )
   PersonUUID,
   CreatedAt,
   CreatedBy,
   LastChangedAt,
   LocalLastChangedAt,
   LastChangedBy;

  field ( numbering : managed )
   PersonUUID;
   
  determination set_email on save {create;} //追加
```

**Determinationの構文**
Determinationの構文は以下のようになります。

```abap
determination DetName {on modify { CUD1; CUD2; ... }
                               | { field Field1, Field2, ... ; }}
                     |{on save   { CUD1; CUD2; ... }
                               | { field Field1, Field2, ... ; }}
```
参考：[CDS BDL - determinations](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenbdl_determinations.htm)

Determinationが動くタイミングは2つあり、一つはmodify（エンティティが変更される都度）、もう一つはsave（保存時）です。定義する際はどちらかを選択します。
CUD1; CUD2; ...では起動条件となるオペレーション（create, update, delete）を指定し、field Field1, Field2, ...ではどの項目が変更された場合に起動するかを指定します。オペレーションと項目はどちらかの一方の指定でも、両方指定してもよいです。両方指定する場合は以下のような書き方になります。

```abap
  determination set_email on save {create; field FirstName, LastName;}
```

今回はシンプルに、「登録時に、保存したタイミングで動かす」こととしました。

####1.2. Behavior Implementationの実装
追加したDeterminationの上でCtrl + 1（Quick Fix）を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/517866e6-e964-ff9e-72cd-27f0a6212146.png)
すると、"Add method to determination ..."というオプションが出てくるのでそれを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/135e1aea-beca-d4f9-38fd-778ffe07e886.png)
以下のようにBehevior ImplementationクラスのLocal Typesにメソッドが作られます。

```abap
CLASS LCL_HANDLER DEFINITION INHERITING FROM CL_ABAP_BEHAVIOR_HANDLER.
  PRIVATE SECTION.
    METHODS:
      GET_GLOBAL_AUTHORIZATIONS FOR GLOBAL AUTHORIZATION
        IMPORTING
           REQUEST requested_authorizations FOR Person
        RESULT result,
      set_email FOR DETERMINE ON SAVE
            IMPORTING keys FOR Person~set_email.
ENDCLASS.

CLASS LCL_HANDLER IMPLEMENTATION.
  METHOD GET_GLOBAL_AUTHORIZATIONS.
  ENDMETHOD.
  METHOD set_email.
  ENDMETHOD.

ENDCLASS.
```
`set_email`を以下のように実装します。

**1. 処理対象インスタンスの取得**
まず、メソッドのインポートパラメータkeysを使い、処理対象のインスタンスからFirstName、LastNameを取得します。keys、および結果のpersonsはテーブル型ですが、実際には1行だけ取得されます。

```abap
    "Get firsName, lastName from instance
    READ ENTITIES OF zi_person_m IN LOCAL MODE
    ENTITY Person
      FIELDS ( FirstName LastName )
      WITH CORRESPONDING #( keys )
      RESULT DATA(persons).
```
ドキュメント：[READ ENTITY, ENTITIES](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenbdl_action.htm)

なお、`IN LOCAL MODE`というのは、権限チェックなどの制約を回避して何でも取ってこられるようにするためのおまじないです。
ドキュメント：[IN LOCAL MODE](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abapread_entity_entities.htm)

以下がデバッグで取得したkeys、およびpersonsです。この中身は、ドラフトの使用有無やDeterminationが動くタイミングによっても変わってくるので、一度デバッグで確認しておくとよいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7c633f12-1d45-d3fb-dd9c-27c5b567e861.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/386f0410-ba5d-71e5-5ebc-ff2349d3382b.png)

**2. Emailの編集**
FirstName、LastNameをつなげてEmailを編集します。

```abap
    DATA(email) = |{ persons[ 1 ]-FirstName }_{ persons[ 1 ]-LastName }@com|.
    email = to_lower( email ).
```

**3. Emailを設定**
最後に`MODIFY ENTITIES`命令を使いインスタンスを更新します。
MODIFY ENTITIESから返ってくるテーブルupdate_reportedには、更新が失敗した場合にエラーメッセージが入ってきます。それを、set_emailのアウトプットであるreportedに設定します。reportedはメソッドの定義にも出てこないので、どこから来たの？という感じですね。

```abap
    "Set email
    MODIFY ENTITIES OF zi_person_m IN LOCAL MODE
    ENTITY Person
      UPDATE
        FIELDS ( Email )
        WITH VALUE #( FOR person IN persons (
                         %tky = person-%tky
*                        PersonUUID = person-PersonUUID
*                        %is_draft = person-%is_draft
                        email = email
           ) )
      REPORTED DATA(update_reported).

    reported = CORRESPONDING #( DEEP update_reported ).
```
ドキュメント：[MODIFY ENTITY, ENTITIES](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abapmodify_entity_entities.htm)

**%tkyについて**
`WITH VALUE`の中で更新対象のインスタンスを指定する方法に2つあり、一つは%tkyにエンティティの%tkyをそのまま指定する方法、もう一つは上記でコメントアウトしている部分ですが、キーを直接指定する方法です。ドラフトがあるエンティティは%is_draftも含めてキーとなります。
%tkyはデバッグで見るとpersonsの構造の中にはないのですが、不思議とちゃんと存在していて使うことができます。
%tkyを使うとドラフトかどうかも含めすべてのキーを自動的に指定できるので、こちらの方法がお勧めです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e548c2b6-6d10-1f1e-0e98-81d37d533d05.png)


####1.3. 動作確認
First Name、Last Nameを入力して"Create"を押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d3656d38-bef5-7761-a643-ed25b6c63d89.png)
Emailが設定されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2cf52bba-6164-e30f-9f1e-e2e0f98b9dcc.png)

###2. 1. Validationを使って必須チェック
Emailを設定するために必要なFirstName, LastNameを必須項目にします。

####2.1. Behavior Definitionの設定
Behavior Definitionに以下のコードを追加します。

```abap
  field ( readonly )
   PersonUUID,
   CreatedAt,
   CreatedBy,
   LastChangedAt,
   LocalLastChangedAt,
   LastChangedBy,
   Email; //追加

  field ( numbering : managed )
   PersonUUID;

  field ( mandatory ) //追加
  FirstName,
  LastName;

  determination set_email on save {create; }
  validation mandatory_check on save {create; update; } //追加
```


**項目制御**
`field ( readonly )`では、照会モードにする項目を指定します。EmailはDeterminationで設定するためreadonlyにしました。
`field ( mandatory )`では、必須にしたい項目を指定します。この設定により画面に必須マークが出ますが、このままではブランクのまま保存できてしまいます。
※項目制御の追加後、ブラウザをリフレッシュしても反映されなかったのですが、Service Bindingから立ち上げ直したところ反映されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/cfa13247-7afc-525c-14f2-f31252e3d737.png)

**Validationの構文**
Validationの構文はDeterminationと基本的な考え方は同じですが、動くのは`save`のタイミングだけです。

```abap
validation ValName on save { CUD1; CUD2; ... }
                         | { field Field1, Field2, ... ; }
```
参考：[CDS BDL - validations](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenbdl_validations.htm)

####2.3. メッセージクラスの登録
エラーメッセージを出力するため、メッセージクラスを登録します。パッケージの上で右クリックし、New>Other ABAP Repository Objectを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f8adf0dc-869f-50e9-7d3f-cc31ade5182e.png)
Message Classを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e873d516-6437-c8d7-ac16-fc2a444d0450.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/17652398-10a4-cc2a-53a5-cad5651d237f.png)
登録が完了すると、以下の画面が表示されます。ここからメッセージを登録していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/00921b2d-cc05-d83e-9e2f-7b6e9b9b1448.png)
今回は以下の1件を登録しました。
**Number**: 001
**Short Text**: Enter &1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ab4823ee-bfaa-fc1b-93ce-85b255347c75.png)

####2.2. Behavior Implementationの実装
Quick Fixでメソッドmandatory_checkをBehavior Implementationに追加し、以下のように実装します。

**1. 処理対象インスタンスの取得**
Determinationのときと同様に、処理対象のインスタンスを取得します。

```abap
    "Get Mandatory Fields
    READ ENTITIES OF zi_person_m IN LOCAL MODE
    ENTITY Person
      FIELDS ( FirstName LastName )
      WITH CORRESPONDING #( keys )
      RESULT DATA(persons).
```

**2. チェックを実行**
取得したインスタンスをループして（実際には一行ですが）、チェックを実行します。
チェックでエラーの場合にやることは2つです。
①failedという構造にエラーのあったインスタンスのキーを設定する
②reportedという構造にエラーメッセージを格納する

```abap
    "Do check
    LOOP AT persons INTO DATA(person).
      IF person-FirstName IS INITIAL.

        "Set failed keys
        APPEND VALUE #( %tky = person-%tky )
               TO failed-person.

        "Set message
        APPEND VALUE #( %tky = person-%tky
                        %element-FirstName = if_abap_behv=>mk-on
                        %msg = new_message(
                                 id       = 'ZRAP_MSG_YASU2122_2'
                                 number   = 001
                                 severity = if_abap_behv_message=>severity-error
                                 v1       = 'First Name'
                               ) )
               TO reported-person.
      ENDIF.

      IF person-LastName IS INITIAL.
        "省略
      ENDIF.
    ENDLOOP.
```
reported-personを返すときに、以下の設定をすることにより、UI上でエラーを出したい項目に印がつきます。

```abap
%element-FirstName = if_abap_behv=>mk-on
```

####2.4. 動作確認
First Name、Last Nameのいずれかを未入力のまま"Create"を押すと、エラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a2c179f7-d77c-121b-27ee-458b7cd81595.png)

####2.5. わからなかったこと - State Messageについて
RAPで出すメッセージの種類は2つあり、一つが[Transition Message](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/ea190836e94e49af956212eb67e0a3b8.html)、もう一つが[State Message](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/c69301e17aab41e2b24b4ef957432568.html)です。
ヘルプによれば、Transition Messageとはビジネスオブジェクトの（中身の）状態によらず出るメッセージのことで、例えば「伝票が他のユーザにロックされています」といったメッセージが該当します。これに対しState Messageとはビジネスオブジェクトそのものの状態によって出るメッセージで、バリデーションエラーなどはこれに相当します。よって、今回のValidationではState Messageを出すべきと考えられます。
State Messageとするには、reportedにメッセージを渡すときに以下のように%state_areaを指定します。

```abap
        APPEND VALUE #( %tky = person-%tky
                        %element-FirstName = if_abap_behv=>mk-on
                        %state_area = 'mandatory_check'
                        %msg = new_message(
                                 id       = 'ZRAP_MSG_YASU2122_2'
                                 number   = 001
                                 severity = if_abap_behv_message=>severity-error
                                 v1       = 'First Name'
                               ) )
               TO reported-person.
```
ところが、このように設定したところ、Validationで以下のエラーが発生しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/fea6016f-d743-49f6-5f85-768098b13457.png)

一方で、[こちら](https://developers.sap.com/tutorials/abap-environment-behavior-action.html#a3eaba05-ab3e-432a-91db-1af2222e94ed)のチュートリアルでは、%state_areaは指定していませんでした。

```abap
        APPEND VALUE #( %key = ls_travel_result-%key
                        %msg = new_message( id       = /dmo/cx_flight_legacy=>begin_date_before_system_date-msgid
                                            number   = /dmo/cx_flight_legacy=>begin_date_before_system_date-msgno
                                            severity = if_abap_behv_message=>severity-error )
                        %element-begin_date = if_abap_behv=>mk-on
                        %element-end_date   = if_abap_behv=>mk-on ) TO reported-travel.
```
この件は現在SAP Communityに[問い合わせ中](https://answers.sap.com/questions/13550315/rap-using-state-message-causes-dump.html)です。