【SAP Cloud Integration】Batchリクエストで複数エンティティを送信する

## はじめに
この記事では、Cloud Integrationを使いODataのバッチリクエストを投げる方法について、複数の方法を試した結果をまとめます。

### シナリオ例
2つシステムAとBがあり、システムAに登録された発注(PO)をシステムBに受注(SO)として登録したいとします。システムAには発注を取得するためのAPIがあり、システムBには受注を登録するためのOData APIがあります。

そこで、Cloud Integrationを使い以下を行います。
1. システムAのAPIを呼び発注を取得する
2. システムAの発注とシステムBの受注の項目をマッピングする
3. システムBのAPIを呼び受注を登録する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/1cfbe740-f058-cd04-7514-bfaffb4b8a92.png)

システムAからの発注取得はリアルタイムではなく、定期的なバッチ処理として行います。そのため、複数のデータを一度に取得してシステムBに登録する必要があります。

### Cloud IntegrationでBatchクエストを投げる
ODataで複数のデータを一度に投げる場合、Batchクエストを使います。Cloud Integrationの[OData Adapter](https://help.sap.com/viewer/368c481cd6954bdfa5d0435479fd4eaf/Cloud/en-US/2d8251111ea74dbbb9f5525db5d9ad04.html?q=odata%20adapter)を使うとBatchリクエストを投げることができます。また、[HTTP Adapter](https://help.sap.com/viewer/368c481cd6954bdfa5d0435479fd4eaf/Cloud/en-US/2da452effb764b3bb28f8e0a2f5bd480.html?q=http%20adapter)を使ってもBatchリクエストを投げることができます（この場合、Groovy Scriptでペイロードの編集が必要）。

## 検証
「シナリオ例」の構成に似た形にするため、以下のサービスを使います。本来は発注-受注でやりたいところですが、マッピングを簡単にするため受注-受注としています。

- ソースシステム：[SAP GraphのSales Order （OData V4）](https://api.sap.com/api/sap_graph_SalesOrder/overview)
- ターゲットシステム：CAPで作ったODataサービス

CAPのODataサービスのソースは以下に格納しています。
https://github.com/miyasuta/api-sales-orders-mock

### 検証パターン
1. HTTPアダプター + Groovy Script
1. OData V2アダプター
1. OData V4アダプター

### 結果
試してみたところ、うまくいった方法とうまくいかなかった方法がありました。以下が今のところの結果です。OData V4アダプターを使って、今回やりたいシナリオは実現できませんでした。
※私の設定が間違っている可能性もありますので、ご参考程度に
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/616b9d35-2550-f413-8475-29a051da7d19.png)

### 検証内容
#### 1. HTTPアダプター + Groovy Script
##### インテグレーションフロー
インテグレーションフローは以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a89b7133-8de4-2ea3-46b6-c5ed98b6ab91.png)

###### ①HTTPS Sender Adapter
HTTPリクエストでインテグレーションフローを呼べるように、[HTTPS Sender Adapter](https://help.sap.com/viewer/368c481cd6954bdfa5d0435479fd4eaf/Cloud/en-US/0ae4a78909c4479cbc3cc414250919de.html)を使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d7c8f18d-026e-0313-7f9e-cf3a086a121c.png)

###### ②Set Header [Content Modifier]
SAP GraphのAPIを呼ぶ際に使用するヘッダの設定をします。
※API Business HubでapiKeyを確認する方法は[こちら](https://qiita.com/tami/items/3f4a51d80ff7acd83ce5#api-key%E3%81%AE%E5%8F%96%E5%BE%97)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e2946599-3c05-0d14-59a4-27c735ef291f.png)

###### ③Get Graph SO [OData V4 Receiver Adapter]
[SAP GraphのSalesOrder API](https://api.sap.com/api/sap_graph_SalesOrder/overview)を呼び、データを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/70e66e45-87fa-975a-f6ce-5a67a4500940.png)
複数件取得してバッチリクエストに渡すため、Query Optionsに`$top=2`を指定しています。また、ヘッダに②で設定したapiKeyを設定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0f2e2558-faa0-30e1-3574-7ecefb907fb1.png)

###### ④Message Mapping
SAP Graphで取得したSalesOrderをCAPのODataサービスの項目にマッピングします。マッピングにはメタデータのedmxファイルを使用することもできますが、edmxファイルベースだとヘッダ+明細のようなディープな構造にマッピングすることができません。そこで、以下のブログを参考にディープな構造を持つxsdファイルを作成しておきます。
[Multiple Entities Mapping in OData Service in Cloud Platform Integration.](https://blogs.sap.com/2021/02/05/multiple-entities-mapping-in-odata-service-in-cloud-platform-integration./)

**OData V2 Receiver Adapter（仮）を作成**
これはxsdファイルを作成するためだけのものなので、後で削除します。Selectをクリックして詳細を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/74deee37-ac10-f990-aca8-6576652fc816.png)
Sub Levelsに1を設定すると、to_Item（ナビゲーション先の項目）にもチェックを入れられます。Generate XML Schema Definitionにチェックを入れるとxsdファイルが生成されます。このファイルはマッピングの中で参照することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/35168c8d-1c91-0d69-f1a1-2a0f4dcdca1f.png)

**Message Mapping**
ターゲットに前のステップで生成したxsdファイルを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/29804d73-1fa1-60f5-7923-d2455c9e4a80.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/25d0bd71-1824-8355-66a7-00524f113d61.png)
マッピングは以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/db4e94d9-7d08-5b7d-27ea-73348d759bc2.png)
SAP GraphのAPIには明細データがないので、明細は固定値で2件設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5a49b8ba-de34-8432-64ce-44215d3d3afe.png)

