1
1

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サービスを利用する

Last updated at Posted at 2024-02-18

はじめに

CAP JavaでリモートのODataサービスを利用したい場合、2つの方法が考えられます。一つはCAPのQuerying APIを使用して、SQLライクな書き方でクエリを実行する方法、もう一つはSAP Cloud SDKのOData Clientを使う方法です。
この記事では、Querying APIを使ってリモートサービスに接続し、データを読み込んだり更新する方法について紹介します。接続先はBTPにDestinationとして登録されていることを前提とします。

CAPireのドキュメントに説明がありますが、ドキュメントでは省略されているステップもあってかなり苦労したので、記事に残しておきます。ソースコードは以下のGitリポジトリにあります。

参考:初回執筆時点のリポジトリ (@sap/cds-dk: 7.4.0)
https://github.com/miyasuta/cap-java-remote-consumer

手順

  1. ODataサービスの仕様(edmxファイル)を取得
  2. edmxファイルをインポート
  3. サービスの定義
  4. モックで実行
  5. イベントハンドラの実装
  6. デプロイ
  7. 開発環境からリモートサービスに接続

@sap/cds-dkのバージョン:8.8.0

使用するリモートODataサービス

自由に読み書きできるODataサービスが欲しかったので、簡単なODataサービスを作成し、Cloud Foundryにデプロイしました。ソースコードは以下にあります(最後まで設定したものはdeploymentブランチにあります)。

1. ODataサービスの仕様(edmxファイル)を取得

リモートODataサービス側のプロジェクトで以下のコマンドを実行し、edmxファイルを取得します。接続先がS/4HANAのODataサービスであればSAP Business Accelerator Hubから取得できます。

cds compile srv -s CatalogService -2 edmx > CatalogService.edmx

2. edmxファイルをインポート

2.1. edmxファイルをインポート

リモートODataサービスを使用する側のプロジェクトのルートにedmxファイルを置いた状態で以下のコマンドを実行し、サービス定義を取り込みます。

cds import CatalogService.edmx --as cds

srv/externalフォルダに以下のファイルができます。
image.png

後からリモートサービスの定義が変更になった場合、作成されたファイルを消して取り込みなおす必要があります。

2.2. application.yamlの設定

srv/src/main/resources/application.yamlに以下の設定が追加されます。

---  
cds:
  ...
  remote:
    services:
      '[CatalogService]':
        destination:
          name: CatalogService
        type: odata-v4      

http.suffixでサービスまでのパスを指定します。destination.nameは使用するDestinationの名前に変更します。

cds:
  ...
  remote:
    services:
      '[CatalogService]':
        destination:
          name: catalogservice
        type: odata-v4
        http:
          suffix: /odata/v4 

2.3. pom.xmlの設定

プロジェクトルートのpom.xmlに以下の設定を追加します。

	<properties>
		<cloud.sdk.version><最新のSDKバージョン></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に以下の設定を追加します。これはCloud FoundryのDestinationを使うために必要です。

        <dependency>
			<groupId>com.sap.cloud.sdk</groupId>
			<artifactId>sdk-core</artifactId>
		</dependency>

3. サービスの定義

以下のサービスを定義しました。Booksエンティティはリモートサービスのエンティティの定義をそのまま使っています。アクションcreateBookおよびupdateBookにより、リモートのBooksエンティティを登録、更新します。

srv/books-service.cds
using { CatalogService as external } from './external/CatalogService';

service BooksService {
    @readonly
    entity LocalBooks as projection on external.Books 
        actions {
            action updateStock(stock: Integer);
        };

    action createBook(book: LocalBooks) returns LocalBooks;    
}

4. モックで実行

作成したサービスをローカル(モック)で実行します。CAPでのモック実行には2段階あります。
①簡単なモック(HTTPプロトコルを使わない)⇒ @sap/cds-sk:8.8.0では動かなかった
②リモートサービスをODataサービスとしてモック

