Node.jsとJavaでのCAP開発の違い

## はじめに
私は普段Node.jsを使ってCAPの開発をしているのですが、Javaでの開発はどのようなものだろうと思い以下のチュートリアルを実施してみました。この記事では、その結果わかったNode.jsでの開発との違いについて記載します。

https://developers.sap.com/mission.cap-java-app.html

## トピック
1. プロジェクトの作り方
1. プロジェクト構成
1. ローカルでの実行方法
1. ODataサービスのURL
1. イベントハンドラの作り方
1. 定数やインターフェースを定義したファイルが自動生成される
1. デプロイの方法
1. 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=<パッケージ名>
```

別の方法として、[ドキュメント](https://cap.cloud.sap/docs/java/getting-started#run-the-cap-java-maven-archetype)には以下のコマンドが紹介されていました。こちらは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](https://docs.spring.io/spring-boot/docs/1.5.16.RELEASE/reference/html/using-boot-devtools.html)を使うと、Javaのソースを変更したときに自動的に再起動が行われます。
- [mvn cds:watch](https://cap.cloud.sap/docs/java/development/#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の場合**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b7fa43bf-0960-452f-fc32-68d033c6ad66.png)
**Javaの場合**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/e52ebb7e-babb-698b-1878-39f6cc474812.png)
Javaでは`odata/v4`が先頭につきます。

### 5. イベントハンドラの作り方
**Node.jsの場合**
srvフォルダの下に`.cds`ファイルと同じ名前で拡張子を`.js`にしたファイルを作成するのが慣例です。イベントハンドラをサービスごとに分けたければ、サービスごとに`.cds`ファイルを分ける必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/69536db6-a4ac-0405-36a4-5eb89193c7ae.png)

**Javaの場合**
handlersフォルダの下にサービスごとに`.java`ファイルを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8540c98d-5d3e-c766-6d1a-7894bf1b41d2.png)
デコレーター`@ServiceName`でサービス名を指定するようになっています。
```java
@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](https://www.npmjs.com/package/cds-routing-handlers)というオープンソースのモジュールがあります。試してみたことがありますが、ローカル開発が難しくて挫折したので、Javaでは標準でこのような仕組みが用意されていてよいと思いました。

### 6. 定数やインターフェースを定義したファイルが自動生成される
Javaの場合、ビルドしたときにsrv/src/gen/.../フォルダに定数やインターフェースを定義したファイルが自動生成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c41dea39-6337-ea02-56bc-ef27c4d3f7d2.png)
ファイルには2種類あり、<サービス名>.javaと<サービス名>_.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_;
```

以下のように処理の中で使うことができます。

```Java
    @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)](https://qiita.com/tami/items/ed4f3fa2ccf27f4fd3e5)

### 7. デプロイの方法
**Node.jsの場合**
mta.yamlファイルを作成して全てのモジュール（db、サービス）を同時にデプロイします。HDIやXSUAAのサービスインスタンスはデプロイ時に作成されます。

**Javaの場合**
Javaの場合（というか、実施したチュートリアルでは）事前にHDIサービスインスタンスを作成し、
`cds deploy`コマンドでHANA Cloudにデプロイします。
サービスモジュールはmanifest.ymlに指定して`cf push`でデプロイします。バインドするXSUAAなどのサービスインスタンスは事前に作成しておきます。cf pushはサービスモジュールだけデプロイするので、比較的早く終わります。

```manifest.yml
---
applications:
- name: bookstore
  path: srv/target/bookstore-exec.jar
  random-route: true
  services:
  - bookstore-hana
  - bookstore-xsuaa
```
※JavaでもMTAによるデプロイは可能です。

https://qiita.com/tami/items/4a90dd6356fa30e7a72f

### 8. Javaのバージョンについて
開発中、Javaのバージョンには何度か足をすくわれました。バージョンはプロジェクトを生成するときに`DjdkVersion`で指定します。指定しない場合は17となっていました。

```xml:pom.xml
	<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にインストールされているバージョンとアンマッチだったためエラーになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d6b489cd-e82e-d14f-192d-f4f3f8799493.png)

バージョンを11に変えると、ビルドは成功しました。しかし今度はデプロイ後、インスタンスの起動でエラーになりました。Cloud Foundryではバージョン8でないと実行できないようです（※）。バージョンを8に変えてビルド、デプロイすると、起動は成功しました。

※SapMachineを使うとJava 11を使うことができます。
https://help.sap.com/docs/btp/sap-business-technology-platform/sapmachine
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/c395a7a5-dae2-c1ec-81e1-76e763018bac.png)

素人考えで、バージョンは新しいほど良いもののように思えるのですが、バージョン17が出ている中で8や11が使われているのはなぜなのでしょうか。別の世界線なのでしょうか。。

## 良かった点、悪かった点
CAPでJavaを使ってみて、Node.jsよりよかった点、悪かった点を挙げます（個人の感想です）。

### 良かった点
- 型付けされているので、コーディング時にエラーに気づくことができ、実行時にスペルミスによるエラーが起きない
- イベントハンドラのファイルを任意の単位で作成できるので、ソースコードを短く保てる
- デプロイ対象がサービスモジュールのみなので、デプロイが早い

### 悪かった点
- ローカルで実行する場合、都度ビルドする必要があり、待ちが発生する
- プロジェクト構成が複雑（イベントハンドラにたどり着くまでの階層が深い）