###### ⑤XML to JSON Converter
Message Mappingの結果はXML形式になるので、それをJSON形式に変換します。以下の設定はデフォルトのままです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/5a012144-9d4e-7e0b-3524-4dad550694d5.png)

###### ⑥Create Batch Payload [Groovy Script]
前ステップでのマッピングの後、ペイロードは以下のような形になっています。

```json:before
{
	"A_SalesOrder": {
		"A_SalesOrder": [{
			"SalesOrder": "9000000000",
			"to_Item": {
				"A_SalesOrderItem": [{
					"SalesOrderItem": "10",
					"Material": "ABC"
				}, {
					"SalesOrderItem": "20",
					"Material": "DEF"
				}]
			},
			"SalesOrderType": "OR",
			"SalesOrganization": "1100",
			"DistributionChannel": "01",
			"SoldToParty": "10004"
		}, {
			"SalesOrder": "9000000010",
			"to_Item": {
				"A_SalesOrderItem": [{
					"SalesOrderItem": "10",
					"Material": "ABC"
				}, {
					"SalesOrderItem": "20",
					"Material": "DEF"
				}]
			},
			"SalesOrderType": "OR",
			"SalesOrganization": "1100",
			"DistributionChannel": "01",
			"SoldToParty": "10332"
		}]
	}
}
```
それを、JSON形式のBatchリクエスト（以下の形式）になるようにGroovy scriptで編集します。
参考：https://docs.microsoft.com/en-us/odata/client/batch-operations#json-batch

```json
{
	"requests": [{
		"id": "1",
		"method": "POST",
		"url": "/A_SalesOrder",
		"headers": {
			"content-type": "application/json"
		},
		"body": {
			"SalesOrderType": "OR",
			"SalesOrganization": "1100",
			"DistributionChannel": "01",
			"SoldToParty": "10004",
			"PurchaseOrderByCustomer": "",
			"to_Item": [{
				"SalesOrderItem": "10",
				"Material": "ABC"
			},{
				"SalesOrderItem": "20",
				"Material": "DEF"
			}]
		}
	}, {
		"id": "2",
		"method": "POST",
		"url": "/A_SalesOrder",
		"headers": {
			"content-type": "application/json"
		},
		"body": {
			"SalesOrderType": "OR",
			"SalesOrganization": "1100",
			"DistributionChannel": "01",
			"SoldToParty": "10004",
			"PurchaseOrderByCustomer": "",
			"to_Item": [{
				"SalesOrderItem": "10",
				"Material": "ABC"
			},{
				"SalesOrderItem": "20",
				"Material": "DEF"
			}]
		}
	}]
}
```

Groovy script

```groovy
import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
def Message processData(Message message) {
    def stringBody = message.getBody(java.lang.String) as String;
    def slurper = new groovy.json.JsonSlurper();
    def object = slurper.parseText(stringBody);
    def json = new groovy.json.JsonBuilder();
    def orders = json(object).A_SalesOrder.A_SalesOrder;
    def requests = [];
    def id = 0;
    
    orders.each() {
        id += 1;
        def items = it.to_Item.A_SalesOrderItem;
        def formattedItems = [];
        
        items.each() {
            def item = $/{
                "SalesOrderItem": "${it.SalesOrderItem}",
                "Material": "${it.Material}"
            }/$;
            formattedItems.push(item);
        }
        
        def obj = $/{
            "id": "${id}",
            "method": "POST",
            "url": "/A_SalesOrder",
            "headers": {
                "content-type": "application/json"
            },
            "body": {
                "SalesOrderType": "${it.SalesOrderType}",
                "SalesOrganization": "${it.SalesOrganization}",
                "DistributionChannel": "${it.DistributionChannel}",
                "SoldToParty": "${it.SoldToParty}",
                "PurchaseOrderByCustomer": "${it.PurchaseOrderByCustomer}",
                "to_Item": ${formattedItems}
            }        
        }/$;
        requests.push(obj);
    }
    
    def newBody = $/{
        "requests": ${requests}
    }/$.toString();
    
    message.setBody(newBody);
    return message;
}
```

###### ⑦Post SO [HTTP Receiver Adapter]
OData V2の宛先にBatchリクエストを送信します。ヘッダに②で設定した`content-type`を設定しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e8942900-69da-7840-b355-3faccec60a9d.png)

##### テスト
インテグレーションフローをデプロイし、Postmanからテストします。複数のエンティティを登録することができました。
なお、⑦の宛先をOData V4に変えても正常に動作します。（Batchのペイロードの形式はV2とV4で変わらないため）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/655678a7-aa57-84e4-d2e3-86bf2347fd4e.png)

