はじめに
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
手順
- ODataサービスの仕様(edmxファイル)を取得
- edmxファイルをインポート
- サービスの定義
- モックで実行
- イベントハンドラの実装
- デプロイ
- 開発環境からリモートサービスに接続
※@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フォルダに以下のファイルができます。
後からリモートサービスの定義が変更になった場合、作成されたファイルを消して取り込みなおす必要があります。
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エンティティを登録、更新します。
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フォルダにモックデータを格納します。
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 > "${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>
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のコードが必要になります。
@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プロファイル用の設定が入っています。
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
以下のリクエストが実行できることを確認します。
@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"
}
参考にしたリポジトリ、および記事