こんにちは。@masatomix です。
最近、現場でGraphQLとRESTサーバを構築する機会があったので、そのとき得られたナレッジを備忘としてまとめておこうと思います。
TL;DR
- RESTサーバとGraphQLサーバを構築します
- EntityとRepositoryから RESTサービスを自動生成します(Spring Data RESTの機能)
- EntityからDDLを実行してテーブルの作成、データ投入を行います(SpringBootとJPAの機能)
- JPAとSpring Data RESTの機能を使って
- ManyToOneの関連があるRESTサービスを自動生成します
- Swggger(OpenAPI)を有効にします。API仕様書のJSONもダウンロード出来ます
- openapi-generator をつかってRESTにアクセスするプロキシ(TypeScript)を自動生成します
- Apollo Serverの機能を使って
- GraphQLのリクエストを受け取って、RESTからデータを取得してGraphQLで返却しています。
Spring Data RESTは、JPAのRepositoryまで作成すればServiceやControllerを作成せずにRESTサービスとして公開してくれるフレームワークです。
今回RESTサーバはこの機構を使ってみています。
環境
ではHelloworldレベルですが、実際に操作できる環境を作ります。
システムの全体構成は以下の通りです。
BackendがCRUDするテーブルは以下の通り。
このように、会社には複数のユーザが属している、そんなデータ構造です。
BFFとして公開するGraphQLのスキーマは以下のようにしてみました
type User{
id: ID!
firstName: String
lastName: String
email: String
age: Int
company: String
}
type Company{
code: ID!
name: String
}
開発の臨場感を出そうと、user_id → id, company_code → code など微妙にテーブルの項目名じゃない名前を定義しています。
事前準備
動かすにはJava/Node.jsなど、いくつか実行環境が必要です。
使うモノは以下の通り。
$ java --version
openjdk 17.0.11 2024-04-16
OpenJDK Runtime Environment (build 17.0.11+9-Ubuntu-120.04.2)
OpenJDK 64-Bit Server VM (build 17.0.11+9-Ubuntu-120.04.2, mixed mode, sharing)
$ node -v
v20.5.0
OpenAPIからソースコードを生成するため、下記のライブラリも使用します。
$ openapi-generator-cli version
7.6.0
$
記事の最後にインストール方法も記載しておきますので、いったんこれらはインストール済みとしてすすめていきます。
やってみる
ソースをcloneしてきます。
$ git clone -b init https://github.com/masatomix/spring-data-rest-example.git
Cloning into 'spring-data-rest-example'...
remote: Enumerating objects: 149, done.
remote: Counting objects: 100% (149/149), done.
remote: Compressing objects: 100% (95/95), done.
remote: Total 149 (delta 37), reused 133 (delta 21), pack-reused 0
Receiving objects: 100% (149/149), 117.60 KiB | 10.69 MiB/s, done.
Resolving deltas: 100% (37/37), done.
$ cd spring-data-rest-example/
$
まずはBackendサーバ
まずはBackend から。SpringBootサーバを起動してみます。
$ ./gradlew clean bootRun
> Task :bootRun
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.5)
2024-06-08T14:10:30.956+09:00 INFO 4684 --- [demo] [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.11 with PID 4684 (/home/sysmgr/spring-data-rest-example/build/classes/java/main started by sysmgr in /home/sysmgr/spring-data-rest-example)
...
: HHH000412: Hibernate ORM core version 6.4.4.Final
2024-06-08T14:10:31.934+09:00 INFO 4684 --- [demo] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2024-06-08T14:10:32.027+09:00 INFO 4684 --- [demo] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2024-06-08T14:10:32.397+09:00 INFO 4684 --- [demo] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-06-08T14:10:32.403+09:00 DEBUG 4684 --- [demo] [ main] org.hibernate.SQL
: drop table if exists app_user cascade
2024-06-08T14:10:32.405+09:00 DEBUG 4684 --- [demo] [ main] org.hibernate.SQL
: drop table if exists company cascade
2024-06-08T14:10:32.408+09:00 DEBUG 4684 --- [demo] [ main] org.hibernate.SQL
: create table app_user (age integer not null, user_id varchar(6) not null, company_code varchar(255) not null, email varchar(255), first_name varchar(255), last_name varchar(255), primary key (user_id))
2024-06-08T14:10:32.411+09:00 DEBUG 4684 --- [demo] [ main] org.hibernate.SQL
: create table company (company_code varchar(255) not null, company_name varchar(255), primary key (company_code))
2024-06-08T14:10:32.412+09:00 DEBUG 4684 --- [demo] [ main] org.hibernate.SQL
: alter table if exists app_user add constraint FK7hjs5p84vn7rc6tj2nssqyi50 foreign key (company_code) references company
2024-06-08T14:10:32.419+09:00 INFO 4684 --- [demo] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-06-08T14:10:32.540+09:00 INFO 4684 --- [demo] [ main] o.s.d.j.r.query.QueryEnhancerFactory : Hibernate is in classpath; If applicable, HQL parser will be used.
2024-06-08T14:10:32.749+09:00 WARN 4684 --- [demo] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-06-08T14:10:33.128+09:00 INFO 4684 --- [demo] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
2024-06-08T14:10:33.134+09:00 INFO 4684 --- [demo] [ main] com.example.demo.DemoApplication : Started DemoApplication in 2.379 seconds (process running for 2.576)
<==========---> 83% EXECUTING [20s]
> :bootRun
起動できたようです。
などにアクセスしてみましょう。CURLとかだとこんな感じ。
$ curl http://localhost:8080/users | jq
{
"_embedded": {
"user": [
{
"userId": "u001",
"firstName": null,
"lastName": "木野1",
"email": "kino1@example.com",
"age": 48,
"_links": {
"self": {
"href": "http://localhost:8080/users/u001"
},
"appUser": {
"href": "http://localhost:8080/users/u001"
},
"company": {
"href": "http://localhost:8080/users/u001/company"
}
}
},
...
]
},
"_links": {
"self": {
"href": "http://localhost:8080/users?page=0&size=20"
},
"profile": {
"href": "http://localhost:8080/profile/users"
},
"search": {
"href": "http://localhost:8080/users/search"
}
},
"page": {
"size": 20,
"totalElements": 6,
"totalPages": 1,
"number": 0
}
}
おお、JSONデータが返ってきたようですね!
あとでソースは改めてちゃんと見るとして、、、SpringBootがリクエストを受けたのち、SQL文を発行してDBサーバ(H2だけど)からデータ取得した結果が返ってきました。
そのほか、
などにアクセスしてみてください。SwaggerのUIが表示されたり、H2データベースのコンソール画面が表示されます。ちなみにH2のコンソールへは
JDBC URL: jdbc:h2:mem:testdb
Driver Class: org.h2.Driver
username: sa
password:
でアクセスできます。
BFFもうごかしてみる
BFFサーバも動かしてみます。
Backendは起動したままにしたいので、別のプロンプトを開いて、
$ cd spring-data-rest-example/
$ cd apollo-server-example/
$
yarn でライブラリのダウンロード
$ yarn
BackendのAPIにアクセスするためAPI仕様が記載されたJSONファイルをダウンロード。
$ curl http://localhost:8080/v3/api-docs -o api-docs.json
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 16043 100 16043 0 0 23802 0 --:--:-- --:--:-- --:--:-- 23767
$
それをもとに、BackendのAPI を使用するためのライブラリを生成
$ openapi-generator-cli generate -i api-docs.json -g typescript-axios -o ./src/generated/
Did set selected version to 7.6.0
[main] INFO o.o.codegen.DefaultGenerator - Generating with dryRun=false
...
[main] INFO o.o.codegen.TemplateManager - writing file /home/sysmgr/spring-data-rest-example/apollo-server-example/./src/generated/.openapi-generator-ignore
[main] INFO o.o.codegen.TemplateManager - writing file /home/sysmgr/spring-data-rest-example/apollo-server-example/./src/generated/.openapi-generator/VERSION
[main] INFO o.o.codegen.TemplateManager - writing file /home/sysmgr/spring-data-rest-example/apollo-server-example/./src/generated/.openapi-generator/FILES
################################################################################
$ ls -lrt src/generated/
total 132
-rw-r--r-- 1 sysmgr sysmgr 403 Jun 11 10:47 index.ts
-rw-r--r-- 1 sysmgr sysmgr 4714 Jun 11 10:47 common.ts
-rw-r--r-- 1 sysmgr sysmgr 1734 Jun 11 10:47 base.ts
-rw-r--r-- 1 sysmgr sysmgr 1830 Jun 11 10:47 git_push.sh
-rw-r--r-- 1 sysmgr sysmgr 3375 Jun 11 10:47 configuration.ts
-rw-r--r-- 1 sysmgr sysmgr 110132 Jun 11 10:47 api.ts
$
準備ができたのでBFFサーバを起動します。
$ yarn start
yarn run v1.22.22
$ ts-node src/index.ts
Server is running on http://localhost:3000
GraphQL Playground available at http://localhost:3000/graphql
起動したようです。
上記のURLは、GraphQLを発行するための便利なWeb画面なのですが、いったん CURLアクセスしてみます。
$ curl --request POST \
--header 'content-type: application/json' \
--url http://localhost:3000/graphql \
--data '{"query":"query Users {\r\n users {\r\n id\r\n firstName\r\n lastName\r\n email\r\n age\r\n company\r\n }\r\n}","variables":{}}' | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 960 100 813 100 147 13100 2368 --:--:-- --:--:-- --:--:-- 17454
{
"data": {
"users": [
{
"id": "u001",
"firstName": null,
"lastName": "木野1",
"email": "kino1@example.com",
"age": 48,
"company": "http://localhost:8080/users/u001/company"
},
{
"id": "u002",
"firstName": null,
"lastName": "木野2",
"email": "kino2@example.com",
"age": 47,
"company": "http://localhost:8080/users/u002/company"
},
{
"id": "u003",
"firstName": null,
"lastName": "木野3",
"email": null,
"age": 15,
"company": "http://localhost:8080/users/u003/company"
},
{
"id": "u004",
"firstName": null,
"lastName": "佐藤1",
"email": null,
"age": 16,
"company": "http://localhost:8080/users/u004/company"
},
{
"id": "u005",
"firstName": null,
"lastName": "佐藤2",
"email": null,
"age": 17,
"company": "http://localhost:8080/users/u005/company"
},
{
"id": "u006",
"firstName": null,
"lastName": "佐藤3",
"email": null,
"age": 18,
"company": "http://localhost:8080/users/u006/company"
}
]
}
}
BFFサーバにGraphQLの電文を送信したら、JSONデータが返却されました。
環境構築、お疲れさまでした。
中身の説明とかTIPS
今回は動かすだけで、中身の説明などは後日にします!
まとめ
- RESTサーバとGraphQLサーバを構築しました。
- JPAとSpring Data RESTの機能を使って
- ManyToOneの関連があるRESTサービスを自動生成しました。
- Swggger(OpenAPI)を有効にして、API仕様書のJSONもダウンロードできました
- openapi-generator をつかってRESTにアクセスするプロキシ(TypeScript)を自動生成しました
- Apollo Serverの機能を使って
- GraphQLのリクエストを受け取って、RESTからデータを取得してGraphQLデータを返却しました。
RESTの戻り電文のMayToOne部分のcompanyなどが
"company": "http://localhost:8080/users/u006/company"
などが気になるところですが、、、まずはOKとしましょう!
お疲れさまでした。
おまけ。必要な環境の構築手順
WindowsのWSL(Ubuntu)だったり、Ubuntu系のLinuxでの手順です。
Javaのインストール
$ java --version
Command 'java' not found, but can be installed with:
...
$ apt-cache search openjdk
$ sudo apt update
$ sudo apt install openjdk-17-jdk
Reading package lists... Done
Building dependency tree
Reading state information... Done
...
After this operation, 878 MB of additional disk space will be used.
Do you want to continue? [Y/n]
...
$ java --version
openjdk 17.0.11 2024-04-16
OpenJDK Runtime Environment (build 17.0.11+9-Ubuntu-120.04.2)
OpenJDK 64-Bit Server VM (build 17.0.11+9-Ubuntu-120.04.2, mixed mode, sharing)
$
お疲れさまでした
openapi-generator-cli のインストール
Java、Node.js が必要です。
$ java --version
openjdk 17.0.11 2024-04-16
OpenJDK Runtime Environment (build 17.0.11+9-Ubuntu-120.04.2)
OpenJDK 64-Bit Server VM (build 17.0.11+9-Ubuntu-120.04.2, mixed mode, sharing)
$
npmでインストールします。
あ、Node.jsがまだの方はこちらをご参考に。
$ npm install @openapitools/openapi-generator-cli -g
npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
added 116 packages in 14s
23 packages are looking for funding
run `npm fund` for details
$ openapi-generator-cli version
openapi-generator-cli: command not found
$ exec $SHELL -l
$ openapi-generator-cli version
Download 7.6.0 ...
Downloaded 7.6.0
Did set selected version to 7.6.0
7.6.0
$
お疲れさまでした。
関連リンク
- https://github.com/masatomix/spring-data-rest-example 今回のソースです
- https://github.com/Showichiro/spring-data-rest-example 上記のサンプルは、ここからForkして加筆修正させてもらいました!感謝
- Spring Data REST 公式
- Apollo Server 公式
- Spring Data RESTの要点と利用方法 Spring Data RESTについての記事。参考にさせてもらいました。
- GraphQLにおけるN+1問題の解決策 N+1問題についての解決策を提示しています。ちゃんとキャッチアップしたい。
- GraphQLサーバとRESTサーバをさっと立ちあげて、実際に触ってみる。つづき。関連のあるデータの取得 次の記事