【2019年11月版】Quarkus + Angular で OpenAPI からクライアント自動生成

# Quarkus で REST API を構築し、Angular用のクライアントを自動生成してみる

今回は Quarkus の OpenAPI エンドポイントから Client Generator を使用して Anuglar 用のクライアントを**自動生成**してみます。

## 対象環境

OS: macOS Mojave v10.14.6
Docker : Docker Desktop v2.1.0.5
RDBMS: PostgreSQL (docker イメージの postgres:latest)
Quarkus: 1.0.0 CR-2
Angular: 8.x.x (Angular-CLIコンテナにお任せ)

特にこだわりはありませんが PostgreSQL を使用します。~~(Adminerとの相性は MySQL、MariaDBの方が良い気がしますが・・・)~~

## 1. Quarku プロジェクトの作成

例によっていつものコマンドで Quarkus プロジェクトを作成します。

```shell
$ mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR2:create
```

って、またバージョン上がってますね？！1.0.0 目前なので急ピッチで開発が進んでいます。頑張れQuarkus!!
例によっていろいろ聞かれますが、適当でOKです。

プラグインもモリモリで追加します。

```shell
$ mvn quarkus:add-extension -Dextensions="quarkus-jdbc-postgresql,quarkus-smallrye-openapi,quarkus-hibernate-orm-panache,jackson-datatype-jsr310,quarkus-resteasy-jsonb"
```

`quarkus-smallrye-openapi` が今回のキモですね。

また、AngularアプリからAPIを叩くためにCORSの設定を追加します。今回はこんな感じです。

```properties:src/main/resources/application.properties
quarkus.http.port=8082
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:4200

quarkus.datasource.url = jdbc:postgresql://localhost:5432/mydatabase
quarkus.datasource.username = postgres
quarkus.datasource.password = postgres

# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation = drop-and-create
```

続いてJavaのコーディングに入っていきましょう。

## 2. REST API の準備

さて、今回はいつものシンプルなモデルではつまらないしモデルを増やすのもアレなので、自モデル参照型の`friends` メンバを追加してみます！

### 2-1. 多対多のリレーションについて

Panache は Hibernate をより使いやすくした ORM マッパーですので、Hiberneteが標準で持つJPA、JTAのAPIはもちろん利用可能です。

多対多の対照表を挟んだリレーションもアノテーションだけでバッチリです。

```java:src/main/java/org/acme/quarkus/sample/model/Person.java
package org.acme.quarkus.sample.model;
...
@Entity
public class Person extends PanacheEntity {
  public enum Status {
    Alive, DECEASED
  }
  
  public String name;
  @JsonFormat(pattern = "yyyy-MM-dd")
  public LocalDate birth;
  public Status status;

  @ManyToMany
  @JoinTable(name = "friends")
  public List<Person> friends;
}
```

一番最後に追加したメンバーに注目です。 `@ManyToMany` と `@JoinTable` に `name = "friends"` 追加しただけですが・・・後ほど、Databaseがどうなるのか、Adminerで確認してみましょう！

### 2-2. JSONのメンバーから外す

ただし、多対多のメンバーはシリアライズの際に邪魔ですので、Transientアノテーションで対象外にします。

```java:src/main/java/org/acme/quarkus/sample/model/Person.java
...
  @JsonbTransient
  @ManyToMany
  @JoinTable(name = "friends")
  public List<Person> friends;
}
```
`@JsonbTransient` アノテーションつけました。これで Jsonシリアライズ時に無限にFriendsを探しに逝ってしまうことはないでしょう。

### 2-3. REST API の実装

今回は簡易に以下のように実装いたしました。

