【OData】Batch Processの実装：ODataからSAPUI5アプリまで

##Batch Processとは
SAPUI5におけるBatch Processとは、ODataに対する複数のHTTPリクエストを一つにまとめて処理することです。まとめてリクエストを受け取ると、バックエンド側でも処理をまとめて実行することができ、レスポンスの向上が期待できます。

Batch ProcessはLUW（トランザクション）の単位となり、ここに含まれるリクエストは全て成功か、すべて失敗のいずれかの結果となります。このためヘッダ、明細のように関連を持つエンティティを同時に更新するときに使うことができます。

なお、注意点としてBatch Processの中でCOMMIT WORKを実行してはいけないこととなっています。実行した場合、ショートダンプが発生します。

##やりたいこと
今回は、ヘッダと明細の登録を1つのBatch Processの中で行います。同じことはDeep insertでもできますが、今回はBatch Processを使ってみようと思います。
参考：[【SAPUI5】Deep Insertを使ったデータの登録(1)](https://qiita.com/tami/items/98fdef77d7645429d402)

##前提
ODataサービスはV2を使用します。
OData V4では、UI5側のメソッドが変わるのでご注意ください。
参考：[ODataV4のBatch Control](https://sapui5.hana.ondemand.com/#/topic/74142a38e3d4467c8d6a70b28764048f)

##Batch Processを行うには
Batch Processを行うためには、OData側、UI5側で以下の設定が必要です。
###OData側
####①DPC_EXTクラスで3つのメソッドを再定義する

- /IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_BEGIN
- /IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_END
- /IWBEP/IF_MGW_APPL_SRV_RUNTIME~CHANGESET_PROCESS

注意点として、/IWBEP/IF_MGW_CORE_SRV_RUNTIMEに同じメソッドが存在します。最初、誤ってこちらのメソッドを実装してしまい思ったように動きませんでした。

####②CHANGESET_BEGINメソッドの中で、CV_DEFER_MODEを設定する
CV_DEFER_MODEとは、リクエストをまとめて実行するかどうかを判断するフラグです。フラグをONにすると、リクエストはCHANGESET_PROCESSに送られ、そこで一括処理されます。フラグをOFFにすると、通常のCREATE、UPDATE、DELETEメソッドに入ります。

参考：[Details about some new features in SAP Gateway 2.0 SP09](https://archive.sap.com/documents/docs/DOC-57113)

###UI5側
####①ODataの設定で、バッチリクエストが有効になっていることを確認する
デフォルトモデルの場合、manifest.jsonにバッチ有効化の設定があります。
何も設定しなければバッチリクエストは有効になっています。
useBatchにfalseが設定されている場合はバッチリクエストが無効になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/645403eb-cabc-9c0a-2407-3ae20dbfb82b.png)

####②create, updateなどの処理の後にsubmitChangesを呼ぶ
create, updateのタイミングではサーバにリクエストはまだ送信されず、submitChangesを呼ぶことでまとめて送信されます。

参考：[OData V2のBatch Processing](https://sapui5.hana.ondemand.com/#/topic/6c47b2b39db9404582994070ec3d57a2.html#loio8a6ae1d390534d05a560bf350af59c29)

####③必要に応じてcontent-IDでヘッダ、明細をひもづける
階層関係のあるエンティティの場合、リクエストヘッダにcontent-IDを渡すことで、OData側でエンティティ同士のひもづけが可能になります。

##実装手順
ヘッダと明細の登録を1つのBatch Processの中で行うための実装をしていきます。動作確認目的で作ったので、あまりきれいな作りにはなっていない点をご了承ください。

###OData側の実装
[【SAPUI5】Deep Insertを使ったデータの登録(1)](https://qiita.com/tami/items/98fdef77d7645429d402)の記事で作成したODataサービスを土台にします。
※このとき作ったサービスが使えなくなったため、別のサービスを登録しました。そのため項目が多少変わっています。

OData側は以下のブログを参考に実装しました。
[How To Implement OData $batch Processing with Content ID in SAP Gateway](https://blogs.sap.com/2016/03/09/how-to-implement-odata-batch-processing-with-content-id-in-sap-gateway/)

####CHANGESET_BEGIN

```abap
  METHOD /iwbep/if_mgw_appl_srv_runtime~changeset_begin.

    cv_defer_mode = abap_true.
    LOOP AT it_operation_info INTO DATA(ls_operation_info).
      IF ls_operation_info-content_id IS NOT INITIAL OR
        ls_operation_info-content_id_ref IS NOT INITIAL.
        CONTINUE.
      ELSE.
        cv_defer_mode = abap_false.
      ENDIF.
    ENDLOOP.

  ENDMETHOD.
```
画面からcontent-IDが渡されてヘッダ、明細がひもづいている場合に一括処理を行うこととします。ヘッダ、明細が渡されたとき、IT_OPERATION_INFOには以下のようなデータが入ってきます。ヘッダのCONTENT_ID、および明細のCONTENT_ID_REFに同じ値が設定されてひもづいていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/3c84fc94-4819-b70a-7374-89926e84cdb0.png)


####CHANGESET_PROCESS

```abap
  METHOD /iwbep/if_mgw_appl_srv_runtime~changeset_process.

    DATA: lt_group_request TYPE /iwbep/if_mgw_appl_types=>ty_t_changeset_request.

    FIELD-SYMBOLS:
           <ls_header>       TYPE zcl_z_mob49_02_salesor_mpc=>ts_salesorder.

* Handle consecutive operations with the same operation types
    LOOP AT it_changeset_request INTO DATA(ls_changeset_request).
      APPEND ls_changeset_request TO lt_group_request.
      AT END OF operation_type.

        CASE ls_changeset_request-operation_type.
          WHEN /iwbep/if_mgw_appl_types=>gcs_operation_type-create_entity.

            handle_changeset_creates(
              EXPORTING
                it_group_request      = lt_group_request
              CHANGING
                ct_changeset_response = ct_changeset_response
            ).

          WHEN /iwbep/if_mgw_appl_types=>gcs_operation_type-update_entity.
            "Todo
          WHEN /iwbep/if_mgw_appl_types=>gcs_operation_type-delete_entity.
            "Todo
          WHEN /iwbep/if_mgw_appl_types=>gcs_operation_type-execute_action.
            "Todo
          WHEN /iwbep/if_mgw_appl_types=>gcs_operation_type-patch_entity.
            "Todo
        ENDCASE.
        REFRESH: lt_group_request.
      ENDAT.
    ENDLOOP.

  ENDMETHOD.
```
create, updateなどのオペレーションの種類ごとにリクエストをまとめて、サブメソッド（ここではhandle_changeset_creates）を呼びます。
以下がサブメソッドの定義です。

```abap
  METHOD handle_changeset_creates.
    DATA: lo_create_context     TYPE REF TO /iwbep/if_mgw_req_entity_c,
          lv_entity_type        TYPE string,
          ls_header             TYPE zcl_z_mob49_02_salesor_mpc=>ts_salesorder,
          lt_header             TYPE zcl_z_mob49_02_salesor_mpc=>tt_salesorder,
          ls_item               TYPE zcl_z_mob49_02_salesor_mpc=>ts_salesorderitem,
          lt_item               TYPE zcl_z_mob49_02_salesor_mpc=>tt_salesorderitem,
          lv_so_id_max          TYPE snwd_so_id,
          lv_item_no            TYPE zmob49_so_item-so_item_pos,
          ls_changeset_response TYPE /iwbep/if_mgw_appl_types=>ty_s_changeset_response.

    FIELD-SYMBOLS:
          <ls_header>       TYPE zcl_z_mob49_02_salesor_mpc=>ts_salesorder.

    LOOP AT it_group_request INTO DATA(ls_group_request).
      CLEAR: ls_header, ls_item.
      "ヘッダと明細にわけ、登録用データを作成
      lo_create_context ?= ls_group_request-request_context.
      lv_entity_type = lo_create_context->get_entity_type_name( ).

      CASE lv_entity_type.
        WHEN  'SalesOrder'.
          "リクエストデータの取得
          ls_group_request-entry_provider->read_entry_data(
            IMPORTING
              es_data = ls_header
          ).
          "GUIDの採番
          ls_header-so_key = cl_reca_guid=>get_new_guid( ).

          "SO_IDの設定
          IF lv_so_id_max IS INITIAL.
            SELECT MAX( so_id ) AS so_id_max FROM zmob49_so INTO @lv_so_id_max.
          ENDIF.
          IF lv_so_id_max IS INITIAL.
            lv_so_id_max = '5000000000'.
          ELSE.
            ADD 1 TO lv_so_id_max.
          ENDIF.
          ls_header-so_id = lv_so_id_max.

          "登録・更新日時の設定
          ls_header-created_by = sy-uname.
          ls_header-changed_by = sy-uname.
          GET TIME STAMP FIELD ls_header-created_at.
          GET TIME STAMP FIELD ls_header-changed_at.

          APPEND ls_header TO lt_header.

          "レスポンスに設定
          copy_data_to_ref(
            EXPORTING
              is_data = ls_header
            CHANGING
              cr_data = ls_changeset_response-entity_data
          ).
          ls_changeset_response-operation_no = ls_group_request-operation_no.
          INSERT ls_changeset_response INTO TABLE ct_changeset_response.

        WHEN 'SalesOrderItem'.
          "ヘッダの取得（前提として、ヘッダは明細より前に処理されている）
          READ TABLE it_group_request INTO DATA(ls_changeset_req_parent)
          WITH KEY content_id = ls_group_request-content_id_ref.
          IF sy-subrc <> 0.
            RAISE EXCEPTION TYPE /iwbep/cx_mgw_tech_exception
              EXPORTING
                textid      = /iwbep/cx_mgw_busi_exception=>precondition_failed
                operation   = 'CREATE_ENTITY'
                entity_type = lv_entity_type.
          ENDIF.

          READ TABLE ct_changeset_response INTO DATA(ls_changeset_resp_parent)
          WITH KEY operation_no = ls_changeset_req_parent-operation_no.
          IF sy-subrc <> 0.
            RAISE EXCEPTION TYPE /iwbep/cx_mgw_tech_exception
              EXPORTING
                textid      = /iwbep/cx_mgw_busi_exception=>precondition_failed
                operation   = 'CREATE_ENTITY'
                entity_type = lv_entity_type.
          ENDIF.

          "リクエストデータの取得
          ls_group_request-entry_provider->read_entry_data(
            IMPORTING
              es_data = ls_item
          ).
          "GUIDの採番
          ls_item-item_key = cl_reca_guid=>get_new_guid( ).

          "明細番号の設定
          IF lv_item_no IS INITIAL.
            lv_item_no = '10'.
          ELSE.
            ADD 10 TO lv_item_no.
          ENDIF.

          "ヘッダGUIDの設定
          ASSIGN ls_changeset_resp_parent-entity_data->* TO <ls_header>.
          IF <ls_header> IS NOT INITIAL.
            ls_item-parent_key = <ls_header>-so_key.
          ENDIF.

          APPEND ls_item TO lt_item.

          "レスポンスに設定
          copy_data_to_ref(
            EXPORTING
              is_data = ls_item
            CHANGING
              cr_data = ls_changeset_response-entity_data
          ).
          ls_changeset_response-operation_no = ls_group_request-operation_no.
          INSERT ls_changeset_response INTO TABLE ct_changeset_response.

      ENDCASE.
    ENDLOOP.

    TRY.
        INSERT zmob49_so FROM TABLE lt_header.
        INSERT zmob49_so_item FROM TABLE lt_item.

      CATCH cx_sy_open_sql_db INTO DATA(lx_osql).
        RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception
          EXPORTING
            textid            = /iwbep/cx_mgw_busi_exception=>business_error_unlimited
            entity_type       = 'SalesOrderSet'
            message_unlimited = 'Changeset Deferred Processing: Mass Create Error'.
    ENDTRY.

  ENDMETHOD.
```


####CHANGESET_END
再定義しただけで、特に何もしません。
ここでCOMMITしなくてもDBには登録されました。

###SAPUI5側の実装
簡単にするため、登録画面だけを持つアプリを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/13a79222-32a5-2eb6-7ce9-c7ff5850190b.png)

ここではAddボタンが押されたときのソースを紹介します。全体のコードは[GitHub](https://github.com/miyasuta/Train_10_ChangeSet)にあります。


```javascript
		onSave: function(){
			//デフォルトモデルであるODataを取得
			var oModel = this.getView().getModel();
			//ビューモデルからODataに送る項目を取得（ヘッダ、明細を分ける）
			var oViewModel = this.getView().getModel("createOrderView");
			var payload_h = oViewModel.getProperty("/salesOrder");
			var payload_i = oViewModel.getProperty("/items");
			
			//DeferredGroupを設定
			//1回にまとめる場合は設定しなくても変わらない
			oModel.setDeferredGroups(["group1"]);
			
			//ヘッダの登録
			oModel.create("/SalesOrderSet", payload_h, {
				headers: {
					"content-ID": 1
				},
				groupId: "group1"
			});
			
			//明細の登録
			//まとめるとエラーになってしまうので1行ずつ
			for (var i = 0; i < payload_i .length; i++) {
				var oEntry = payload_i[i];
				oModel.create("/$1/ToItems", oEntry, {
					groupId: "group1"
				});
			}
			
			//busyを設定
			this._setBusy(true);
			var that = this;
			
			//結果をサブミット
			oModel.submitChanges({
				success: function(data) {
					//busyを解除
					that._setBusy(false);
					MessageToast.show("Sales order has been created");
				},
				error : function(e) {
					that._setBusy(false);
					MessageBox.error(e.message);
				},
				groupId: "group1"
			});
			
		},
		
		_setBusy : function (bIsBusy) {
			var oModel = this.getView().getModel("createOrderView");
			oModel.setProperty("/busy", bIsBusy);
		}
```
DeferredGroupというのは、リクエストをグルーピングする単位です。
ヘッダ、明細に別々のgroupIdを設定した場合は、submitChangesで指定したグループだけが送信されます。今回のように1回のリクエストで送る場合はgroupIdは指定しなくても動作は変わりません。（デフォルトのグループにまとめられるのだと思います）

##動作確認
登録ボタンを押すと、$batchリクエストが1回だけ送信されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/81cea739-87d9-ad68-ef4b-0efa059c95bf.png)

以下がリクエストのペイロードです。

```http
--batch_1d41-7217-ef06
Content-Type: multipart/mixed; boundary=changeset_517f-c417-786e

--changeset_517f-c417-786e
Content-Type: application/http
Content-Transfer-Encoding: binary

POST SalesOrderSet HTTP/1.1
content-ID: 1
sap-contextid-accept: header
Accept: application/json
Accept-Language: en-US
DataServiceVersion: 2.0
MaxDataServiceVersion: 2.0
x-csrf-token: EyhE9CVy4mt8z-DTazxt3g==
Content-Type: application/json
Content-Length: 42

{"GrossAmount":"100","CurrencyCode":"USD"}
--changeset_517f-c417-786e
Content-Type: application/http
Content-Transfer-Encoding: binary

POST $1/ToItems HTTP/1.1
sap-contextid-accept: header
Accept: application/json
Accept-Language: en-US
DataServiceVersion: 2.0
MaxDataServiceVersion: 2.0
x-csrf-token: EyhE9CVy4mt8z-DTazxt3g==
Content-Type: application/json
Content-Length: 77

{"Product":"HT-1040","Quantity":"1","Unit":"EA","Price":"100","Amount":"100"}
--changeset_517f-c417-786e
Content-Type: application/http
Content-Transfer-Encoding: binary

POST $1/ToItems HTTP/1.1
sap-contextid-accept: header
Accept: application/json
Accept-Language: en-US
DataServiceVersion: 2.0
MaxDataServiceVersion: 2.0
x-csrf-token: EyhE9CVy4mt8z-DTazxt3g==
Content-Type: application/json
Content-Length: 77

{"Product":"HT-1040","Quantity":"1","Unit":"EA","Price":"100","Amount":"100"}
--changeset_517f-c417-786e--

--batch_1d41-7217-ef06--
```
DBにレコードが登録されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e5a349d2-262f-4ee2-0d95-1adcc45511a0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f7fd07fa-ccb9-1d02-8b46-1035aa36d1ba.png)

##submitChangesのコールバックについて（2019/12/2追記）
submitChangesは、バッチプロセスでアプリケーションエラー（例外）が発生した場合でもerrorのコールバックに入りません。このため、successコールバックの中でエラーハンドリングを行う必要があります。参考のため、この件についてSAP Communityへ質問した内容（答えあり）のリンクを載せておきます。
https://answers.sap.com/questions/12920154/odata-v2-submitchanges-error-call-back-not-working.html?childToView=12922093#answer-12922093

##参考
###バックエンド側
- [Details about some new features in SAP Gateway 2.0 SP09](https://archive.sap.com/documents/docs/DOC-57113)
- [$BATCH request in SAP GATEWAY](https://blogs.sap.com/2018/05/06/batch-request-in-sap-gateway/)
- [How To Implement OData $batch Processing with Content ID in SAP Gateway](https://blogs.sap.com/2016/03/09/how-to-implement-odata-batch-processing-with-content-id-in-sap-gateway/)

###フロントエンド側
- [Batch Processing](https://sapui5.hana.ondemand.com/#/topic/6c47b2b39db9404582994070ec3d57a2.html#loio8a6ae1d390534d05a560bf350af59c29)
- [Batch Calls From SAPUI5 Application](http://www.techippo.com/2017/10/OData-Batch-SAPUI5-SAPGateway-service.html)