1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CAP JavaでリモートのODataサービスを利用する(SAP Cloud SDKを利用)

Posted at

はじめに

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

手順

  1. ODataクライアントの生成
  2. サービスの定義
  3. イベントハンドラの実装
  4. ローカルで実行
  5. デプロイ

使用する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で保存します。
image.png

以降は、サービスを利用する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は最新のバージョンを指定してください。

pom.xml
	<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の場合

srv/pom.xml
		<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の場合

srv/pom.xml
		<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の場合

srv/pom.xml
			<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の場合

srv/pom.xml
			<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ファイルを置きます。ここで指定したファイル名が生成されるサービスのクラス名になるので、わかりやすい名前にしておきます。
image.png

以下のコマンドを実行します。

mvn clean install

その結果、outputDirectoryで指定したパスにクラスが生成されます。
image.png

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()に複数のリクエストを渡すことができます。

SAP Cloud SDKのサンプルソース

.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. デプロイの通りです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?