```java:src/main/java/org/acme/quarkus/sample/PersonResource.java
package org.acme.quarkus.sample;
...
@Path("/person")
@Produces(MediaType.APPLICATION_JSON)
public class PersonResource {

    @POST
    @Transactional
    public Person create(Person person) {
        person.persist();
        return person;
    }

    @POST
    @Path("/{id}/fiends")
    @Transactional
    public Person setFriends(@PathParam("id") Long id, Long firends_id) {
        Person person = Person.findById(id);
        person.friends.add(Person.findById(firends_id));
        person.persist();
        return person;
    }

    @GET
    @Path("/{id}")
    @Transactional
    public Person get(@PathParam("id") Long id) {
        return Person.findById(id);
    }

    @GET
    @Path("/{id}/fiends")
    @Transactional
    public List<Person> getFriends(@PathParam("id") Long id) {
        return ((Person)Person.findById(id)).friends;
    }
```

いつもの `create`と`get`に追加で`setFriends`と`getFriends`を追加しました。

## 3. サーバの準備

### 3-1. PostgreSQL と Adminerの起動

たまには docker-compose.yml も載せましょう。PostgreSQLとAdminerを動かす以下のようになります。

```docker-compose.yml
version : "3"
services:
  db:
    image: postgres
    ports:
      - 5432:5432
    environment:
      - POSTGRES_DB=mydatabase
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
```

起動は `docker-compose up -d`でOKです。

### 3-2. quarkus 開発サーバーの起動

PostgreSQLのポート待ち受けが開始したら、以下のコマンドでquarkusの開発サーバーを起動します。今回は(せっかくなので) Swagger UI も確認するためにネイティブビルドはしません。

```shell
$ mvn quarkus:dev
```

これで Hibernate-Panache がテーブルを生成(Drop&Create)してくれます。

### 3-3. Adminer での確認

多対多のリレーションを定義したあの`friends`はどうなっているでしょうか・・・
Adminerの画面で確認してみたいと思います。

ブラウザで `http://localhost:4200` にアクセスし、`postgres/postgres`でログインします。
ログインが成功すると・・・

![スキーマ  public - db - Adminer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/7561e379-f098-0c85-5bd9-a967e1a3e162.png)

ふっ・・・`friends` テーブルが追加されているようですね・・・！！！

こっ、この中身を確認してみましょう！

![テーブル  friends - db - Adminer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/208dc8ef-615f-1d4a-2c50-b25428b9fd52.png)

レコードの構造はエラーで見れませんが(これは他のテーブルでも一緒なのです)、外部キーの制約で `person_id` と `firends_id` が `person(id)` で追加されてますね・・・！！！さ、流石、賢い！！
このように対照表を挟んだ多対多のリレーションも**アノテーションだけでバッチリ**のようです！

## 4. Angular プロジェクトの作成

OpenAPIのクライアントを自動生成する前に、受け皿となるAngulerプロジェクトを作成しましょう。

さらに今回は `ng` コマンドをインストールするのがアレなので、以下のような `alias` を定義してしまいます。

```bash
alias ng='docker run -u $(id -u) -p 4200:4200 --rm -v "$PWD":/app trion/ng-cli ng'
alias npm='docker run -u $(id -u) -p 4200:4200 --rm -v "$PWD":/app trion/ng-cli npm'
```
`ng-cli`と`npm`をDockerコンテナとして動かします。`~/.envrc`などに追記しておけばもっとグッドです。 ~~これで `npm -g ng-cli`から解放されました！！~~
`npm` は `ng` と同じバージョンで動かすために `trion/ng-cli` に同梱の `npm` を使います。(つまり、こういうところが `npm` と `npm -g` を避けたい理由なんです。)

さて、以下のコマンドでプロジェクトを作成します。

```shell
$ ng new myQuarkusAngular
...
$ cd myQuarkusAngular
```

質問の回答は適当でOKです。プロジェクトが生成できたら以下はその中で作業をいたします。

## 5. REST API クライアントを生成

それではいよいよ今回のメインイベント、OpenAPI でのクライアント自動生成です。
今回はジェネレーターにその名もズバリ、`typescript-angular` ジェネレーターを使用します。
で、いくつかパラメーターがあるのですが、全てはリポジトリのREADMEをご覧ください。

