Help us understand the problem. What is going on with this article?

【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)

前提

ODataサービスはV2を使用します。
OData V4では、UI5側のメソッドが変わるのでご注意ください。
参考:ODataV4のBatch Control

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

UI5側

①ODataの設定で、バッチリクエストが有効になっていることを確認する

デフォルトモデルの場合、manifest.jsonにバッチ有効化の設定があります。
何も設定しなければバッチリクエストは有効になっています。
useBatchにfalseが設定されている場合はバッチリクエストが無効になっています。
image.png

②create, updateなどの処理の後にsubmitChangesを呼ぶ

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

参考:OData V2のBatch Processing

③必要に応じてcontent-IDでヘッダ、明細をひもづける

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

実装手順

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

OData側の実装

【SAPUI5】Deep Insertを使ったデータの登録(1)の記事で作成したODataサービスを土台にします。
※このとき作ったサービスが使えなくなったため、別のサービスを登録しました。そのため項目が多少変わっています。

OData側は以下のブログを参考に実装しました。
How To Implement OData $batch Processing with Content ID in SAP Gateway

CHANGESET_BEGIN

  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

CHANGESET_PROCESS

  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)を呼びます。
以下がサブメソッドの定義です。

  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

ここではAddボタンが押されたときのソースを紹介します。全体のコードはgitHubにあります。

        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

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

--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
image.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

参考

バックエンド側

フロントエンド側

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away