①は設定は簡単ですが、モックのリモートサービスが実際のODataサービスと同じ動きをしないケースもあるので、より本物に近いテストをする場合は②を使用します。

4.1. 簡単なモック

@sap/cds-sk:8.8.0で作成したプロジェクトの場合、"Failed to get destination"というエラーになりました。このため②の方法を使うことを推奨します。

4.1.1. モックデータを作成

モック用のCSVファイルを作成し、db/dataフォルダに格納します。dbフォルダに格納するのは意外に感じますが、Javaの場合はモックデータをH2データベースにデプロイするためこのような設定になっています。なお、Node.jsの場合はsrv/external/dataフォルダにモックデータを格納します。

image.png

CatalogService-Books.csv
ID;title;stock
1;Test A;100
2;Test B;500

4.1.2. pom.xmlの設定

srv/pom.xmlに以下のコマンドがあります。H2データベースにデプロイするためのコマンドです。

<command>deploy --to h2 --dry &gt; "${project.basedir}/src/main/resources/schema-h2.sql"</command>

ここに-- with mocksというオプションを追加します。

<command>deploy --to h2 --with-mocks --dry &gt; "${project.basedir}/src/main/resources/schema-h2.sql"</command>

4.1.3. モックで実行

以下のコマンドでサービスを実行します。

mvn spring-boot:run

test/test.httpファイルを作成し、以下のリクエストを設定します。

@server=http://localhost:8080
@auth=Basic authenticated:

###
GET {{server}}/odata/v4/BooksService/LocalBooks
Authorization: {{auth}}

設定したモックデータが返されます。

{
  "@context": "$metadata#LocalBooks",
  "@metadataEtag": "W/\"295b951f32c05e27c1fed90764dd1b8d541735a928830475c77c51897dab1fe7\"",
  "value": [
    {
      "ID": 1,
      "title": "Test A",
      "stock": 100
    },
    {
      "ID": 2,
      "title": "Test B",
      "stock": 500
    }
  ]
}

この方法で実行できるのは、イベントハンドラの実装前までです。イベントハンドラの実装後は、4.2.の方法でリモートサービスをモックする必要があります。

4.2. リモートサービスをODataサービスとしてモック

※4.1.の設定が前提

4.2.1. application.xmlの設定

application.yamlにモック用のプロファイルを追加します。ドキュメントにはsuffixの指定がありませんが、これがないとリモートサービスに接続したときに404エラーになってしまいます。

---
spring:
  config.activate.on-profile: mocked
cds:
  application.services:
    CatalogService-mocked:
      model: CatalogService
      serve:
        path: CatalogService      

4.2.2. DestinationConfigurationを追加

リモートサービスをODataサービスとしてモックするときはdestinationを使用します。実際にはこのdestinationが指すのはCAPのサービスそのものなのですが、destinationを使用するために以下のようなJavaのコードが必要になります。

srv/main/java/namespace/config/DestinationCondifuration.java
@Component
@Profile("mocked")
public class DestinationCondifuration {
    @Autowired
    private Environment environment;

    Logger logger = LoggerFactory.getLogger(DestinationCondifuration.class);

    @EventListener
    void applicationReady(ApplicationReadyEvent ready) {
        logger.info("Application is ready, configuring destinations.");
        Integer port = environment.getProperty("local.server.port", Integer.class);
        String destinationName = environment.getProperty("cds.remote.services.[CatalogService].destination.name");
        if(port != null && destinationName != null) {
            DefaultHttpDestination mockDestination = DefaultHttpDestination
            .builder("http://localhost:" + port)
            .basicCredentials(new BasicCredentials("authenticated", ""))
            .name(destinationName).build();

            DestinationAccessor.prependDestinationLoader(
                new DefaultDestinationLoader().registerDestination(mockDestination)
            );
            logger.info("Destination has configured with name: " + destinationName);
        }        
    }   

4.2.3. モックで実行

以下のコマンドでサービスを実行します。

mvn spring-boot:run -Dspring-boot.run.profiles=default,mocked

4.1.3.で作成したリクエストが実行できることを確認します。

5. イベントハンドラの実装

5.1. クラスの形

リモートサービスにアクセスするためのCqnService型の変数を定義します。

@Component
@ServiceName(BooksService_.CDS_NAME)
public class BooksServiceHandler implements EventHandler{
    @Autowired
    @Qualifier(CatalogService_.CDS_NAME)
    CqnService catalogService;

