Spring for GraphQLを利用したGraphQL APIの構築手順

# Spring for GraphQLとは
JavaでGraphQL APIを構築する場合は[GraphQL Java](https://www.graphql-java.com/)を利用することが一般的でした。
SpringBootを利用する場合も同様でしたが、2022年5月にSpring for Javaの1.0.0がリリースされSpringとして公式にGraphQLをサポートを提供する事になりました。
まだリリースから日が経っていないこもあり、Spring for GraphQLの[公式ドキュメント](https://docs.spring.io/spring-graphql/docs/current/reference/html/)や[サンプル](https://github.com/spring-projects/spring-graphql/tree/main/samples)は2022年7月現在であまり充実した内容ではないないと思います。

今回はサンプルやドキュメントを参考に独自にSpring for GraphQLを利用したAPIの実装を行っていきます。

https://spring.io/projects/spring-graphql

# 今回実装するAPIのデータモデル
DBでストアされた下記テーブルについてGraphQLのスキーマに再定義し柔軟性の高いデータを取得できるAPIを実装します。
まずは、下図のようなデータモデルをベースとしてGraphQLのスキーマ定義を行っていく。

![er.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177076/e3797f42-b94d-cdd9-e1df-46702c0fc0d5.png)

各テーブルの利用用途は以下の通りです。
また、適当に作ったので完全に正規化できていないですがそこは気にしないでください。

* Accountテーブル：ユーザのアカウント情報を管理します。
* Service_Groupテーブル：ユーザが所属するサービスグループを管理し、サービスグループは複数のチームを持ちます。
* Teamテーブル：ユーザが所属するチームを管理します。


# ブランクプロジェクトの作成
まずは[Spring Initializr](https://start.spring.io/)でブランクプロジェクトを作成し必要なパッケージとディレクトリを作成します。
利用するビルドツール、JDK、SpringBootおよび依存ライブラリは以下の通りです。

|FW/ライブラリ等|バージョン|
|:-------------|:--------|
|AdoptOpenJDK  |11       |
|SpringBoot    |2.7.1    |
|Maven         |3.5.4    |
|Spring Web    |-       |
|Spring for GraphQL |-  |
|Lombok             |-  |
|H2 Database        |-  |
|log4j2             |-  |

 
またLog4j2はSpring Initializrで指定できないのでpom.xmlに直接依存関係を追加します。
その際に`spring-boot-starter<`が依存するロギングライブラリを除外してやる必要があるので`<exclusion>`で除外します。
```xml:pom.xml
<dependency>
  <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
      <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
      </exclusion>
    </exclusions>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

```

# サンプルアプリ構成
プロジェクト構成は下記構成とします。

```
spring-graphql-sample
 ├ src
 │  ├ main
 │  │  ├ java/com.spring.graphql.example 
 │  │  │  ├ controller
 │  │  │  ├ entity
 │  │  │  ├ repository
 │  │  │  └ Main.java 
 │  │  │
 │  │  └ respurces
 │  │     ├ graphql
 │  │     │  └ graphql-schema.graphqls --- graphqlsファイル
 │  │     ├ aplication.yaml
 │  │     ├ log4j2.xml --- ログ出力設定
 │  │     ├ schema.sql --- H2 DBテーブル初期化スクリプト
 │  │     └ data.sql  --- H2 DBデータ初期化スクリプト
 │  │
 │  └ test --- テスト用(今回使用せず)
 └ pom.xml
```

# 実装準備
いきなりGraphQLのAPIを実装するの前にプロジェクトにDBに流すデータやログの設定を行います。

## 準備作業1：テーブル定義とDB接続定義設定
DBMSを個別に設定するのが面倒なのでH2DBを利用します。
アプリ起動時にH2DBにテーブルとデータを自動で流し込むために`schema.sql`に下記内容で保存します。
（`data.sql`の内容は省略するので各自で適当なデータを設定してください）

```sql:schema.sql
-- Service Group
CREATE TABLE service_group (
  service_group_id VARCHAR(10) PRIMARY KEY COMMENT 'サービスグループID',
  service_group_name VARCHAR(40) NOT NULL COMMENT 'サービスグループ名'
);
-- Team
CREATE TABLE team (
  team_id VARCHAR(10) PRIMARY KEY COMMENT 'チームID',
  belonging_service_group_id VARCHAR(10) NOT NULL COMMENT '所属サービスグループグID',
  team_name VARCHAR(40) NOT NULL COMMENT 'チーム名',
  team_authority VARCHAR(15) COMMENT 'チーム権限',
  FOREIGN KEY (belonging_service_group_id) REFERENCES service_group (service_group_id)
);
-- Account Table
CREATE TABLE account (
  account_id VARCHAR(10) PRIMARY KEY COMMENT 'アカウントID',
  user_name VARCHAR(40) NOT NULL COMMENT 'ユーザ名',
  age Int NOT NULL COMMENT '年齢',
  account_type VARCHAR(10) NOT NULL COMMENT 'アカウント分類',
  belonging_service_group_id VARCHAR(10) NOT NULL COMMENT '所属サービスグループID',
  belonging_team_id VARCHAR(10) NOT NULL COMMENT '所属チームID',
  FOREIGN KEY (belonging_service_group_id) REFERENCES service_group (service_group_id),
  FOREIGN KEY (belonging_team_id) REFERENCES team (team_id)
);
```

次に`application.yaml`ファイルにDB接続定義を設定しておきます。
```yaml:application.yaml
spring:
  datasource:
    platform: h2
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=1;DB_CLOSE_ON_EXIT=FALSE;MODE=DB2
    username: sa
    password: ''
```

## 準備作業2：ログ設定
ロギングにはlog4j2を利用します。

```xml:log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="error">

    <Properties>
        <Property name="format1">[%d{yyyy/MM/dd HH:mm:ss.SSS}] [%t] [%-6p] [%c{10}] : %m%n</Property>
    </Properties>

    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout>
                <pattern>${format1}</pattern>
            </PatternLayout>
        </Console>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</Configuration>
```

# GraphQLAPIの実装
事前準備が完了したので本題のGraphQLAPIを実装していきます。

## 手順1：GraphQLのスキーマ定義
`resources/graphql/`に`graphql-schema.graphqls`という名前のファイルを作成しGraphQLのスキーマおよびクエリを定義します。
今回は`accountById`というクエリを定義します。

このクエリはアカウントIDをパラメータとしてAPIリクエストを投げることで、アカウントIDに紐づくアカウント情報、そのアカウントが所属するサービスグループ情報、チーム情報を取得することが可能となっています。
またあまり意味はありませんが、取得したチーム情報からチームが所属するサービスグループ情報も取得可能となっています。

GraphQLのスキーマ定義はDBのテーブル定義とは異なりデータを利用する側が利用しやすい形でスキーマを定義することが推奨されています。
スキーマ定義に関するルールや考え方はこちらの[リンク](https://graphql-rules.com/)が参考になります。


```graphql:graphql-schema.graphqls
# クエリ定義
type Query {
    accountById(accountId: ID): Account
}
# タイプ定義
type ServiceGroup {
    serviceGroupId: ID!
    serviceGroupName: String
    teams: [Team]
}
type Team {
    teamId: ID!
    teamName: String
    teamAuthority: String
    serviceGroup: ServiceGroup!
}
type Account {
    accountId: ID!
    userName: String
    age: Int
    accountType: String
    serviceGroup: ServiceGroup!
    team: Team!
}

# Enum定義
enum TeamAuthority {
    PRIVILEGE
    DEVELOP
    AUDIT
}
enum AccountType {
    ADMIN
    DEVELOPER
    GUEST
}
```

## 手順2：DTOクラス実装
DBから取得したデータを格納するDTOクラスを`entity`パッケージに作成します。
今回は3つのテーブルを定義しているのでそれぞれのテーブル用のDTOクラスを作成します。
また、Setter/Getterをいちいち書くのが面倒なのでLombokを利用しています。
PKと紐づく変数には`@Id`アノテーションを付与しています。

```java:Account.java
@Setter
@Getter
@AllArgsConstructor
public class Account {
    @Id
    private String accountId;
    private String userName;
    private int age;
    private String accountType;
    private String belongingServiceGroupId;
    private String belongingTeamId;
}
```

```java:ServiceGroup.java
@Setter
@Getter
public class ServiceGroup {
    @Id
    private String serviceGroupId;
    private String serviceGroupName;
}
```

```java:Team.java
@Setter
@Getter
public class Team {
    @Id
    private String teamId;
    private String belongingServiceGroupId;
    private String teamName;
    private String teamAuthority;
}
```

## 手順3：RepositoryIFの実装
DBとのやり取りには`Spring Data JDBC`を利用します。
各テーブルと対応するRepositoryIFを実装し、`repository`パッケージに格納します。
（今回は単純なテーブル構造なのでテーブル単位でIFを分けています。）

extendsで指定している`CrudRepository`は`findById`メソッドがデフォルトで用意されているためPKをパラメータとしてデータを取得する場合は個別にDBクエリを実装する必要はありません。


```java:AccountRepository.java
public interface AccountRepository extends CrudRepository<Account, String>{
}
```

```java:ServiceGroupRepository.java
public interface ServiceGroupRepository extends CrudRepository<ServiceGroup, String> {
}
```

`TeamRepository`についてはPK以外をパラメータとしたSELECTクエリを実装する必要があるため下記例の通りにメソッドを定義します。

```java:TeamRepository.java
public interface TeamRepository extends CrudRepository<Team, String> {

    @Query("SELECT team_id, belonging_service_group_id, team_name, team_authority" +
            " FROM team WHERE belonging_service_group_id = :serviceGroupId")
    List<Team> findByServiceGroupId(String serviceGroupId);
}
```

## 手順4：Controllerクラスの実装
APIリクエストを受け取るControllerクラスを実装します。

`@QueryMapping`アノテーションを付けたメソッドにて受け付けたGraphQLクエリを処理するメソッドを定義します。
今回は`accountById`が対象のGraphQLクエリとなり、`accountId`に`@Argument`を付けることで引数を受け取ることを明記します。

また、`@SchemaMapping`には`Account`GraphQLスキーマに紐づく`serviceGroup`や`Team`を取得するための処理を実装します。
今回は、`Account`に紐づく`ServiceGroup`、`Team`および`Team`に紐づく`ServiceGroup`を取得するために3つのメソッドを定義しています。

```java:
@Controller
public class AccountGraphqlController {

    private final Logger logger = LogManager.getLogger(AccountGraphqlController.class);

    private AccountRepository accountRepository;

    private ServiceGroupRepository serviceGroupRepository;

    private TeamRepository teamRepository;

    public AccountGraphqlController(final AccountRepository accountRepository,
                                    final ServiceGroupRepository serviceGroupRepository,
                                    final TeamRepository teamRepository) {
        this.accountRepository = accountRepository;
        this.serviceGroupRepository = serviceGroupRepository;
        this.teamRepository = teamRepository;
    }

    @QueryMapping
    public Account accountById(@Argument final String accountId) {
        logger.info("=== Query Call, queryByAccountId. === ");
        final Optional<Account> acc = accountRepository.findById(accountId);
        return acc.get();
    }

    @SchemaMapping
    public ServiceGroup serviceGroup(final Account account) {
        final Optional<ServiceGroup> sg = serviceGroupRepository.findById(account.getBelongingServiceGroupId());
        return sg.get();
    }

    @SchemaMapping
    public Team team(final Account account) {
        final Optional<Team> t = teamRepository.findById(account.getBelongingTeamId());
        return t.get();
    }

    @SchemaMapping
    public ServiceGroup serviceGroup(final Team team) {
        final Optional<ServiceGroup> sg = serviceGroupRepository.findById(team.getBelongingServiceGroupId());
        return sg.get();
    }
}
```

## 手順5：GraphQLエンドポイントURLの設定
`application.yaml`ファイルにGraphQLのAPIエンドポイントパスの設定や有効化を行います。

```yaml:application.yaml
spring:
  # データソース定義は省略
  graphql:
    graphiql:
      enabled: true
      path: /graphiqls
```

以上でAPIの実装は完了です。
次は実際にAPIアプリを起動して動作確認を行います。

# GraphQLAPIサーバの起動と動作確認
APIアプリの起動には`Main.java`を実行するだけです。
Mavenでビルドしてjarを実行する方法や、IDEの機能で実行するなど実行方法は好きな方法で大丈夫です。

またGraphQLのクライアントツールとして[GraphiQL](https://www.electronjs.org/apps/graphiql)を利用することにします。

* エンドポイント：http://localhost:8080/graphql
* メソッド：POST

## 動作確認1
下記クエリを実行することで、アカウント情報が取得できました。

```graphql:例1
query {
    accountById(accountId:"ACC01") {
        accountId,
        userName,
        age,
        accountType
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177076/efdad074-4ddb-f03b-cd81-a4936de9753b.png)


## 動作確認2
次にアカウント情報とそれに紐づく各種情報を取得します。
下記クエリを実行することで、アカウント情報とそれに紐づくサービスグループ、チーム、チームに紐づくサービスグループ情報が一括で取得できました。

```graphql:例2
query {
    accountById(accountId:"ACC01") {
        accountId,
        userName,
        age,
        accountType,
        serviceGroup {
            serviceGroupId
            serviceGroupName
        }
        team {
            teamId
            teamName
            teamAuthority
            serviceGroup {
                serviceGroupId
                serviceGroupName
            }
        }
    }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177076/14a06d88-b1ac-1b38-a313-1c5898cf1b28.png)

# 最後に
今回は簡単なクエリサンプルの実装手順を解説しましたが、次はMutationやページネーションなどの応用を試してみようかと思います。