- https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/typescript-angular.md

例えばAngularのバージョンを8系にしたいという場合は以下のようにします。

```json:myQuarkusAngular/openapi-config.json
{
   "ngVersion": "8.0.0"
}
```
実際にはデフォルトで`8.0.0`を使用するので指定しなくていいのですが、例としてね。。。
準備が整ったところで、以下のコマンドを実行します。

```shell
$ docker run --rm \
  -v ${PWD}:/local openapitools/openapi-generator-cli generate \
  -i http://docker.for.mac.localhost:8082/openapi \
  -g typescript-angular \
  -o /local/src/app/quarkus-client \
  -c /local/openapi-config.json
...
$ npm i
```

現在のパスを `/local` にマウントしてますので、`myQuarkusAngular/src/app/quarkus-client` 以下にAngular用のコンポーネント？クラス群？がダラダラ〜っと生成されます。
また今回は docker コンテナとしてジェネレーターを動かすので、コンテナの中からホスト側の Quarkus サーバーが見れるように `docker.for.mac.localhost:8082` を使用してます。

クライアントが生成されたところで念の為、`npm i`で追加された関連モジュールのインストール漏れがないようにいたします。

## 6. API クライアントを使う準備

API群とともに READMe.md も生成されてますので、こちらの手順に従っていくつかセットアップを行います。今回のパスでは `myQuarkusAngular/src/app/quarkus-client/README.md` となります。

いくつか手順がありますが、`environment`を使用するパターンで行きましょう。
以下のように`environment`に`API_BASE_PATH`を追加します。

```ts:myQuarkusAngular/src/environments/environment.ts
export const environment = {
  production: false,
  API_BASE_PATH: 'http://localhost:8082'
};
```

続いて `app.modules.ts` に以下のように定義を追加します。

```ts:myQuarkusAngular/src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { ApiModule, BASE_PATH} from "./quarkus-client"
import { HttpClientModule } from '@angular/common/http'

import { AppComponent } from './app.component';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ApiModule,
    HttpClientModule,
    BrowserModule
  ],
  providers: [{ provide: BASE_PATH, useValue: environment.API_BASE_PATH }],
  bootstrap: [AppComponent]
})
export class AppModule { }
```
まず、`imports` で `HttpClientModule` と `ApiModule` を追加しています。この`ApiModule`が生成された Anuglar用のモジュールとなります。
そして `providers` で `BASE_PATH` に上で定義した `environment.API_BASE_PATH` を設定しています。

準備は以上です！

いや〜Anuglar はAOPスタイルで設定値を`組み込んでいく`ので、README.md見ながらじゃないとなかなかキツいっすね。。。

## 7. APIクライアントを使ったコンポーネントの実装

画面の方はざっくりと以下のように実装いたします。

```ts:myQuarkusAngular/src/app/app.component.ts
import { DefaultService } from './quarkus-client';
import { Person } from './quarkus-client/model/models'
import { Component , OnInit} from '@angular/core';
import { traceable } from 'jaeger-tracer-decorator';

@traceable()
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'myQuarkusAngular';

  user : Person;

  constructor(private apiGateway: DefaultService) { }

  @traceable()
  async ngOnInit() {
    this.apiGateway.personIdGet(1,'body').subscribe(p => {
      console.log(`person::${JSON.stringify(p, null, " ")}`);
      this.user = p;});
  }
}
```
`Person`が `quarkus-client/model/models` から import できます。Javaで宣言したモデルクラスがこちらでも**割とそのまま再利用できる**のはありがたいですね。(正確にはJSONとしてシリアライズされるフィールドだけが含まれます。)

そしてコンストラクタで受け取る`DefaultService`。このクラスのメソッドが各REST APIを呼び出すインタフェースとなっております。
メソッドの命名規則はちょっとよくわからないのですが `引数名＋エントリポイントのメソッド名`なのかな？
`/person/get`のエントリポイントは `personIdGet` メソッドにバインドされているようです。
この`personIdGet`の第２引数で戻り値のタイプが選べますが、`body`と指定すると Person 型のJSONそのものが取得できます。
そのほか、"'response'" と指定すると `HttpResponse` 丸ごと取得できるようです。