    //メソッドがここに来る
}

5.2. Readリクエスト

Booksエンティティに対するReadリクエストをそのままリモートサービスに渡して実行します。CqnServiceのrunメソッドにコンテクストから取得したクエリを渡しています。

    @On(event = CqnService.EVENT_READ, entity = LocalBooks_.CDS_NAME)
    public Result readBooks(CdsReadEventContext context) {
        return catalogService.run(context.getCqn());
    }

5.3. Createリクエスト

createBookアクションに対するイベントハンドラを実装します。Insert.into...でインサート用のクエリを自分で組み立て、それをCqnServiceのrunメソッドに渡します。

    @On(event = CreateBookContext.CDS_NAME)
    public void createBook(CreateBookContext context) {
        LocalBooks book = context.getBook();
        CqnInsert insert = Insert.into("CatalogService.Books").entry(book); 
        Result result = catalogService.run(insert);
        LocalBooks createdBook = result.single(LocalBooks.class);
        context.setResult(createdBook);      
    }

5.4. Updateリクエスト

updateStockアクションに対するイベントハンドラを実装します。このアクションはBoundアクションなので、/Books(1)/BooksService.updateStockという形でキーが指定されます。このキーを取得するためにCqnAnalyzerなるものを使います。

まず、コンストラクタでCqnAnalyzerのインスタンスを作成します。

    private final CqnAnalyzer analyzer;

    BooksServiceHandler(CdsModel model) {
        this.analyzer = CqnAnalyzer.create(model);
    }

そして、イベントハンドラの中でコンテキストから取得したCQNをアナライザに渡してキーを取得します。

analyzer.analyze(context.getCqn()).targetKeys().get(Books.ID);

updateStockのハンドラは以下のようになります。このアクションには返り値がないので、context.setCompleted()により処理が終わったことをフレームワークに伝えます。

