Spring for GraphQLとは
JavaでGraphQL APIを構築する場合はGraphQL Javaを利用することが一般的でした。
SpringBootを利用する場合も同様でしたが、2022年5月にSpring for Javaの1.0.0がリリースされSpringとして公式にGraphQLをサポートを提供する事になりました。
まだリリースから日が経っていないこもあり、Spring for GraphQLの公式ドキュメントやサンプルは2022年7月現在であまり充実した内容ではないないと思います。
今回はサンプルやドキュメントを参考に独自にSpring for GraphQLを利用したAPIの実装を行っていきます。
今回実装するAPIのデータモデル
DBでストアされた下記テーブルについてGraphQLのスキーマに再定義し柔軟性の高いデータを取得できるAPIを実装します。
まずは、下図のようなデータモデルをベースとしてGraphQLのスキーマ定義を行っていく。
各テーブルの利用用途は以下の通りです。
また、適当に作ったので完全に正規化できていないですがそこは気にしないでください。
- Accountテーブル:ユーザのアカウント情報を管理します。
- Service_Groupテーブル:ユーザが所属するサービスグループを管理し、サービスグループは複数のチームを持ちます。
- Teamテーブル:ユーザが所属するチームを管理します。
ブランクプロジェクトの作成
まずはSpring Initializrでブランクプロジェクトを作成し必要なパッケージとディレクトリを作成します。
利用するビルドツール、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>
で除外します。
<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
の内容は省略するので各自で適当なデータを設定してください)
-- 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接続定義を設定しておきます。
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 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のテーブル定義とは異なりデータを利用する側が利用しやすい形でスキーマを定義することが推奨されています。
スキーマ定義に関するルールや考え方はこちらのリンクが参考になります。
# クエリ定義
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
アノテーションを付与しています。
@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;
}
@Setter
@Getter
public class ServiceGroup {
@Id
private String serviceGroupId;
private String serviceGroupName;
}
@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クエリを実装する必要はありません。
public interface AccountRepository extends CrudRepository<Account, String>{
}
public interface ServiceGroupRepository extends CrudRepository<ServiceGroup, String> {
}
TeamRepository
についてはPK以外をパラメータとしたSELECTクエリを実装する必要があるため下記例の通りにメソッドを定義します。
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つのメソッドを定義しています。
@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エンドポイントパスの設定や有効化を行います。
spring:
# データソース定義は省略
graphql:
graphiql:
enabled: true
path: /graphiqls
以上でAPIの実装は完了です。
次は実際にAPIアプリを起動して動作確認を行います。
GraphQLAPIサーバの起動と動作確認
APIアプリの起動にはMain.java
を実行するだけです。
Mavenでビルドしてjarを実行する方法や、IDEの機能で実行するなど実行方法は好きな方法で大丈夫です。
またGraphQLのクライアントツールとしてGraphiQLを利用することにします。
- エンドポイント:http://localhost:8080/graphql
- メソッド:POST
動作確認1
下記クエリを実行することで、アカウント情報が取得できました。
query {
accountById(accountId:"ACC01") {
accountId,
userName,
age,
accountType
}
動作確認2
次にアカウント情報とそれに紐づく各種情報を取得します。
下記クエリを実行することで、アカウント情報とそれに紐づくサービスグループ、チーム、チームに紐づくサービスグループ情報が一括で取得できました。
query {
accountById(accountId:"ACC01") {
accountId,
userName,
age,
accountType,
serviceGroup {
serviceGroupId
serviceGroupName
}
team {
teamId
teamName
teamAuthority
serviceGroup {
serviceGroupId
serviceGroupName
}
}
}
}
最後に
今回は簡単なクエリサンプルの実装手順を解説しましたが、次はMutationやページネーションなどの応用を試してみようかと思います。