続いてHTMLの方は質素に以下のように定義します。

```html:myQuarkusAngular/src/app/app.component.html
<h2>user is {{user?.name}}</h2>

<ul>
  <li>Birth: {{user?.birth}}</li>
  <li>Status: {{user?.status}}</li>
</ul>
```
相変わらず・・・適当ですいません。

## 8. 動作確認

まず、`curl` コマンドでデータを投入します。

```shell
$ curl -H 'Content-Type:application/json' -d '{"name":"Alice","birth":"2010-10-11","status":"Alive"}' http://localhost:8082/person
{"id":1,"birth":"2010-10-11","name":"Alice","status":"Alive"}
```
はい、`id = 1`のデータが無事に登録された模様です。もはや当たり前になってしまいましたが、モデル定義と永続化の手続き、そしてAPIの実装がそれぞれちゃんと動作しています。簡単に書けすぎてしまって実感湧かないですが、**コモディティ化**とはそういうものでしょうか？(なんか違う。)

続いて`ng serve --host 0.0.0.0` でAngularの開発サーバーを立ち上げておいてください。
`ng`コマンドはコンテナないで実行されるように `alias` したので `--host 0.0.0.0`で待ち受けのネットワークをどこからでも受け付けるようにしておいてください。

続いて、ブラウザで `http://localhost:4200` を開きます。

![MyQuarkusAngular.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/3b13bea9-ef3b-0d2a-2764-5915694639f6.png)
はい！データ取得できましたね！
Enumの列挙値も文字列でバッチリ表示されていますw
シームレスですごい。。。

## 9. Swagger UI と OpenAPIの注意

ついでに Swagger-UIの方も(久しぶりに)のぞいてみましょう。
ブラウザから `http://localhost:8082/swagegr-ui` で Quarkus が提供する Swagger UI にアクセスできます。

![Swagger UI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/e6196b01-a628-d3e6-710e-607cabf218b7.png)

上記ではプロジェクト作成時に聞かれる `/hello`のAPIを`Yes`として生成された `text/plain` を返却する API ですが、これ実は `typescript-angular` で生成されたクライアントでは**アクセス時にエラーが出てアクセス出来ません！**
中の処理でResponse.Bodyを強制的にJSONにパースしようとしてエラーが出ている模様です。

修正中のようですが、以下のPRで絶賛対応中？炎上中？の模様です。

- https://github.com/OpenAPITools/openapi-generator/pull/3021

また Quarkus の以下の公式手順で紹介されているものですが・・・

- https://quarkus.io/guides/rest-json
- https://quarkus.io/guides/openapi-swaggerui

ここで実装される `Set<Fruit>` を返却する API は Quarkus が `/openapi` にて自動生成するモデル定義で**循環参照**を起こしてしまい、クライアントが生成できません。`List<T>` は `array` に直してくれるのですが**`Set<T>` はダメみたい**です。

今回の試行でたまたま踏んだ地雷ですがまだまだありそうですね・・・
このあたりはまだまだ発展途上といった感じでしょうか。

## まとめ

今回は OpenAPI のジェネレーターからAngular用のクライアントを生成してみることを中心として、Hibernateの永続化処理からSPAでの取得まで一通りの手順を確認いたしました。

サーバーサイドの実装は確実にシンプルに書けるようになり、さらに OpenAPI のジェネレータで一気にクライアントのAPIも生成できてしまいます。便利な世の中になったもんですね〜。。。

今回の成果物も以下のリポジトリに追加いたしました。ご参考にどうぞ〜

- [quarkus-examples ブランチ：panache-openapi](https://github.com/Yoshinori-Koide-PRO/quarkus-examples/tree/panache-openapi)

今回は以上といたします！