はじめに
私は普段Node.jsを使ってCAPの開発をしているのですが、Javaでの開発はどのようなものだろうと思い以下のチュートリアルを実施してみました。この記事では、その結果わかったNode.jsでの開発との違いについて記載します。
トピック
- プロジェクトの作り方
- プロジェクト構成
- ローカルでの実行方法
- ODataサービスのURL
- イベントハンドラの作り方
- 定数やインターフェースを定義したファイルが自動生成される
- デプロイの方法
- Javaのバージョンについて
1. CAPプロジェクトの作り方
Node.jsの場合
cds init <プロジェクト名>
Javaの場合
mvn -B archetype:generate -DarchetypeArtifactId=cds-services-archetype -DarchetypeGroupId=com.sap.cds \
-DarchetypeVersion=RELEASE -DjdkVersion=11 \
-DgroupId=<グループID> -DartifactId=<プロジェクト名> -Dpackage=<パッケージ名>
別の方法として、ドキュメントには以下のコマンドが紹介されていました。こちらはNode.jsに近いです。
cds init <PROJECT-ROOT> --add java
2. プロジェクト構成
以下に主要なディレクトリ、ファイルだけ抜粋したJavaのプロジェクト構成を示します。DB用のCDS(schema.cds)とサービス用のCDS(services.cds)の定義方法はNode.jsと同じです。
違いとしては、イベントハンドラのソースが/srv/src/java/.../と深い階層にある点、package.jsonの代わりにpom.xmlを使って依存するモジュールを指定する点です。また、デプロイにはmta.yamlではなくmanifest.ymlを使います(後述)。
.
├── db
│ ├── data
│ ├── src
│ └── schema.cds
├── srv
│ ├── src
│ │ └── main
│ │ └── java
│ │ └── ...
│ │ └── handlers
│ │ └── OrdersService.java
│ │ └── Application.java
│ ├── pom.xml
│ └── services.cds
├── .cdsrc.json
├── manifest.yml
├── package.json
└── pom.xml
3. ローカルでの実行方法
Node.jsの場合
cds watch
Javaの場合
//初回はまずビルドする
mvn clean install
//アプリケーションを起動する
mvn clean spring-boot:run
Node.jsではcds watch
コマンドを1回実行すると、何か変更をするたび自動的に再起動がかかります。一方Javaは、変更を反映させるためには都度mvn clean spring-boot:run
を実行する必要があります。
2023/10/11追記
- spring-boot-devtoolsを使うと、Javaのソースを変更したときに自動的に再起動が行われます。
-
mvn cds:watchを使うと、.cdsファイルを変更したとき自動的に再起動が行われます。
これらのツールは併用することができます。
4. ODataサービスのURL
※CAPのversion7からは、Node.jsのサービスパスがJavaに合わせてodata/v4
となりました。
https://cap.cloud.sap/docs/releases/archive/2023/jun23#changed-default-service-path
Node.jsの場合
Javaの場合
Javaではodata/v4
が先頭につきます。
5. イベントハンドラの作り方
Node.jsの場合
srvフォルダの下に.cds
ファイルと同じ名前で拡張子を.js
にしたファイルを作成するのが慣例です。イベントハンドラをサービスごとに分けたければ、サービスごとに.cds
ファイルを分ける必要があります。
Javaの場合
handlersフォルダの下にサービスごとに.java
ファイルを作ります。
デコレーター@ServiceName
でサービス名を指定するようになっています。
@Component
@ServiceName(OrdersService_.CDS_NAME)
public class OrdersService implements EventHandler{
@Autowired
PersistenceService db;
@Before(event = CdsService.EVENT_CREATE, entity = OrderItems_.CDS_NAME)
似たような仕組みをNode.jsで実現するためのツールとして、cds-routing-handlersというオープンソースのモジュールがあります。試してみたことがありますが、ローカル開発が難しくて挫折したので、Javaでは標準でこのような仕組みが用意されていてよいと思いました。
6. 定数やインターフェースを定義したファイルが自動生成される
Javaの場合、ビルドしたときにsrv/src/gen/.../フォルダに定数やインターフェースを定義したファイルが自動生成されます。
ファイルには2種類あり、<サービス名>.javaと<サービス名>_.javaの形式のものがあります。よく似ていますが違いはアンダースコアの有無です。
アンダースコア付きの方は、サービスで定義されたエンティティや項目の名前(すなわち定数)を定義したファイルです。アンダースコアなしの方は、エンティティのデータにアクセスするためのセッターやゲッターなどが定義されています。
使い方
イベントハンドラの中でこれらのファイルをインポートします。
import cds.gen.ordersservice.OrderItems;
import cds.gen.ordersservice.OrderItems_;
import cds.gen.ordersservice.Orders;
import cds.gen.ordersservice.Orders_;
import cds.gen.sap.capire.bookstore.Books;
import cds.gen.sap.capire.bookstore.Books_;
以下のように処理の中で使うことができます。
@Before(event = CdsService.EVENT_CREATE, entity = OrderItems_.CDS_NAME) //定数を使用
public void validateBookAndDecreaseStock(List<OrderItems> items) { //インターフェースを使用
for (OrderItems item : items) {
String bookId = item.getBookId(); //インターフェースを使用
Integer amount = item.getAmount(); //インターフェースを使用
// check if the book that should be ordered is existing
CqnSelect sel = Select.from(Books_.class).columns(b -> b.stock()).where(b -> b.ID().eq(bookId)); //定数を使用
Books book = db.run(sel).first(Books.class)
.orElseThrow(() ->new ServiceException(ErrorStatuses.NOT_FOUND, "Book does not exist"));
これにより、項目名などをハードコーディングする必要がなくなります。スペルミスをすればコーディングしているときに気づくので、実行時にエラーが起きることがなくなります。この仕組みをNode.jsでも取り入れたのがtypeScriptなのだと実感しました。
関連記事:CAPでTypeScriptを使ってみる (cds2types)
7. デプロイの方法
Node.jsの場合
mta.yamlファイルを作成して全てのモジュール(db、サービス)を同時にデプロイします。HDIやXSUAAのサービスインスタンスはデプロイ時に作成されます。
Javaの場合
Javaの場合(というか、実施したチュートリアルでは)事前にHDIサービスインスタンスを作成し、
cds deploy
コマンドでHANA Cloudにデプロイします。
サービスモジュールはmanifest.ymlに指定してcf push
でデプロイします。バインドするXSUAAなどのサービスインスタンスは事前に作成しておきます。cf pushはサービスモジュールだけデプロイするので、比較的早く終わります。
---
applications:
- name: bookstore
path: srv/target/bookstore-exec.jar
random-route: true
services:
- bookstore-hana
- bookstore-xsuaa
※JavaでもMTAによるデプロイは可能です。
8. Javaのバージョンについて
開発中、Javaのバージョンには何度か足をすくわれました。バージョンはプロジェクトを生成するときにDjdkVersion
で指定します。指定しない場合は17となっていました。
<properties>
<!-- OUR VERSION -->
<revision>1.0.0-SNAPSHOT</revision>
<!-- DEPENDENCIES VERSION -->
<jdk.version>17</jdk.version>
<cds.services.version>1.34.1</cds.services.version>
<spring.boot.version>2.7.11</spring.boot.version>
<cds.install-cdsdk.version>6.8.0</cds.install-cdsdk.version>
<cds.install-node.downloadUrl>https://nodejs.org/dist/</cds.install-node.downloadUrl>
</properties>
これをビルドした際、BASにインストールされているバージョンとアンマッチだったためエラーになりました。
バージョンを11に変えると、ビルドは成功しました。しかし今度はデプロイ後、インスタンスの起動でエラーになりました。Cloud Foundryではバージョン8でないと実行できないようです(※)。バージョンを8に変えてビルド、デプロイすると、起動は成功しました。
※SapMachineを使うとJava 11を使うことができます。
https://help.sap.com/docs/btp/sap-business-technology-platform/sapmachine
素人考えで、バージョンは新しいほど良いもののように思えるのですが、バージョン17が出ている中で8や11が使われているのはなぜなのでしょうか。別の世界線なのでしょうか。。
良かった点、悪かった点
CAPでJavaを使ってみて、Node.jsよりよかった点、悪かった点を挙げます(個人の感想です)。
良かった点
- 型付けされているので、コーディング時にエラーに気づくことができ、実行時にスペルミスによるエラーが起きない
- イベントハンドラのファイルを任意の単位で作成できるので、ソースコードを短く保てる
- デプロイ対象がサービスモジュールのみなので、デプロイが早い
悪かった点
- ローカルで実行する場合、都度ビルドする必要があり、待ちが発生する
- プロジェクト構成が複雑(イベントハンドラにたどり着くまでの階層が深い)