    @On(event = LocalBooksUpdateStockContext.CDS_NAME)
    public void updateStock(LocalBooksUpdateStockContext context) {
        //更新用のbookオブジェクトを作成
        LocalBooks book = LocalBooks.create();
        Integer bookId = (Integer) analyzer.analyze(context.getCqn()).targetKeys().get(LocalBooks.ID);
        book.setId(bookId);
        book.setStock(context.getStock());

        //更新実行
        CqnUpdate update = Update.entity("CatalogService.Books").data(book);
        catalogService.run(update);
        context.setCompleted();
    }

5.5. テスト

Create, Update処理を以下のHTTPリクエストでテストします。サービスは「4.2. リモートサービスをODataサービスとしてモック」の方法で実行してください。

###
POST {{server}}/odata/v4/BooksService/createBook
Content-Type: application/json
Authorization: {{auth}}

{
    "book": {
        "ID": 11,
        "title": "My Test",
        "stock": 99
    }
}

### 
POST {{server}}/odata/v4/BooksService/LocalBooks(1)/BooksService.updateStock
Content-Type: application/json
Authorization: {{auth}}

{
    "stock": 7
}

6. デプロイ

6.1. xsuaa, destinationを追加

以下のコマンドでxsuaaとdestinationサービスを使用するための設定を追加します。リモートサービスがオンプレミスにある場合は、connectivityも追加します。

cds add xsuaa,destination --for production

6.2. mta.yamlを追加

以下のコマンドでmta.yamlファイルを追加します。

cds add mta

6.3. cloud用のプロファイル作成

dbフォルダでドメインモデルを定義していない場合、application.yamlにsql.init.schema-locationsの設定を含まないcloud用のプロファイルを用意します。

---
spring:
  config:
    activate:
      on-profile: cloud
  # sql:
  #   init:
  #     schema-locations: classpath:schema-h2.sql
cds:
  data-source:
    auto-config:
      enabled: false
  remote:
    services:
      '[CatalogService]':
        destination:
          name: catalogservice
        type: odata-v4
        http:
          suffix: /odata/v4   

cds.remote.servicesの設定はdefaultプロフィアイルと重複していますが、これがないとリモートサービスではなくDBを見に行こうとする動きになっていました。

6.3. ビルド、デプロイ

以下のコマンドでビルド、デプロイします。

mbt build -t gen --mtar mta.tar
cf deploy gen/mta.tar

7. 開発環境からリモートサービスに接続

最後はBASなどの開発環境から実際のリモートサービスに接続する方法です。デプロイ時にxsuaaとdestination(およびconnectivity)のサービスインスタンスが登録されているので、それらを利用します。

7.1. サービスにバインド

以下のコマンドで、サービスにバインドします。

cds bind -2  <destinationサービスインスタンス>

※xsuaaサービスインスタンスもバインドした場合、xsuaaによる認証が必要になるためここではスキップ

これにより.cdsrc-private.jsonファイルが作成されます。ここにはhybridプロファイル用の設定が入っています。
image.png

7.3. application.yamlの設定

application.yamlにhybridプロファイルの設定を追加します。

---
spring:
  config.activate.on-profile: hybrid
  sql.init.schema-locations:
  - "classpath:schema-nomocks.sql"
cds:
  remote.services:
    CatalogService:
      destination:
        type: "odata-v4"
        suffix: "/odata/v4"
        name: "catalogservice"

dbフォルダでドメインモデルを定義していない場合、sql.init.schema-locationsの設定は不要です。

7.4. pom.xmlの設定

dbフォルダでドメインモデルを定義していない場合、以下の設定は不要です。

srv/pom.xmlの<command>deploy --to h2 --with-mocks...</command>の後に以下のコマンドを追加します。これは、モックを使わない版のスキーマの設定です。

<command>deploy --to h2 --dry &gt; "${project.basedir}/src/main/resources/schema-nomocks.sql"</command>

7.5. サービスの実行

以下のコマンドでサービスを実行します。

cds bind --exec -- mvn spring-boot:run \
  -Dspring-boot.run.profiles=default,hybrid

以下のリクエストが実行できることを確認します。

@server=http://localhost:8080
@auth=Basic authenticated:

###
GET {{server}}/odata/v4/BooksService/LocalBooks
Authorization: {{auth}}

###
POST {{server}}/odata/v4/BooksService/createBook
Content-Type: application/json
Authorization: {{auth}}

{
    "book": {
        "ID": 11,
        "title": "My Test",
        "stock": 99
    }
}

### 
POST {{server}}/odata/v4/BooksService/LocalBooks(1)/BooksService.updateStock
Content-Type: application/json
Authorization: {{auth}}

{
    "stock": 7
}

おまけ

プロジェクトルートのpackage.jsonに以下のスクリプトを追加すると便利です。

  "scripts": {
    "build": "mbt build -t gen --mtar mtar.tar",
    "deploy": "cf deploy gen/mtar.tar",
    "bind": "cds bind -2 java-consumer-auth,java-consumer-destination",
    "run:mock": "mvn spring-boot:run",
    "run:local-odata": "mvn spring-boot:run -Dspring-boot.run.profiles=default,mocked",
    "run:hybrid": "cds bind --exec -- mvn spring-boot:run -Dspring-boot.run.profiles=default,hybrid"
  }

参考にしたリポジトリ、および記事

1
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?