GraphQLサーバとRESTサーバをさっと立ちあげて、実際に触ってみる


こんにちは。@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レベルですが、実際に操作できる環境を作ります。
システムの全体構成は以下の通りです。

![Pasted image 20240610133257.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/d56203eb-d4f1-2e1c-0ca4-9914cb838f83.png)

BackendがCRUDするテーブルは以下の通り。

- 論理名
![image-20240610114229464.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/de1047e6-768b-1e4e-5d5f-0dddce32b1cd.png)

- 物理名
![image-20240610114259607.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/e02c3ae5-3b96-c6e0-7623-ebadc25fcdef.png)

このように、会社には複数のユーザが属している、そんなデータ構造です。

BFFとして公開するGraphQLのスキーマは以下のようにしてみました

 ```typescript
  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など、いくつか実行環境が必要です。
使うモノは以下の通り。

```console
$  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)
```

```console
$ node -v
v20.5.0
```

OpenAPIからソースコードを生成するため、下記のライブラリも使用します。
```console
$ openapi-generator-cli  version
7.6.0
$
```

記事の最後にインストール方法も記載しておきますので、いったんこれらはインストール済みとしてすすめていきます。

## やってみる

ソースをcloneしてきます。
```console
$ 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サーバを起動してみます。

```console
$ ./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
```

起動できたようです。

- http://localhost:8080/users
- http://localhost:8080/companies

などにアクセスしてみましょう。CURLとかだとこんな感じ。

```console
$ curl  http://localhost:8080/users | jq
```

```json
{
  "_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だけど)からデータ取得した結果が返ってきました。

![Pasted image 20240611191946.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/b80fc45a-7c2a-dde4-16b0-165b13abb1c5.png)

そのほか、

- http://localhost:8080/swagger-ui/index.html
- http://localhost:8080/h2-console/

などにアクセスしてみてください。SwaggerのUIが表示されたり、H2データベースのコンソール画面が表示されます。ちなみにH2のコンソールへは

```properties
JDBC URL: jdbc:h2:mem:testdb
Driver Class: org.h2.Driver
username: sa
password: 
```
でアクセスできます。


### BFFもうごかしてみる

BFFサーバも動かしてみます。
Backendは起動したままにしたいので、別のプロンプトを開いて、

```console
$ cd spring-data-rest-example/
$ cd apollo-server-example/
$
```

yarn でライブラリのダウンロード
```console
$ yarn

```

BackendのAPIにアクセスするためAPI仕様が記載されたJSONファイルをダウンロード。
```console
$ 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 を使用するためのライブラリを生成
```console
$ 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サーバを起動します。

```console
$ 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
```

```json
{
  "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データが返却されました。

![Pasted image 20240611192043.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/3447de3e-ef7e-8669-3958-7deaa08f5c7e.png)

環境構築、お疲れさまでした。

## 中身の説明とか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などが
```JSON
 "company": "http://localhost:8080/users/u006/company"
```
などが気になるところですが、、、まずはOKとしましょう！


お疲れさまでした。

## おまけ。必要な環境の構築手順

WindowsのWSL(Ubuntu)だったり、Ubuntu系のLinuxでの手順です。

### Javaのインストール

```console
$  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 が必要です。

```console
$ 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がまだの方はこちらをご参考に。

https://qiita.com/masatomix/items/d69ac664a2fcbdb98f6c


```console
$ 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](https://spring.io/projects/spring-data-rest) 公式
- [Apollo Server](https://www.apollographql.com/docs/apollo-server/) 公式
- [Spring Data RESTの要点と利用方法](https://qiita.com/umiushi_1/items/b369f659bbd94576b8f4) Spring Data RESTについての記事。参考にさせてもらいました。
- [GraphQLにおけるN+1問題の解決策](https://qiita.com/nakawodayo/items/2f2f98c9833659da66f0) N+1問題についての解決策を提示しています。ちゃんとキャッチアップしたい。
- [GraphQLサーバとRESTサーバをさっと立ちあげて、実際に触ってみる。つづき。関連のあるデータの取得](https://qiita.com/masatomix/items/d52ef0bdeb131f9dd6f7) 次の記事