はじめに
CAP JavaでリモートのODataサービスを利用したい場合、2つの方法が考えられます。一つはCAPのQuerying APIを使用して、SQLライクな書き方でクエリを実行する方法、もう一つはSAP Cloud SDKのOData Clientを使う方法です。CAPのQuerying APIを使用する方法については、以下の記事で紹介しました。
基本的には上記を使用すればよいと思っていますが、リモートODataサービスに対してバッチリクエストを送りたい場合はSAP Cloud SDKが選択肢になります。CAPにバッチリクエストを送るためのAPIはないからです。
この記事では、OData V2, V4のOData Clientの使い方について説明します。以下の説明ではV2とV4の設定を並べて書いていますが、実際にはV2とV4で別々のCAPプロジェクトを作成しています。
V2
https://github.com/miyasuta/java-batch-root/tree/main/java-batch
V4
https://github.com/miyasuta/java-batch-root/tree/main/java-batch-v4
手順
- ODataクライアントの生成
- サービスの定義
- イベントハンドラの実装
- ローカルで実行
- デプロイ
使用するODataサービス
CAPで簡単なODataサービスを作成し、Cloud Foundryにデプロイしました。Deep Createを試したかったのでSalesOrders, Itemsという親子関係のあるエンティティを定義しています。
※こちらのプロジェクトはNode.jsで作成しています。
https://github.com/miyasuta/java-batch-root/tree/main/salesorder
OData V2としても公開するため、@cap-js-community/odata-v2-adapter
を使用しています。
ODataサービスを実行し、V2, V4のメタデータをそれぞれダウンロードし、拡張子.edmxで保存します。
以降は、サービスを利用するCAP Javaのプロジェクト側の設定です。
1. ODataクライアントの生成
参考:https://sap.github.io/cloud-sdk/docs/java/features/odata/vdm-generator
1.1. pom.xmlの設定
プロジェクトルートのpom.xmlに以下の設定を追加します。cloud.sdk.version
は最新のバージョンを指定してください。
<properties>
<cloud.sdk.version>5.8.0</cloud.sdk.version>
...
</properties>
<dependencyManagement>
<dependencies>
...
<dependency>
<groupId>com.sap.cloud.sdk</groupId>
<artifactId>sdk-modules-bom</artifactId>
<version>${cloud.sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
srv/pom.xmlに以下のdependencyを追加します。
OData V2の場合
<dependency>
<groupId>com.sap.cloud.sdk.datamodel</groupId>
<artifactId>odata-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk</groupId>
<artifactId>sdk-core</artifactId>
</dependency>
OData V4の場合
<dependency>
<groupId>com.sap.cloud.sdk.datamodel</groupId>
<artifactId>odata-v4-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk</groupId>
<artifactId>sdk-core</artifactId>
</dependency>
さらに、srv/pom.xmlに以下のプラグインを追加します。
OData V2の場合
<plugin>
<groupId>com.sap.cloud.sdk.datamodel</groupId>
<artifactId>odata-generator-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-consumption</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputDirectory>${project.basedir}/edmx</inputDirectory>
<outputDirectory>${project.basedir}/src/gen/java</outputDirectory>
<deleteOutputDirectory>true</deleteOutputDirectory>
<packageName>com.mycompany.vdm</packageName>
<defaultBasePath>odata/v2/sales/</defaultBasePath>
<compileScope>COMPILE</compileScope>
<serviceMethodsPerEntitySet>true</serviceMethodsPerEntitySet>
</configuration>
</execution>
</executions>
</plugin>
OData V4の場合
<plugin>
<groupId>com.sap.cloud.sdk.datamodel</groupId>
<artifactId>odata-v4-generator-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-consumption</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputDirectory>${project.basedir}/edmx</inputDirectory>
<outputDirectory>${project.basedir}/src/gen/java</outputDirectory>
<deleteOutputDirectory>true</deleteOutputDirectory>
<packageName>com.mycompany.vdm</packageName>
<defaultBasePath>odata/v4/sales</defaultBasePath>
<compileScope>COMPILE</compileScope>
<serviceMethodsPerEntitySet>true</serviceMethodsPerEntitySet>
</configuration>
</execution>
</executions>
</plugin>
1.2. ODataクライアントの生成
inputSpec
で指定したディレクトリにedmxファイルを置きます。ここで指定したファイル名が生成されるサービスのクラス名になるので、わかりやすい名前にしておきます。
以下のコマンドを実行します。
mvn clean install
その結果、outputDirectory
で指定したパスにクラスが生成されます。
2. サービスの定義
以下のサービスを定義しました。疎通確認のため、簡単なファンクションとアクションのみ定義しています(Vxのxには2または4が入る)。
- furncion readOrderVx: 全てのSalesOrderを取得
- action postOrderVx: SalesOrderを1件登録
- action postBatchVx: バッチリクエストでSalesOrderを登録
service CatalogService {
entity SalesOrders {
key ID: UUID;
orderDate: Date;
customer: String(50);
items: Composition of many OrderItems on items.order = $self;
}
entity OrderItems {
key ID: UUID;
order: Association to SalesOrders;
product: String(50);
quantity: Integer;
price: Integer;
}
function readOrderV4() returns array of SalesOrders;
action postOrderV4(order: SalesOrders) returns SalesOrders;
action postBatchV4(orders: array of SalesOrders) returns array of SalesOrders;
}
3. イベントハンドラの実装
Cloud SDKを使用してAPIを呼び出す場合の特徴として、DestinationAccessorおよびHttpDestinationを使用してBTPに登録したDestinationを取得するところがあります。
また、OData Clientを生成することで登録されたDefaultxxxService
クラスを利用してサービスにアクセスします。
package customer.java_batch_v4.handlers;
...
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
import com.mycompany.vdm.services.DefaultSalesServiceV4Service;
import com.mycompany.vdm.namespaces.salesservicev4.OrderItems;
import com.mycompany.vdm.namespaces.salesservicev4.SalesOrders;
...
@Component
@ServiceName(CatalogService_.CDS_NAME)
public class CatalogServiceHandler implements EventHandler {
Logger logger = LoggerFactory.getLogger(CatalogServiceHandler.class);
@On(event = ReadOrderV4Context.CDS_NAME)
public void ReadOrderV4(ReadOrderV4Context context) {
HttpDestination destination = DestinationAccessor.getDestination("salesorder-srv").asHttp();
DefaultSalesServiceV4Service service = new DefaultSalesServiceV4Service().withServicePath("/odata/v4/sales");
...
}
}
※OData Clientを生成するときにdefaultBasePath
を指定しているので .withServicePath("/odata/v4/sales")
はつけなくてもよいはずですが、V4の場合はつけないとなぜかサービスの呼び出しで404エラーになってしまいました。
以降では、V2とV4で書き方が少し異なるので、それぞれについて紹介します。
3.1. OData V2
3.1.1. GETリクエスト
getAllSalesOrders()
メソッドを使用して複数のレコードを取得します。select()
メソッドで取得する項目を絞ることができます。最後にexecuteRequest()
メソッドでリクエストを実行します。
@On(event = ReadOrderV2Context.CDS_NAME)
public void ReadOrderV2(ReadOrderV2Context context) {
logger.info("Readv2 handler called");
HttpDestination destination = DestinationAccessor.getDestination("salesorder-srv").asHttp();
DefaultSalesServiceV2Service service = new DefaultSalesServiceV2Service().withServicePath("/odata/v2/sales");
// Get request
final List<SalesOrders> salesOrdersResp = service.getAllSalesOrders()
.select(SalesOrders.ID,
SalesOrders.CUSTOMER,
SalesOrders.ORDER_DATE,
SalesOrders.TO_ITEMS)
.executeRequest(destination);
//結果のマッピング(省略)
context.setResult(salesOrdersOut);
}
.httpファイルを作成して以下のリクエストを実行すると、レスポンスが返ってきます。
@server=http://localhost:8080
###
GET {{server}}/odata/v4/CatalogService/readOrderV2()
{
"@context": "$metadata#Collection(CatalogService.SalesOrders)(items())",
"@metadataEtag": "W/\"5baafd6f93a46c60552a823276e53ef730c6c000ea72bc368204be711d4f21e1\"",
"value": [
{
"ID": "b5242176-e5bc-43f7-9bac-375052d65b5f",
"orderDate": "2024-05-07",
"customer": "Java-V2",
"items": [
{
"ID": "ce8bba46-2568-452c-88b9-6b39065178db",
"order_ID": "b5242176-e5bc-43f7-9bac-375052d65b5f",
"product": "PC",
"quantity": 1,
"price": 1000
}
]
}
]
}
3.1.2. 登録リクエスト
createSalesOrders()
メソッドで登録リクエストを送信します。
@On(event = PostOrderV2Context.CDS_NAME)
public void PostOrderv2(PostOrderV2Context context) {
logger.info("PostOrderv2 handler called");
HttpDestination destination = DestinationAccessor.getDestination("salesorder-srv").asHttp();
DefaultSalesServiceV2Service service = new DefaultSalesServiceV2Service().withServicePath("/odata/v2/sales");
// map request to salesorder
SalesOrders salesOrderReq = new SalesOrders();
salesOrderReq.setCustomer(context.getOrder().getCustomer());
salesOrderReq.setOrderDate(context.getOrder().getOrderDate().atStartOfDay());
context.getOrder().getItems().forEach(itemIn -> {
OrderItems itemReq = new OrderItems();
itemReq.setProduct(itemIn.getProduct());
itemReq.setQuantity(itemIn.getQuantity());
itemReq.setPrice(itemIn.getPrice());
salesOrderReq.addItems(itemReq);
});
ModificationResponse<SalesOrders> response = service.createSalesOrders(salesOrderReq)
.executeRequest(destination);
//結果のマッピング(省略)
context.setResult(salesOrderOut);
}
リクエストとレスポンスのサンプルは以下です。
@server=http://localhost:8080
###
POST {{server}}/odata/v4/CatalogService/postOrderV2
Content-Type: application/json
{
"order": {
"orderDate": "2024-05-07",
"customer": "Java-V2",
"items": [
{
"product": "PC",
"quantity": 1,
"price": 1000
}
]
}
}
{
"@context": "$metadata#CatalogService.SalesOrders(items())",
"@metadataEtag": "W/\"5baafd6f93a46c60552a823276e53ef730c6c000ea72bc368204be711d4f21e1\"",
"ID": "0ca340d2-f269-4f61-beeb-be60d0125cf8",
"orderDate": "2024-05-07",
"customer": "Java-V2",
"items": [
{
"ID": "38f695c3-2b69-4fa5-8729-a5c33cd38031",
"order_ID": "0ca340d2-f269-4f61-beeb-be60d0125cf8",
"product": "PC",
"quantity": 1,
"price": 1000
}
]
}
3.1.3. バッチリクエスト
OData V2のバッチリクエストの基本形は以下のようになります。beginChangeSet()
とendChangeSet()
でcreateリクエストを挟む形です。
BatchResponse result =
service
.batch()
.beginChangeSet()
.createSalesOrders(salesOrderReq)
.endChangeSet()
.executeRequest(destination);
今回の例では登録するデータの件数が決まっていないので、リクエストデータをforループで回しながらバッチリクエストを組み立てます。
// create batch request
SalesServiceV2ServiceBatch batchrequest = service.batch();
Collection<cds.gen.catalogservice.SalesOrders> salesOrdersIn = context.getOrders();
for (cds.gen.catalogservice.SalesOrders salesOrderIn : salesOrdersIn) {
// ...
batchrequest = batchrequest.beginChangeSet()
.createSalesOrders(salesOrderReq)
.endChangeSet();
}
バッチリクエストの結果はリクエストに渡したのと同じ順番で返されるので、indexを指定して取得することができます。
Try<BatchResponseChangeSet> changeset = result.get(index);
今回の例では、登録用に作成したリスト(salesOrdersIn)をループしながら現在のindexに対応する結果を取得し、そこから登録されたSalesOrderを取り出しています。
// batch call
BatchResponse result = batchrequest.executeRequest(destination);
int index = 0;
Collection<cds.gen.catalogservice.SalesOrders> salesOrdersOut = new ArrayList<>();
for (cds.gen.catalogservice.SalesOrders salesOrderIn : salesOrdersIn) {
Try<BatchResponseChangeSet> changeset = result.get(index);
index++; // increment index for next loop
if (changeset.isSuccess()) {
//登録されたエンティティを取得
List<VdmEntity<?>> SalesOrdersResp = changeset.get().getCreatedEntities();
List<cds.gen.catalogservice.SalesOrders> salesOrdersList = SalesOrdersResp.stream()
.map(entity -> {
if (entity instanceof SalesOrders) {
//entityをSalesOrdersにキャスト
SalesOrders salesOrderResp = (SalesOrders) entity;
//結果のマッピング(省略)
}
})
.collect(Collectors.toList());
salesOrdersOut.addAll(salesOrdersList);
}
}
全体のソースは以下のようになります。
@On(event = PostBatchV2Context.CDS_NAME)
public void PostBatchV2(PostBatchV2Context context) {
logger.info("PostBatchv2 handler called");
HttpDestination destination = DestinationAccessor.getDestination("salesorder-srv").asHttp();
DefaultSalesServiceV2Service service = new DefaultSalesServiceV2Service().withServicePath("/odata/v2/sales");
// create batch request
SalesServiceV2ServiceBatch batchrequest = service.batch();
Collection<cds.gen.catalogservice.SalesOrders> salesOrdersIn = context.getOrders();
for (cds.gen.catalogservice.SalesOrders salesOrderIn : salesOrdersIn) {
// map request to salesorder
SalesOrders salesOrderReq = new SalesOrders();
salesOrderReq.setCustomer(salesOrderIn.getCustomer());
salesOrderReq.setOrderDate(salesOrderIn.getOrderDate().atStartOfDay());
salesOrderIn.getItems().forEach(itemIn -> {
OrderItems itemReq = new OrderItems();
itemReq.setProduct(itemIn.getProduct());
itemReq.setQuantity(itemIn.getQuantity());
itemReq.setPrice(itemIn.getPrice());
salesOrderReq.addItems(itemReq);
});
batchrequest = batchrequest.beginChangeSet()
.createSalesOrders(salesOrderReq)
.endChangeSet();
}
// batch call
BatchResponse result = batchrequest.executeRequest(destination);
int index = 0;
Collection<cds.gen.catalogservice.SalesOrders> salesOrdersOut = new ArrayList<>();
for (cds.gen.catalogservice.SalesOrders salesOrderIn : salesOrdersIn) {
Try<BatchResponseChangeSet> changeset = result.get(index);
index++; // increment index for next loop
if (changeset.isSuccess()) {
List<VdmEntity<?>> SalesOrdersResp = changeset.get().getCreatedEntities();
List<cds.gen.catalogservice.SalesOrders> salesOrdersList = SalesOrdersResp.stream()
.map(entity -> {
//結果のマッピング(省略)
})
.collect(Collectors.toList());
salesOrdersOut.addAll(salesOrdersList);
}
}
context.setResult(salesOrdersOut);
}
リクエストとレスポンスのサンプルは以下です。
POST {{server}}/odata/v4/CatalogService/postBatchV2
Content-Type: application/json
{
"orders": [{
"orderDate": "2024-04-29",
"customer": "Java1",
"items": [
{
"product": "PC",
"quantity": 1,
"price": 1000
}
]
},{
"orderDate": "2024-04-29",
"customer": "Java2",
"items": [
{
"product": "PC",
"quantity": 1,
"price": 1000
}
]
}]
}
{
"@context": "$metadata#Collection(CatalogService.SalesOrders)(items())",
"@metadataEtag": "W/\"5baafd6f93a46c60552a823276e53ef730c6c000ea72bc368204be711d4f21e1\"",
"value": [
{
"ID": "da2d97f9-90f3-46ea-a1c0-0d20b76eb9ad",
"orderDate": "2024-04-29",
"customer": "Java1",
"items": [
{
"ID": "43f68201-e3e7-4d0c-95b9-1b619e9b8a3f",
"order_ID": "da2d97f9-90f3-46ea-a1c0-0d20b76eb9ad",
"product": "PC",
"quantity": 1,
"price": 1000
}
]
},
{
"ID": "ff7ad37f-f9c0-461a-9bb7-c79558f7e505",
"orderDate": "2024-04-29",
"customer": "Java2",
"items": [
{
"ID": "abd8de5a-cc67-46d1-9c02-aeb4d9954c6b",
"order_ID": "ff7ad37f-f9c0-461a-9bb7-c79558f7e505",
"product": "PC",
"quantity": 1,
"price": 1000
}
]
}
]
}
3.2. OData V4
3.2.1. GETリクエスト
OData V2と異なる点は、V4ではexecute()
メソッドでリクエストを実行することです。V2ではexecuteRequest()
を使用していました。
@On(event = ReadOrderV4Context.CDS_NAME)
public void ReadOrderV4(ReadOrderV4Context context) {
logger.info("ReadV4 handler called");
HttpDestination destination = DestinationAccessor.getDestination("salesorder-srv").asHttp();
DefaultSalesServiceV4Service service = new DefaultSalesServiceV4Service().withServicePath("/odata/v4/sales");
// get request
final List<SalesOrders> salesOrdersResp = service.getAllSalesOrders()
.select(SalesOrders.ID,
SalesOrders.CUSTOMER,
SalesOrders.ORDER_DATE,
SalesOrders.TO_ITEMS)
.execute(destination);
//結果のマッピング(省略)
context.setResult(readorders);
}
3.2.2. 登録リクエスト
V2と異なる点はGETリクエストと同じです。
@On(event = PostOrderV4Context.CDS_NAME)
public void PostOrderv4(PostOrderV4Context context) {
logger.info("PostOrderV4 handler called");
HttpDestination destination = DestinationAccessor.getDestination("salesorder-srv").asHttp();
DefaultSalesServiceV4Service service = new DefaultSalesServiceV4Service().withServicePath("/odata/v4/sales");
// map request to salesorder
SalesOrders salesOrderReq = new SalesOrders();
salesOrderReq.setCustomer(context.getOrder().getCustomer());
salesOrderReq.setOrderDate(context.getOrder().getOrderDate());
context.getOrder().getItems().forEach(item -> {
OrderItems itemReq = new OrderItems();
itemReq.setProduct(item.getProduct());
itemReq.setQuantity(item.getQuantity());
itemReq.setPrice(item.getPrice());
salesOrderReq.addItems(itemReq);
});
// post salesorder
ModificationResponse<SalesOrders> response = service.createSalesOrders(salesOrderReq).execute(destination);
//結果のマッピング(省略)
context.setResult(salesOrderOut);
}
3.2.3. バッチリクエスト
OData V4のバッチリクエストでは、addChangeSet()
に複数のリクエストを渡すことができます。
.addChangeset(createBusinessPartnerRequest1, createBusinessPartnerRequest2, deleteBusinessPartnerRequest)
今回の例では登録するデータの件数が決まっていないので、インプットで受け取った複数件のSalesOrdersを配列に変換してaddChangeset()
に渡しています。
// create request
List<CreateRequestBuilder<SalesOrders>> createReusests = context.getOrders().stream()
.map(salesOrderIn -> {
...
}).collect(Collectors.toList());
//配列に変換
ModificationRequestBuilder<?>[] requestArray = new ModificationRequestBuilder<?>[createReusests.size()];
createReusests.toArray(requestArray);
// batch call
try (
BatchResponse result = service
.batch()
.addChangeset(requestArray)
.execute(destination);) {
}
バッチリクエストの結果は、getModificationResult()
メソッドに登録用に作成したオブジェクトを渡すことで取得することができます。
SalesOrders salesOrderResp = result.getModificationResult(salesOrderReq).getModifiedEntity();
全体のソースは以下のようになります。
@On(event = PostBatchV4Context.CDS_NAME)
public void PostBatchV4(PostBatchV4Context context) {
logger.info("PostBatchV4 handler called");
HttpDestination destination = DestinationAccessor.getDestination("salesorder-srv").asHttp();
DefaultSalesServiceV4Service service = new DefaultSalesServiceV4Service().withServicePath("/odata/v4/sales");
// create request
List<CreateRequestBuilder<SalesOrders>> createReusests = context.getOrders().stream()
.map(salesOrderIn -> {
SalesOrders salesOrderReq = new SalesOrders();
salesOrderReq.setCustomer(salesOrderIn.getCustomer());
salesOrderReq.setOrderDate(salesOrderIn.getOrderDate());
salesOrderIn.getItems().stream()
.map(itemIn -> {
OrderItems itemReq = new OrderItems();
itemReq.setProduct(itemIn.getProduct());
itemReq.setPrice(itemIn.getPrice());
itemReq.setQuantity(itemIn.getQuantity());
return itemReq;
}).forEach(salesOrderReq::addItems);
return service.createSalesOrders(salesOrderReq);
}).collect(Collectors.toList());
//配列に変換
ModificationRequestBuilder<?>[] requestArray = new ModificationRequestBuilder<?>[createReusests.size()];
createReusests.toArray(requestArray);
// batch call
try (
BatchResponse result = service
.batch()
.addChangeset(requestArray)
.execute(destination);) {
// map response
List<cds.gen.catalogservice.SalesOrders> salesOrdersOut = createReusests.stream().map(salesOrderReq -> {
SalesOrders salesOrderResp = result.getModificationResult(salesOrderReq).getModifiedEntity();
//結果のマッピング(省略)
}).collect(Collectors.toList());
context.setResult(salesOrdersOut);
4. ローカルで実行
ローカル環境(例:BAS)で実行する場合、destinations
という環境変数を定義します。
https://sap.github.io/cloud-sdk/docs/java/features/connectivity/running-locally#testing-with-destinationaccessor
export destinations='[{name: "salesorder-srv", url: "http://localhost:4004", Authentication: "NoAuthentication" }]'
上記の方法ではベーシック認証または認証なしの宛先しか使えないため、認証が必要な場合は以下のブログの6. 開発環境からリモートサービスに接続の方法を使用します。
5. デプロイ
デプロイの方法は、以下のブログの5. デプロイの通りです。