【BOPF】Determination, Validationの実装

##はじめに
CDS + BOPFでFioriアプリケーションを作るシリーズの続きです。
今回は、BOPFに独自のロジックを実装します。

前回の記事
[【Fiori】CDS + BOPFでトランザクション実行](https://qiita.com/tami/items/b1458094d16dfdc87359)

##今回のゴール
- データを保存するときに登録日、更新日のタイムスタンプが設定されるようにする
- 入力値のチェックをする

BOPFのDetermination, Validationを使って上記を実現します。

##Determination
Determinationとは、ビジネスオブジェクトの変更、保存など、一定の条件が満たされたときに動くロジックです。Determinationを使ってデータを保存するときに登録日、更新日のタイムスタンプが設定されるようにします。

###Determinationを登録する
ADTで、BOPFのルートノードを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/912c8e1c-55c5-4728-8726-a77e36fdcc9a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8fff9bf4-3536-d686-5b49-84690451684c.png)
Neme, Descriptionを入力します。クラス名は自動提案されますが、変更することもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ac0d2cb8-8da0-4d44-c819-070e7a73a823.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0025598b-cc36-39a1-a493-12dc4ec133d6.png)
Determinationをどのタイミングで動かすのかを指定します。今回はデフォルト(Create, Update)のままとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/cbd96519-fb95-4a66-ed16-ca19b8a57850.png)
有効化すると、クラスが生成されます。

###生成されたクラスの構造
生成されたクラスをトランザクションSE24で見てみましょう。
/BOBF/CL_LIB_D_SUPERCL_SIMPLEというクラスを継承していることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e0c206d2-ac5b-393c-a325-1b06f74ac056.png)
/BOBF/IF_FRW_DETERMINATIONというインターフェースが実装されています（親クラスから継承）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f8d32884-b514-da9b-0f15-4351f5450be8.png)
メソッドは3つあり、いずれも親クラスからの継承ですが、EXECUTEというメソッドだけはこのクラスで再定義するようになっています。ここにDeterminationのロジックを書きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/416ff323-f811-a381-aa2f-b27622ce8a46.png)

###動作確認
EXECUTEメソッドのパラメータは以下のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ae53f259-c88f-d77c-63ba-57de10c7678e.png)

それぞれどのような値が入ってくるのか確認するためにデバッグしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/7dc24b7e-f117-79e0-c5df-bfa89db119e5.png)
IS_CTXにはBOのキー、ルートノードのキー、処理対象ノードのキーが入っています。今回はルートノードだけの構造なので、うしろ2つは同じ値です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c3b85f07-18c9-badd-cfb7-ab16cf285d82.png)
IT_KEYのテーブルにはIS_CTXとは異なるキーが入っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/47243750-902b-33db-93e9-730873fbf838.png)



###Determinationロジックを実装する
Determinationのロジックは、大きく分けて2ステップです。
1. 処理対象のデータを取得する
2. 更新する値をデータ更新用のインスタンスに渡す

####処理対象のデータを取得する
```abap
    DATA: lt_data TYPE ztmob58_c_students.

    io_read->retrieve(
      EXPORTING
        iv_node                 = is_ctx-node_key        " Node Name
        it_key                  = it_key                 " Key Table
      IMPORTING
        et_data                 = lt_data                " Data Return Structure
    ).
```
インポートパラメータで渡されたio_readというオブジェクトのメソッドを使って、処理対象のデータを取得します。et_dataで渡されるテーブルの型は、Node Overviewで確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/413458c8-45e2-ae84-55c6-96ead18881ed.png)

####更新する値を更新用のインスタンスに渡す
現在のタイムスタンプを取得し、登録日と更新日に設定します。

```abap
    GET TIME STAMP FIELD DATA(lv_timestamp). "タイムスタンプを取得

    LOOP AT lt_data REFERENCE INTO DATA(lo_data).

      IF lo_data->created_at IS INITIAL.
        lo_data->created_at = lv_timestamp.
      ENDIF.
      lo_data->changed_at = lv_timestamp.

      io_modify->update(
        EXPORTING
          iv_node           = is_ctx-node_key         " Node
          iv_key            = lo_data->key            " Key
          is_data           = lo_data                 " Data
      ).

   ENDLOOP.
```

処理対象データをループし（今回は1件だけですが）、タイムスタンプを設定し、io_modifyというオブジェクトのメソッドを使ってノードのデータを更新します。

注目すべきは、updateメソッドに渡すis_dataという項目です。この項目はオブジェクト型なので、構造ではなくオブジェクトを渡す必要があります。
このために、
`LOOP AT lt_data REFERENCE INTO DATA(lo_data).`
という書き方をして、内部テーブルの行をオブジェクトで参照するようにしています。

ちなみに、it_dataのkeyには何が入っているのか確認したところ、it_keyと同じ値でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/45931bbf-3e22-31d8-7edc-184c25c9f112.png)

###実行結果
####新規登録時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/2219e3e6-02f2-bbf2-27ec-640d641a6888.png)
登録日時、更新日時が設定されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e34c45a8-6566-a723-5593-2c64482cf0eb.png)

####変更時
上で登録したデータを変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/323ee9b6-9ce2-88e8-cade-208539cfc4a2.png)
変更日時だけが更新されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/95f67a33-0420-525e-8701-94407dc67f9e.png)

