【SAP BTP】CAP Service SDK for JavaでOData APIを作ってみる(2)

# はじめに

[CAP(SAP Cloud Application Programming Model)](https://cap.cloud.sap/docs/)を利用してBTP環境にデプロイするODataAPIを作ってみます。（Java）

前回の記事ではサンプルを使ってCAPアプリケーション開発の全体的な流れを確認できたので、今回は自分で定義したデータモデルをサービスとして公開してみます。
また、カスタムロジックでODataリクエスト処理（CRUD処理）を試してみます。
【前回】
[【SAP BTP】CAP Service SDK for JavaでOData APIを作ってみる(1)](https://qiita.com/yonekura-h/items/594de263631726b8881a)

【PostgreSQLにつないでみる】
[【SAP BTP】CAPのAPI(Java)をPostgreSQLで使ってみる](https://qiita.com/yonekura-h/items/a03cc74d95b35d1fab28)

# 前提
(1)があるので事前準備等は参考にしてください
[CAPプロジェクトの作成](https://qiita.com/yonekura-h/items/594de263631726b8881a#cap%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90)までの記載が完了していることを前提としています。


# プロジェクトを新規作成する

CAPのサンプルに追加する形でも問題ないのですが、わかりやすくするために新しいJavaアプリケーションとして作成します。
MTAプロジェクトのルートディレクトリ配下で以下を実行します。

```
$ mvn archetype:generate -DarchetypeArtifactId="cds-services-archetype" -DarchetypeGroupId="com.sap.cds" -DarchetypeVersion="RELEASE" "-DinteractiveMode=true"
```

group ID、artifact ID等は以下を設定しています。
| 内容 | 値 |
|:-:|:-:|
| groupId:  |  jp.co.kyoso.demo |
| artifactId: | customservice  |
| package:  |  ※デフォルト |

`customservice`ディレクトリが作成されます。

# CDSの作成
## データ定義の作成

`srv`を右クリックして「New File」を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/767b5a5b-9fa8-3bbb-3061-58ba92af7306.png)

`user-service.cds`という名称で.cdsファイルを作成します。
内容は以下の通りです。
```cds
service UserService {
    entity Users {
        key ID : Integer;
        FirstName : String(100);
        LastName : String(100);
        Age : Integer;
    }
}
```

.cdsファイルにはODataサービスとして公開したいデータ定義を記載します。

次に、作成した.cdsファイルのデータ定義をコンパイルしてmetadataのxmlを作成してもらいます。
Javaアプリケーションのルートディレクトリに移動し、以下のコマンドを実行します。
```
$ cd customservice/
$ mvn clean install
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/97b4c4b4-5778-1e10-8ddc-b7c893c6493e.png)

Build Successすると`gen` 、`edmx`ディレクトリが作成され、
データ定義をもととしたJavaクラスとmetadata.xmlが出来上がります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/c9a9054e-df4e-6023-3e06-7472c6271594.png)


## テスト実行①
この段階で一度テスト実行をします。

Javaアプリケーションのルートディレクトリで以下のコマンドを実行します。
```
$ mvn spring-boot:run
```

成功するとサービスが立ち上がり、metadataを参照できることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/00bd0fd7-dea1-de78-1e4a-6b62a5767320.png)

# イベントハンドラーの作成

## Javaクラスを作成
CDSを作成できたので、次はODataAPIによるCRUD操作を処理するイベントハンドラーを作成します。
`srv/src/main/java` ディレクトリの中にあるApplication.javaの存在する階層に`handlers`ディレクトリを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/1dffdccb-d70b-c150-055c-d801bbe7399e.png)

作成した`handlers`ディレクトリの中に新しいJavaクラスファイル`UserService.java`を作成します。


```java:UserService.java
package jp.co.kyoso.demo.customservice.handlers;

import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Component;

import com.sap.cds.services.cds.CdsCreateEventContext;
import com.sap.cds.services.cds.CdsReadEventContext;
import com.sap.cds.services.cds.CdsService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.ServiceName;

@Component
@ServiceName("UserService")
public class UserService implements EventHandler {
    
    private Map<Object, Map<String, Object>> users = new HashMap<>();

    // UsersエンティティのCreateイベント
    @On(event = CdsService.EVENT_CREATE, entity = "UserService.Users")
    public void onCreate(CdsCreateEventContext context) {
        context.getCqn().entries().forEach(e -> users.put(e.get("ID"), e));
        context.setResult(context.getCqn().entries());
    }

    // UsersエンティティのReadイベント
    @On(event = CdsService.EVENT_READ, entity = "UserService.Users")
    public void onRead(CdsReadEventContext context) {
        context.setResult(users.values());
    }

}
``` 

## テスト実行②
イベントハンドラーを追加したので、再度テスト実行を行います。
先ほど実行したアプリケーションが生きている場合は一度停止し、再起動します。

Usersエンティティ（/odata/v4/UserService/Users）へアクセスするとデータが存在しない状態です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/6915b99c-b852-40e0-3be8-22c75238361f.png)

イベントハンドラーにはCreateイベントの処理を登録しているので、POSTメソッドでHTTPリクエストを実施し、データを追加してみます。

> PostMan等のRestクライアントを利用する場合はDeveloper Toolから認証情報のCookie(workspaces-xxxxxxx_WSR_SESSIONID)を取ってきて設定します。
【Cookieの参照】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/01f71c21-3033-eb56-a003-c2c59b3e73ae.png)
【Cookieの設定】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/b7015a73-d253-0179-b52b-f1f1b8d4ca92.png)

HTTPヘッダに`Content-Type : application/json` を設定し、以下のBodyをPOSTで送信します。
```json
{
    "ID": 1, 
    "FirstName": "KYOSO", 
    "LastName": "花子",
    "Age": 20
}
```
レスポンスがHTTPステータス：201で返却されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/4e2822fe-fcce-984c-51a1-2b54084d7b02.png)

POSTでのリクエストを実行後、再度GETリクエストを実行すると先ほど登録したデータが参照できることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/91ae9683-fa53-c584-a817-6c587a868f07.png)


# DBへの接続

APIが実行できることを確認できたのですが、この状態でDeploy等をしてもまだDBへつながっていない状態なので永続的なデータとしては扱えません。
DBへ接続できるようにしていきます。

## CDSファイルの作り替え
`db`ディレクトリ配下に`schema.cds`ファイルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/50794212-945d-23a1-c479-5a27b29d9417.png)

`schema.cds`に以下を記載します。
[@sap/cds/common](https://cap.cloud.sap/docs/cds/common#why-use-sapcdscommon)にはCDSで利用できる様々な機能があります。
今回はmanaged をつけてみました。

```schema.cds
namespace demo;

using { managed } from '@sap/cds/common';

entity Users : managed {
    key ID    : Integer;
    FirstName : String(100);
    LastName  : String(100);
    Age       : Integer;
}

```

`schema.cds`にデータ構造定義を記載したので、最初に作っていた`user-service.cds`の中身を修正して`schema.cds`を参照するようにします。
```user-service.cds
using { demo as db } from '../db/schema';

service UserService {
    entity Users   as projection on db.Users;
}
```

## テストデータを準備する

ついでなので、テスト実行用のデータファイルを準備します。
`db/data`ディレクトリを作成し、配下に`demo-Users.csv`を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/b8645f0b-40f9-5d60-a2e3-21aec51a5874.png)

> .csvファイルの命名規則は`schema.cds`に記載した内容から設定する必要があり、フォーマットは`<namespace>-<entity>.csv`です。
今回はnamespaceがdemoで用意するデータの対象entityがdemoなので`demo-Users.csv`になります。

```demo-Users.csv
ID;FirstName;LastName;Age
1;csv;太郎;100
2;csv;次郎;101
3;csv;三郎;99
```

## イベントハンドラーの編集

CAP SDKを使ったDB接続にはODataサービスに必要な機能がそろっているとのことで、イベントハンドラーが不要になるみたいです。
そのため、先ほど作成した`handler/UserService.java`がなくてもOData APIとして基本的な機能は動くようになりました。

ただ、カスタムロジックを入れたいときはイベントハンドラーが必要になります。
学習目的でデータ取得時の結果を編集してみます。

```java:UserService.java
package jp.co.kyoso.demo.customservice.handlers;

import java.util.stream.Stream;

import org.springframework.stereotype.Component;

import com.sap.cds.services.cds.CdsService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.After;
import com.sap.cds.services.handler.annotations.ServiceName;

import cds.gen.userservice.Users;

@Component
@ServiceName("UserService")
public class UserService implements EventHandler {
    
	@After(event = CdsService.EVENT_READ, entity = "UserService.Users")
	public void addText(Stream<Users> users) {
        // Ageが100歳以上の場合はLastNameに" (over 100!!)"を追記します。
		users.filter(b -> b.getAge() > 100).forEach(b -> b.setLastName(b.getLastName() + " (over 100!!)"));
	}
}

```

## テスト実行③
またテスト実行を行います。
先ほど実行したアプリケーションが生きている場合は一度停止し、再起動します。

Usersエンティティ（/odata/v4/UserService/Users）へアクセスするとテストデータが取得できます。
また、イベントハンドラーに追加したカスタムロジックも機能しています。
（csv次郎さんに" (over 100!!)"がついてます。）

schema.cdsに@sap/cds/commonのmanagedをつけているので、定義には含まれてないですがcreatedAt,createdBy,modifiedAt,modifiedByのカラムが追加されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/92562fe2-a7cb-74ad-34da-f5472d6d0356.png)

ロジックは入れていませんが、POSTによる追加も可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/d18b6bfd-a370-1bf5-20c1-655fe89b88cb.png)


# BTP環境へデプロイ

DBへの接続もできるようになったと思うので、MTAをBuildしてデプロイします。

mta.yamlに以下を記載します。
```yaml:mya.yaml
_schema-version: "3.2"
ID: mtademo
version: 0.0.1
modules:
  # サンプルのJavaModule（不要）
  # - name: capdemo_api
  #   type: java
  #   path: capdemoservice
  #   parameters:
  #     buildpack: java_buildpack
  #   properties:
  #     SPRING_PROFILES_ACTIVE: cloud
  #   build-parameters:
  #     builder: custom
  #     commands: [ mvn clean package ]
  #     build-result: "srv/target/*-exec.[wj]ar"
  #   provides:
  #     - name: srv-api # required by consumers of CAP services (e.g. approuter)
  #       properties:
  #         srv-url: ${default-url}
  #   requires:
  #    - name: useDemoShared
  #      properties:
  #         JBP_CONFIG_RESOURCE_CONFIGURATION: '[tomcat/webapps/ROOT/META-INF/context.xml:
  #            {"service_name_for_DefaultDB" : "~{hdi-container-name}"}]'

  # customserviceのJavaModule
  - name: capCustom_api
    type: java
    path: customservice
    parameters:
      buildpack: java_buildpack
    properties:
      SPRING_PROFILES_ACTIVE: cloud
    build-parameters:
      builder: custom
      commands: [ mvn clean package ]
      build-result: "srv/target/*-exec.[wj]ar"
    provides:
      - name: capCustom-api # required by consumers of CAP services (e.g. approuter)
        properties:
          srv-url: ${default-url}
    # resources -> useDemoSharedに定義したHANA DBを使う
    requires:
     - name: useDemoShared
       properties:
          JBP_CONFIG_RESOURCE_CONFIGURATION: '[tomcat/webapps/ROOT/META-INF/context.xml:
             {"service_name_for_DefaultDB" : "~{hdi-container-name}"}]'
resources:
  # 作成済みのHANA DBサービスをresourcesに記載
  - name: useDemoShared
    parameters:
      service-name: demoShared
      service: hanatrial
      service-plan: securestore
      config:
        schema: sampleSchema
    properties:
      hdi-container-name: '${service-name}'
    type: org.cloudfoundry.existing-service
```
MTAをBuildしてデプロイを実行します。
```
$ mbt build
$ cf deploy mta_archives/mtademo_0.0.1.mtar
```

### HANA DBにテーブル追加

APIを実行する前にCDSに定義されているデータ構造をHANA DBに作成します。
```sql
CREATE TABLE demo_Users (
  ID INTEGER NOT NULL,
  FirstName NVARCHAR(100),
  LastName NVARCHAR(100),
  Age INTEGER,
  createdAt TIMESTAMP,
  createdBy NVARCHAR(255),
  modifiedAt TIMESTAMP,
  modifiedBy NVARCHAR(255),
  PRIMARY KEY(ID)
);

CREATE VIEW UserService_Users AS SELECT
  Users_0.createdAt,
  Users_0.createdBy,
  Users_0.modifiedAt,
  Users_0.modifiedBy,
  Users_0.ID,
  Users_0.FirstName,
  Users_0.LastName,
  Users_0.Age
FROM demo_Users AS Users_0;

INSERT INTO "DEMO_USERS" VALUES(
	1/*ID <INTEGER>*/,
	'KYOSO'/*FIRSTNAME <NVARCHAR(100)>*/,
	'太郎'/*LASTNAME <NVARCHAR(100)>*/,
	49/*AGE <INTEGER>*/,
	''/*CREATEDAT <TIMESTAMP>*/,
	'HANA DB'/*CREATEDBY <NVARCHAR(255)>*/,
	''/*MODIFIEDAT <TIMESTAMP>*/,
	'HANA DB'/*MODIFIEDBY <NVARCHAR(255)>*/
);
INSERT INTO "DEMO_USERS" VALUES(
	2/*ID <INTEGER>*/,
	'KYOSO'/*FIRSTNAME <NVARCHAR(100)>*/,
	'次郎'/*LASTNAME <NVARCHAR(100)>*/,
	120/*AGE <INTEGER>*/,
	''/*CREATEDAT <TIMESTAMP>*/,
	'HANA DB'/*CREATEDBY <NVARCHAR(255)>*/,
	''/*MODIFIEDAT <TIMESTAMP>*/,
	'HANA DB'/*MODIFIEDBY <NVARCHAR(255)>*/
);
```

demo_Usersテーブルを作成し、データを投入しました。
UserService_UsersのViewも作成しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/09305939-882f-56dd-970a-c7919e5d2473.png)

### BTPにデプロイしたOData APIを実行する

デプロイに成功したら実行してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/9b4858a5-dd80-68ed-1fa5-52ffd897cdee.png)

HANA DBに投入した値が取得できることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/4a6224ff-a71f-a91f-3886-bab405894309.png)

$filterのクエリオプションも実行可能です。(/odata/v4/UserService/Users?$filter=ID eq 1)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/299bd370-78a3-8589-f694-bc8cac5fd152.png)



ついでにKYOSO花子さんのデータをPOSTメソッドで登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/b3228757-d5ff-a822-d889-2421dc6e567b.png)

DBに登録できることが確認できました。
自動で登録・更新日付等も登録されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159467/37bab9ab-a527-f149-c93a-4dd14c429f2a.png)


# おわりに
CAP Service SDK for Javaを使ってODataAPIが構築できました。
CDS等はもっと多くの機能があります。また、本来であればDBのテーブル等はデータ定義をデプロイして作成するべきところなのかなと思っています。
実際に開発していくとなると調査すべき課題が多いので、調べていきたいと思います。