#### 2. OData V2アダプター
##### インテグレーションフロー
インテグレーションフローは以下のようになります。前半部分は1.と一緒なので、変わる部分だけ紹介します。番号はフロー上で定義する順番で振っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/fa7f2de7-cd77-4792-7978-c4a623d72d59.png)

###### ①Post SO [OData V2 Receiver Adapter]
OData V2の宛先にリクエストを投げます。HTTP Adapterと違い、アドレスに`/$batch`は含みません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b3513ef1-0646-657e-9cea-9eaa08aba1dd.png)
Batchリクエストを投げるため、Enable Batch Processingにチェックを入れます。Content TypeはデフォルトではAtomになっていますが、CAPはBatchリクエストでAtomをサポートしていないためJSONとします。ヘッダに関しては、OData Adapterが自動的に設定してくれるので何も設定しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a97cad8e-f371-f021-8554-fa9ed6c6703f.png)
ここでも、ディープな構造で登録するためSub Levelsに1を設定します。マッピングで使うxsdファイルを生成するため、Generate XML Schema Definitionにチェックを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b21d1c20-92c6-95d7-b549-01f1c4deb038.png)

###### ②Message Mapping
①で生成したxsdファイルを使ってマッピングをします。Batchリクエスト用の構造は以下の形になっています。

```xml
<batchParts>
	<batchChangeSet>
		<batchChangeSetPart>
			<method>POST</method>
			<uri></uri>
			<A_SalesOrder>...</A_SalesOrder>
		</batchChangeSetPart>
	</batchChangeSet>
	<batchChangeSet>
		<batchChangeSetPart>
			<method>POST</method>
			<uri></uri>
			<A_SalesOrder>...</A_SalesOrder>
		</batchChangeSetPart>
	</batchChangeSet>
</batchParts>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/09c1b788-a55b-35f4-2d86-33b8f9b973b4.png)
明細は固定値で設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f54828e0-2c50-fa30-fcc6-c1276eabfd39.png)

###### ③Content Modifier
Exchange Propertyの`SAP_BatchLineSeparator`に`CRLF`を設定します。この設定をしない場合、OData側から"Boundary expected at position 0"というエラーが返ってきました。
参考：https://blogs.sap.com/2017/05/10/batch-operation-in-odata-v2-adapter-in-sap-cloud-platform-integration/#comment-437633
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/17470cd6-d48d-ebbf-72b5-d1f0263bbe3f.png)

##### テスト
インテグレーションフローをデプロイし、Postmanからテストします。レスポンスの形式がxmlになっていますが、OData Adapterのヘッダに`Accept: application/json`を設定しても変わりませんでした。レスポンス形式の指定はOData Adapterが行っていると考えられます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ec93566d-eecd-9ae5-01c3-ee45f0b5c234.png)

#### 3. OData V4アダプター
OData V4アダプターでは、任意の数のリクエストをディープな構造で投げることができませんでした。

##### 問題点
以下では、何が問題だったかについて説明します。

###### ①ディープな構造のxsdファイルを作れない
OData V4アダプターでBatchリクエストを投げるための設定は以下のようになります。更新系のリクエストの場合、"Add Changeset"ボタンを押してChangesetを追加します。1つのChangesetにつきエンティティは1つしか選択できず、"Sub Levels"もグレーアウトされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d865e2ec-4429-c8ea-9a3e-d0b12f04e36d.png)

ODataアダプターの設定は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/715dbdd4-f297-0e06-31b9-f83486f82feb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/4782be6c-2c8b-eab4-27d9-ece6489a2fb2.png)

###### ②任意の数のリクエストを投げられない
メッセージマッピングでシミュレーションをすると、複数リクエストがある場合にbatchChangeSet1が複数できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d524de0c-fee9-5df6-2ea9-d1039e2ec75f.png)
この1という数字がリクエストIDになるのですが、本来一意にならなければならないところ、全部1なのでエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/ab67c62c-4098-6640-ae9c-35d121cd9f51.png)
xsdファイルを作る段階でChangesetを複数追加すればbatchChangeSet2, batchChangeSet3...となりますが、それだとあらかじめ決まった数のリクエストしか投げられません。登録する件数がわからない場合はこの方法は使えないと思われます。

###### ③Deserialization Errorになる
1件のリクエストならどうかということで、GraphのSalesOrderを取ってくるときに`$top=1`を設定してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/f2f3f391-753a-1449-6e84-65eae59e6368.png)

フロー自体は正常終了するものの、OData側でエラーになっていました。
`Deserialization Error: The value of 'SalesOrderType@odata.type' must describe correctly the type 'Edm.String'.","@Core.ContentID":"1`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/217335c5-14b3-8dc9-3622-f8ca3e1fd397.png)

BatchリクエストでないときはOData V4アダプターでも正常に登録できたので、このエラーは謎です。

## まとめ
OData V2の場合、OData V2アダプターを使うのが最も簡単な方法です。
OData V4で任意の数のBatchリクエストを投げたい場合は、今のところHTTPアダプターを使うしかないのではないかと考えています。