はじめに
CAP JavaでリモートのODataサービスを利用したい場合、2つの方法が考えられます。一つはCAPのQuerying APIを使用して、SQLライクな書き方でクエリを実行する方法、もう一つはSAP Cloud SDKのOData Clientを使う方法です。
この記事では、Querying APIを使ってリモートサービスに接続し、データを読み込んだり更新する方法について紹介します。接続先はBTPにDestinationとして登録されていることを前提とします。
CAPireのドキュメントに説明がありますが、ドキュメントでは省略されているステップもあってかなり苦労したので、記事に残しておきます。ソースコードは以下のGitリポジトリにあります。
手順
- ODataサービスの仕様(edmxファイル)を取得
- edmxファイルをインポート
- サービスの定義
- イベントハンドラの実装
- モックで実行
- デプロイ
- 開発環境からリモートサービスに接続
使用するリモート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フォルダに以下のファイルができます。
後からリモートサービスの定義が変更になった場合、作成されたファイルを消して取り込みなおす必要があります。
2.2. application.yamlの設定
srv/src/main/resources/application.yamlに以下の設定を追加します。
---
spring:
config.activate.on-profile: cloud
cds:
remote.services:
CatalogService:
destination:
type: "odata-v4"
suffix: "/odata/v4"
name: "catalogservice" #BTPのdestination
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に以下の設定を追加します。
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-feature-remote-odata</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
<artifactId>sdk-core</artifactId>
</dependency>
3. サービスの定義
以下のサービスを定義しました。Booksエンティティはリモートサービスのエンティティの定義をそのまま使っています。アクションcreateBook
およびupdateBook
により、リモートのBooksエンティティを登録、更新します。
using { CatalogService as external } from './external/CatalogService';
service BooksService {
@readonly
entity Books as projection on external.Books
actions {
action updateStock(stock: Integer);
};
action createBook(book: Books) returns Books;
}
4. イベントハンドラの実装
4.1. クラスの形
リモートサービスにアクセスするためのCqnService型の変数を定義します。
@Component
@ServiceName(BooksService_.CDS_NAME)
public class BooksServiceHandler implements EventHandler{
@Autowired
@Qualifier(CatalogService_.CDS_NAME)
CqnService catalogService;
//メソッドがここに来る
}
4.2. Readリクエスト
Booksエンティティに対するReadリクエストをそのままリモートサービスに渡して実行します。CqnServiceのrunメソッドにコンテクストから取得したクエリを渡しています。
@On(event = CqnService.EVENT_READ, entity = Books_.CDS_NAME)
public Result readBooks(CdsReadEventContext context) {
return catalogService.run(context.getCqn());
}
4.3. Createリクエスト
createBookアクションに対するイベントハンドラを実装します。Insert.into...
でインサート用のクエリを自分で組み立て、それをCqnServiceのrunメソッドに渡します。
@On(event = CreateBookContext.CDS_NAME)
public void createBook(CreateBookContext context) {
Books book = context.getBook();
CqnInsert insert = Insert.into("CatalogService.Books").entry(book);
Result result = catalogService.run(insert);
Books createdBook = result.single(Books.class);
context.setResult(createdBook);
}
4.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 = UpdateStockContext.CDS_NAME)
public void updateStock(UpdateStockContext context) {
//更新用のbookオブジェクトを作成
Books book = Books.create();
Integer bookId = (Integer) analyzer.analyze(context.getCqn()).targetKeys().get(Books.ID);
book.setId(bookId);
book.setStock(context.getStock());
//更新実行
CqnUpdate update = Update.entity("CatalogService.Books").data(book);
catalogService.run(update);
context.setCompleted();
}
5. モックで実行
作成したサービスをローカル(モック)で実行します。CAPでのモック実行には2段階あります。
①簡単なモック(HTTPプロトコルを使わない)
②リモートサービスをODataサービスとしてモック
①は設定は簡単ですが、モックのリモートサービスが実際のODataサービスと同じ動きをしないケースもあるので、より本物に近いテストをする場合は②を使用します。
5.1. 簡単なモック
5.1.1. モックデータを作成
モック用のCSVファイルを作成し、db/dataフォルダに格納します。dbフォルダに格納するのは意外に感じますが、Javaの場合はモックデータをH2データベースにデプロイするためこのような設定になっています。なお、Node.jsの場合はsrv/external/dataフォルダにモックデータを格納します。
ID;title;stock
1;Test A;100
2;Test B;500
5.1.2. pom.xmlの設定
srv/pom.xmlに以下のコマンドがあります。H2データベースにデプロイするためのコマンドです。
<command>deploy --to h2 --dry > "${project.basedir}/src/main/resources/schema-h2.sql"</command>
ここに-- with mocks
というオプションを追加します。
<command>deploy --to h2 --with-mocks --dry > "${project.basedir}/src/main/resources/schema-h2.sql"</command>
5.1.3. モックで実行
以下のコマンドでサービスを実行します。
mvn spring-boot:run
test/test.httpファイルを作成し、以下のリクエストを設定します。
@server=http://localhost:8080
###
GET {{server}}/odata/v4/BooksService/Books
###
POST {{server}}/odata/v4/BooksService/createBook
Content-Type: application/json
{
"book": {
"ID": 11,
"title": "My Test",
"stock": 99
}
}
###
POST {{server}}/odata/v4/BooksService/Books(1)/BooksService.updateStock
Content-Type: application/json
{
"stock": 7
}
すべてのリクエストが正常に実行できます。
5.2. リモートサービスをODataサービスとしてモック
モックデータは5.1.1. で作成したものを使用します。
5.2.1. application.xmlの設定
application.yamlにモック用のプロファイルを追加します。ドキュメントにはsuffixの指定がありませんが、これがないとリモートサービスに接続したときに404エラーになってしまいます。
---
spring:
config.activate.on-profile: mocked
cds:
application.services:
CatalogService-mocked:
model: CatalogService
serve:
path: CatalogService
remote.services:
CatalogService:
destination:
name: "catalogservice-mocked"
suffix: "/odata/v4"
5.2.2. DestinationConfigurationを追加
リモートサービスをODataサービスとしてモックするときはdestinationを使用します。実際にはこのdestinationが指すのはCAPのサービスそのものなのですが、destinationを使用するために以下のようなJavaのコードが必要になります。
@Component
@Profile("mocked")
public class DestinationConfiguration {
@Autowired
private Environment environment;
@EventListener
void applicationReady(ApplicationReadyEvent ready) {
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 httpDestination = DefaultHttpDestination
.builder("http://localhost:" + port)
.basicCredentials(new BasicCredentials("authenticated", ""))
.name(destinationName).build();
DestinationAccessor.prependDestinationLoader(
new DefaultDestinationLoader().registerDestination(httpDestination));
}
}
}
5.2.3. モックで実行
以下のコマンドでサービスを実行します。
mvn spring-boot:run -Dspring-boot.run.profiles=default,mocked
5.1.3.で作成したリクエストが実行できることを確認します。
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. ビルド、デプロイ
以下のコマンドでビルド、デプロイします。
mbt build -t gen --mtar mta.tar
cf deploy gen/mta.tar
7. 開発環境からリモートサービスに接続
最後はBASなどの開発環境から実際のリモートサービスに接続する方法です。デプロイ時にxsuaaとdestination(およびconnectivity)のサービスインスタンスが登録されているので、それらを利用します。
7.1. サービスキーの登録
以下のコマンドでサービスキーを登録します。キーの名前は<サービスインスタンス名>-key
となるようにします。
cf create-service-key <xsuaaサービスインスタンス> <xsuaaサービスインスタンス>-key
cf create-service-key <destinationサービスインスタンス> <destinationサービスインスタンス>-key
7.2. サービスにバインド
以下のコマンドで、サービスにバインドします。
cds bind -2 <xsuaaサービスインスタンス>,<destinationサービスインスタンス>
これにより.cdsrc-private.jsonファイルが作成されます。ここにはhybridプロファイル用の設定が入っています。
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 > "${project.basedir}/src/main/resources/schema-nomocks.sql"</command>
7.5. サービスの実行
以下のコマンドでサービスを実行します。
cds bind --exec -- mvn spring-boot:run \
-Dspring-boot.run.profiles=default,hybrid
5.1.3.で作成したリクエストが実行できることを確認します。
おまけ
プロジェクトルートの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"
}
参考にしたリポジトリ、および記事