##Validation
Validationでは、特定のアクションを実行できるか (action validations)、またはノードのインスタンスの整合性が取れているか(consistency validations)をチェックします。Validationを使って誕生日が入力されているか、また年齢が6歳以上であることをチェックします。
※生徒情報なので、実行日時点で満6歳以上という想定

###Validationを登録する
Determinationと同じ要領で、Validation用のクラスを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a9929f02-3005-a8fe-ffee-ddfaa72c218f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/fe35e3c8-14b2-644d-bfba-a84608c6c731.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/86f9b804-b5be-4543-f82d-4c5a792da429.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/56347f9b-e5c4-5573-78b2-2ee4772923db.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9d9e4488-8f3b-975c-0522-05042636bbac.png)
有効化すると、クラスが生成されます。

###生成されたクラスの構造
Determinationとは異なり、/BOBF/CL_LIB_V_SUPERCL_SIMPLEというクラスを継承しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a4c92032-875c-8f8e-4511-e7b0bd8ff07d.png)
/BOBF/IF_FRW_VALIDATIONというインターフェースが実装されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/683d387a-6d29-5722-8ba2-8d660e5dd24a.png)
メソッドは3つあり、EXECUTEというメソッドを再定義するようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c60df1aa-63e6-7d1c-8b45-20ad00ff28bc.png)

###Validationロジックを実装する
Validationのロジックは、大きく分けて3ステップです。
1. 処理対象のデータを取得する
2. メッセージオブジェクトのインスタンスを登録する
3. データチェックし、エラーの場合はメッセージを出力する

####処理対象のデータを取得する
方法はDeterminationと同じです。

```abap
    DATA: lt_data TYPE ztmob58_c_students.

    io_read->retrieve(
      EXPORTING
        iv_node                 = is_ctx-node_key        " Node Name
        it_key                  = it_key                 " Key Table
      IMPORTING
        et_data                 = lt_data                " Data Return Structure
    ).
```

####メッセージオブジェクトのインスタンスを登録
チェック用のメソッドが呼ばれた時点で、エクスポートパラメータEO_MESSAGEはインスタンス化されていません。なので、インスタンスを登録する必要があります。

```abap
*   メッセージオブジェクトをインスタンス化
    IF eo_message IS NOT BOUND.
      eo_message = /bobf/cl_frw_factory=>get_message( ).
    ENDIF.
```

####データチェックし、エラーの場合はメッセージを出力する
まず、データ定義に2つの変数を追加します。

```abap
    DATA: lt_data TYPE ztmob58_c_students,
          lv_age  TYPE cmp_noyrs,
          ls_msg  TYPE symsg.
```
取得したデータをループする、という流れはDeterminationと同じです。骨格は以下のようになります。

```abap
*   データチェック
    LOOP AT lt_data REFERENCE INTO DATA(lo_data).
*     年齢が入力されているかチェック
      IF lo_data->birthday IS INITIAL.
*       メッセージ設定
      ELSE.
*       TODO: 6歳以上かチェック
          IF lv_age < 6.
*           メッセージ設定
          ENDIF.
      ENDIF.
*     メッセージを出力
      eo_message->add_message(
        EXPORTING
          is_msg       = ls_msg
          iv_node      = is_ctx-node_key
          iv_key       = lo_data->key
      ).
*     エラーになったキーを追加
      APPEND VALUE #( key = lo_data->key ) TO et_failed_key.
    ENDLOOP.
```
メッセージ出力には、eo_messageのadd_messageというメソッドを使います。重要なのはet_failed_keyにエラーになったキーを追加することで、これがないとエラーを出してもそのまま更新に進んでしまいます。

最終的には以下のようなコードになります。

```abap
*   データチェック
    LOOP AT lt_data REFERENCE INTO DATA(lo_data).
*     年齢が入力されているかチェック
      IF lo_data->birthday IS INITIAL.
        ls_msg = VALUE #( msgid = 'ZMOB58' msgno = 000 msgty = 'E').
      ELSE.
*     6歳以上かチェック
        CALL FUNCTION 'HRCM_TIME_PERIOD_CALCULATE'
          EXPORTING
            begda               = lo_data->birthday
            endda               = sy-datum
         IMPORTING
           noyrs               = lv_age
         EXCEPTIONS
           invalid_dates       = 1
           overflow            = 2
           OTHERS              = 3
                  .

          IF lv_age < 6.
            ls_msg = VALUE #( msgid = 'ZMOB58' msgno = 001 msgty = 'E').
          ENDIF.
      ENDIF.
*     メッセージを出力
      IF ls_msg IS NOT INITIAL.
        eo_message->add_message(
          EXPORTING
            is_msg       = ls_msg
            iv_node      = is_ctx-node_key
            iv_key       = lo_data->key
        ).

        APPEND VALUE #( key = lo_data->key ) TO et_failed_key.
      ENDIF.
    ENDLOOP.
```

###実行結果
####誕生日が未入力のとき
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/17000e7b-0cdf-6294-bf1c-f7887d1bb741.png)
必須チェックは別のやり方でもできそうな気がしますが、今回はVaidationの動作確認ということで。

####年齢が6歳未満のとき
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3b82d146-b733-f649-95cf-52ffa07ddd49.png)

##まとめ
BOPFに独自のロジックを実装しました。

##今後やってみたいこと

- Actionの実装
- 階層を持つCDSビューをBOPFで更新