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 プロジェクトを作成します。
$ mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR2:create
って、またバージョン上がってますね?!1.0.0 目前なので急ピッチで開発が進んでいます。頑張れQuarkus!!
例によっていろいろ聞かれますが、適当でOKです。
プラグインもモリモリで追加します。
$ 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の設定を追加します。今回はこんな感じです。
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はもちろん利用可能です。
多対多の対照表を挟んだリレーションもアノテーションだけでバッチリです。
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アノテーションで対象外にします。
...
@JsonbTransient
@ManyToMany
@JoinTable(name = "friends")
public List<Person> friends;
}
@JsonbTransient
アノテーションつけました。これで Jsonシリアライズ時に無限にFriendsを探しに逝ってしまうことはないでしょう。
2-3. REST API の実装
今回は簡易に以下のように実装いたしました。
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を動かす以下のようになります。
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 も確認するためにネイティブビルドはしません。
$ mvn quarkus:dev
これで Hibernate-Panache がテーブルを生成(Drop&Create)してくれます。
3-3. Adminer での確認
多対多のリレーションを定義したあのfriends
はどうなっているでしょうか・・・
Adminerの画面で確認してみたいと思います。
ブラウザで http://localhost:4200
にアクセスし、postgres/postgres
でログインします。
ログインが成功すると・・・
ふっ・・・friends
テーブルが追加されているようですね・・・!!!
こっ、この中身を確認してみましょう!
レコードの構造はエラーで見れませんが(これは他のテーブルでも一緒なのです)、外部キーの制約で person_id
と firends_id
が person(id)
で追加されてますね・・・!!!さ、流石、賢い!!
このように対照表を挟んだ多対多のリレーションもアノテーションだけでバッチリのようです!
4. Angular プロジェクトの作成
OpenAPIのクライアントを自動生成する前に、受け皿となるAngulerプロジェクトを作成しましょう。
さらに今回は ng
コマンドをインストールするのがアレなので、以下のような alias
を定義してしまいます。
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
を避けたい理由なんです。)
さて、以下のコマンドでプロジェクトを作成します。
$ ng new myQuarkusAngular
...
$ cd myQuarkusAngular
質問の回答は適当でOKです。プロジェクトが生成できたら以下はその中で作業をいたします。
5. REST API クライアントを生成
それではいよいよ今回のメインイベント、OpenAPI でのクライアント自動生成です。
今回はジェネレーターにその名もズバリ、typescript-angular
ジェネレーターを使用します。
で、いくつかパラメーターがあるのですが、全てはリポジトリのREADMEをご覧ください。
例えばAngularのバージョンを8系にしたいという場合は以下のようにします。
{
"ngVersion": "8.0.0"
}
実際にはデフォルトで8.0.0
を使用するので指定しなくていいのですが、例としてね。。。
準備が整ったところで、以下のコマンドを実行します。
$ 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
を追加します。
export const environment = {
production: false,
API_BASE_PATH: 'http://localhost:8082'
};
続いて app.modules.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クライアントを使ったコンポーネントの実装
画面の方はざっくりと以下のように実装いたします。
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
の第2引数で戻り値のタイプが選べますが、body
と指定すると Person 型のJSONそのものが取得できます。
そのほか、"'response'" と指定すると HttpResponse
丸ごと取得できるようです。
続いてHTMLの方は質素に以下のように定義します。
<h2>user is {{user?.name}}</h2>
<ul>
<li>Birth: {{user?.birth}}</li>
<li>Status: {{user?.status}}</li>
</ul>
相変わらず・・・適当ですいません。
8. 動作確認
まず、curl
コマンドでデータを投入します。
$ 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
を開きます。
はい!データ取得できましたね!
Enumの列挙値も文字列でバッチリ表示されていますw
シームレスですごい。。。
9. Swagger UI と OpenAPIの注意
ついでに Swagger-UIの方も(久しぶりに)のぞいてみましょう。
ブラウザから http://localhost:8082/swagegr-ui
で Quarkus が提供する Swagger UI にアクセスできます。
上記ではプロジェクト作成時に聞かれる /hello
のAPIをYes
として生成された text/plain
を返却する API ですが、これ実は typescript-angular
で生成されたクライアントではアクセス時にエラーが出てアクセス出来ません!
中の処理でResponse.Bodyを強制的にJSONにパースしようとしてエラーが出ている模様です。
修正中のようですが、以下のPRで絶賛対応中?炎上中?の模様です。
また Quarkus の以下の公式手順で紹介されているものですが・・・
ここで実装される Set<Fruit>
を返却する API は Quarkus が /openapi
にて自動生成するモデル定義で循環参照を起こしてしまい、クライアントが生成できません。List<T>
は array
に直してくれるのですが**Set<T>
はダメみたい**です。
今回の試行でたまたま踏んだ地雷ですがまだまだありそうですね・・・
このあたりはまだまだ発展途上といった感じでしょうか。
まとめ
今回は OpenAPI のジェネレーターからAngular用のクライアントを生成してみることを中心として、Hibernateの永続化処理からSPAでの取得まで一通りの手順を確認いたしました。
サーバーサイドの実装は確実にシンプルに書けるようになり、さらに OpenAPI のジェネレータで一気にクライアントのAPIも生成できてしまいます。便利な世の中になったもんですね〜。。。
今回の成果物も以下のリポジトリに追加いたしました。ご参考にどうぞ〜
今回は以上といたします!