GraphQL
今回は業務の中でGraphQLを使用する機会があったので、簡単にまとめてみました!
記事のターゲット
- graphql_codegenを使ってみたい人
- GraphQLの基礎を学びたい人
この記事に記載されていないこと
- Fragmentについて(今後の記事で扱うかも)
- 自動生成を行わないで実装する方法
GraphQLとは
GraphQLとは、APIを作成し、クライアントがサーバーからデータを取得するためのクエリ言語です。簡単に言えば、クライアントとサーバー間でデータを効率的にやり取りする方法です。
GraphQLとREST APIの違い
GraphQLとよく比較されるのがREST APIです。REST APIとGraphQLの主な違いは以下の通りです。
特徴 | REST API | GraphQL |
---|---|---|
エンドポイント | 複数のエンドポイント | 単一のエンドポイント |
データ取得 | 固定されたレスポンス | 必要なデータのみを指定して取得 |
データ操作 | HTTPメソッド(GET, POST, PUT, DELETE) | クエリとミューテーション |
パフォーマンス | 過剰なデータの送受信が発生する場合がある | 必要なデータのみを取得し効率的 |
柔軟性 | 固定されたデータ構造 | 柔軟にデータ構造を指定可能 |
私がGraphQLを使用して最も良いと感じる点は、データ取得の際に必要なデータのみを指定して取得できることです。
例えば、ユーザー1とユーザー2を表示する画面で、ユーザーにはそれぞれ名前、生年月日、好きな食べ物が設定されているとします。このユーザーデータを取得する場合を考えます。
REST APIの場合:
ユーザー1とユーザー2の名前、生年月日、好きな食べ物の全てのデータを取得します。
GraphQLの場合:
ユーザー1は名前、生年月日、好きな食べ物の全てのデータを取得し、ユーザー2は名前と生年月日のみを指定して取得することが可能です。
GraphQLの場合はユーザー1とユーザー2で取得するデータをこちらから柔軟に指定できます
GraphQLの基本概念
クエリ (Query)
クエリとは、クライアントがサーバーから必要なデータを取得するためのリクエストのことです。
例: あるアプリで、ユーザーのリストを表示する画面があるとします。この画面を表示するために、クライアント(アプリ側)はサーバーに「全てのユーザー情報をください」とリクエストを送ります。これがクエリです。
query GetUsers {
users {
id
name
}
}
ミューテーション (Mutation)
ミューテーションとは、サーバー上のデータを変更するための操作です。
例: あるアプリで、新しいユーザーを登録するフォームがあるとします。ユーザーがフォームに情報を入力して「登録」ボタンを押すと、クライアント(アプリ側)はサーバーに「新しいユーザーを追加してください」とリクエストを送ります。これがミューテーションです。
mutation AddUser($name: String!) {
addUser(name: $name) {
id
name
}
}
スキーマ (Schema)
スキーマとは、APIのデータ構造とそのデータがどのように操作されるかを定義するものです。
例: レストランのメニューがスキーマだとしましょう。メニューには、提供される料理の名前、価格、成分などが記載されています。これを見れば、どんな料理が注文できるのかがわかります。同じように、スキーマを見ると、そのAPIがどんなデータを提供し、どんな操作ができるのかがわかります。
type User {
id: ID!
name: String!
}
type Query {
users: [User!]!
}
type Mutation {
addUser(name: String!): User!
}
二つの型(input型とtype型)
type
型
type
型は、サーバーからクライアントに返されるデータを定義します。つまり、クエリやミューテーションのレスポンスとして使用されます。例えば、ユーザー情報を返す場合の構造を定義します。
例:
type User {
id: ID!
name: String!
email: String!
}
input
型
input
型は、クライアントからサーバーに送信されるデータの構造を定義します。主にミューテーションの引数として使用されます。例えば、新しいユーザーを追加する際に送信するデータの構造を定義します。
例:
input CreateUserInput {
name: String!
email: String!
}
なぜ type
と input
を区別する必要があるのか
GraphQLでは、type
とinput
を混在させないようにする必要があります。これにより、データの送受信時に構造が明確になり、予期しないエラーを防ぐことができます。
type
フィールドに他のtype
を参照することはできますが、input
フィールドにtype
を直接参照することはできません
間違った例とエラー例
初学者として、このルールを知らずに、input
タイプの中でtype
を参照しようとしてエラーになった例を示します。
input CreateUserInput {
name: String!
email: String!
profile: UserProfile # inputにtypeを定義しているから間違い
}
type UserProfile {
bio: String!
avatarUrl: String!
}
上記のように、input
の中でtype
を参照することはできません。これを実行してしまうと、上手く通信ができなかったり、自動生成を使っている場合は生成に失敗します。
以下のようなエラーが表示されます。
[SEVERE] graphql_codegen on lib/data/graphql/schema.graphql:
Bad state: Failed to generate type for UserProfileInput.
正しい例
正しい方法は、input
の中でもinput
タイプを使用することです。
input CreateUserInput {
name: String!
email: String!
profile: UserProfileInput # これが正しい
}
input UserProfileInput {
bio: String!
avatarUrl: String!
}
これらの4つの概念を押さえておけば、GraphQLの基本は大丈夫です!
graphql_codegenで自動生成する方法
ステップ1: パッケージのインストール
まず、必要なパッケージをインストールします。
dependencies:
flutter:
sdk: flutter
graphql_flutter: # GraphQLクライアント用のパッケージ
dev_dependencies:
build_runner: # コード生成のためのパッケージ
graphql_codegen: # GraphQLクエリやミューテーションの自動生成用
ステップ2: ディレクトリとファイルの作成
次に、lib/data/graphql
ディレクトリを作成し、その中に以下のファイルを追加します。各ファイルの役割について簡単に説明します。
1. クエリファイル
query GetUsers {
users {
id
name
}
}
2. ミューテーションファイル
mutation AddUser($input: CreateUserInput!) {
addUser(input: $input) {
id
name
}
}
3. スキーマファイル
GraphQLのスキーマを定義します。
type Query {
users: [User!]!
# !は必須、非nullの場合graphqlでは?は不要
# []は配列
# クエリが複数ある場合は、ここに追加して記載する。
}
type Mutation {
addUser(input: CreateUserInput!): User!
# ミューテーションが複数ある場合は、ここに追加して記載する。
}
# Userタイプの定義
type User {
id: ID!
name: String!
email: String!
}
# 入力タイプの定義
input CreateUserInput {
name: String!
email: String!
profile: UserProfileInput
}
input UserProfileInput {
bio: String!
avatarUrl: String!
}
ステップ3: build.yaml
の設定
build.yaml
ファイルに以下の設定を追加して、コード生成を自動化します。これにより、指定したディレクトリ内のGraphQLファイルから自動的にDartコードが生成されます。
graphql_codegen:
options:
scopes:
- lib/data/graphql/**
clients:
- graphql
- graphql_flutter
/の後に **がないとファイルが認識されないので注意
ステップ4: コード生成コマンドの実行
次に、コード生成コマンドを実行してDartコードを自動生成します。
flutter pub run build_runner build --delete-conflicting-outputs
自動生成されたコード
クエリ[GetUsers]
import 'package:gql/ast.dart';
class Query$GetUsers {
Query$GetUsers({
required this.users,
this.$__typename = 'Query',
});
factory Query$GetUsers.fromJson(Map<String, dynamic> json) {
final l$users = json['users'];
final l$$__typename = json['__typename'];
return Query$GetUsers(
users: (l$users as List<dynamic>)
.map(
(e) => Query$GetUsers$users.fromJson((e as Map<String, dynamic>)))
.toList(),
$__typename: (l$$__typename as String),
);
}
final List<Query$GetUsers$users> users;
final String $__typename;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$users = users;
_resultData['users'] = l$users.map((e) => e.toJson()).toList();
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
return _resultData;
}
@override
int get hashCode {
final l$users = users;
final l$$__typename = $__typename;
return Object.hashAll([
Object.hashAll(l$users.map((v) => v)),
l$$__typename,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Query$GetUsers) || runtimeType != other.runtimeType) {
return false;
}
final l$users = users;
final lOther$users = other.users;
if (l$users.length != lOther$users.length) {
return false;
}
for (int i = 0; i < l$users.length; i++) {
final l$users$entry = l$users[i];
final lOther$users$entry = lOther$users[i];
if (l$users$entry != lOther$users$entry) {
return false;
}
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
return true;
}
}
extension UtilityExtension$Query$GetUsers on Query$GetUsers {
CopyWith$Query$GetUsers<Query$GetUsers> get copyWith =>
CopyWith$Query$GetUsers(
this,
(i) => i,
);
}
abstract class CopyWith$Query$GetUsers<TRes> {
factory CopyWith$Query$GetUsers(
Query$GetUsers instance,
TRes Function(Query$GetUsers) then,
) = _CopyWithImpl$Query$GetUsers;
factory CopyWith$Query$GetUsers.stub(TRes res) =
_CopyWithStubImpl$Query$GetUsers;
TRes call({
List<Query$GetUsers$users>? users,
String? $__typename,
});
TRes users(
Iterable<Query$GetUsers$users> Function(
Iterable<CopyWith$Query$GetUsers$users<Query$GetUsers$users>>)
_fn);
}
class _CopyWithImpl$Query$GetUsers<TRes>
implements CopyWith$Query$GetUsers<TRes> {
_CopyWithImpl$Query$GetUsers(
this._instance,
this._then,
);
final Query$GetUsers _instance;
final TRes Function(Query$GetUsers) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? users = _undefined,
Object? $__typename = _undefined,
}) =>
_then(Query$GetUsers(
users: users == _undefined || users == null
? _instance.users
: (users as List<Query$GetUsers$users>),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
));
TRes users(
Iterable<Query$GetUsers$users> Function(
Iterable<CopyWith$Query$GetUsers$users<Query$GetUsers$users>>)
_fn) =>
call(
users: _fn(_instance.users.map((e) => CopyWith$Query$GetUsers$users(
e,
(i) => i,
))).toList());
}
class _CopyWithStubImpl$Query$GetUsers<TRes>
implements CopyWith$Query$GetUsers<TRes> {
_CopyWithStubImpl$Query$GetUsers(this._res);
TRes _res;
call({
List<Query$GetUsers$users>? users,
String? $__typename,
}) =>
_res;
users(_fn) => _res;
}
const documentNodeQueryGetUsers = DocumentNode(definitions: [
OperationDefinitionNode(
type: OperationType.query,
name: NameNode(value: 'GetUsers'),
variableDefinitions: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'users'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'id'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
FieldNode(
name: NameNode(value: 'name'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
]);
class Query$GetUsers$users {
Query$GetUsers$users({
required this.id,
required this.name,
this.$__typename = 'User',
});
factory Query$GetUsers$users.fromJson(Map<String, dynamic> json) {
final l$id = json['id'];
final l$name = json['name'];
final l$$__typename = json['__typename'];
return Query$GetUsers$users(
id: (l$id as String),
name: (l$name as String),
$__typename: (l$$__typename as String),
);
}
final String id;
final String name;
final String $__typename;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$id = id;
_resultData['id'] = l$id;
final l$name = name;
_resultData['name'] = l$name;
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
return _resultData;
}
@override
int get hashCode {
final l$id = id;
final l$name = name;
final l$$__typename = $__typename;
return Object.hashAll([
l$id,
l$name,
l$$__typename,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Query$GetUsers$users) || runtimeType != other.runtimeType) {
return false;
}
final l$id = id;
final lOther$id = other.id;
if (l$id != lOther$id) {
return false;
}
final l$name = name;
final lOther$name = other.name;
if (l$name != lOther$name) {
return false;
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
return true;
}
}
extension UtilityExtension$Query$GetUsers$users on Query$GetUsers$users {
CopyWith$Query$GetUsers$users<Query$GetUsers$users> get copyWith =>
CopyWith$Query$GetUsers$users(
this,
(i) => i,
);
}
abstract class CopyWith$Query$GetUsers$users<TRes> {
factory CopyWith$Query$GetUsers$users(
Query$GetUsers$users instance,
TRes Function(Query$GetUsers$users) then,
) = _CopyWithImpl$Query$GetUsers$users;
factory CopyWith$Query$GetUsers$users.stub(TRes res) =
_CopyWithStubImpl$Query$GetUsers$users;
TRes call({
String? id,
String? name,
String? $__typename,
});
}
class _CopyWithImpl$Query$GetUsers$users<TRes>
implements CopyWith$Query$GetUsers$users<TRes> {
_CopyWithImpl$Query$GetUsers$users(
this._instance,
this._then,
);
final Query$GetUsers$users _instance;
final TRes Function(Query$GetUsers$users) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? id = _undefined,
Object? name = _undefined,
Object? $__typename = _undefined,
}) =>
_then(Query$GetUsers$users(
id: id == _undefined || id == null ? _instance.id : (id as String),
name: name == _undefined || name == null
? _instance.name
: (name as String),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
));
}
class _CopyWithStubImpl$Query$GetUsers$users<TRes>
implements CopyWith$Query$GetUsers$users<TRes> {
_CopyWithStubImpl$Query$GetUsers$users(this._res);
TRes _res;
call({
String? id,
String? name,
String? $__typename,
}) =>
_res;
}
ミューテーション[AddUser]
import 'package:gql/ast.dart';
class Query$GetUsers {
Query$GetUsers({
required this.users,
this.$__typename = 'Query',
});
factory Query$GetUsers.fromJson(Map<String, dynamic> json) {
final l$users = json['users'];
final l$$__typename = json['__typename'];
return Query$GetUsers(
users: (l$users as List<dynamic>)
.map(
(e) => Query$GetUsers$users.fromJson((e as Map<String, dynamic>)))
.toList(),
$__typename: (l$$__typename as String),
);
}
final List<Query$GetUsers$users> users;
final String $__typename;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$users = users;
_resultData['users'] = l$users.map((e) => e.toJson()).toList();
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
return _resultData;
}
@override
int get hashCode {
final l$users = users;
final l$$__typename = $__typename;
return Object.hashAll([
Object.hashAll(l$users.map((v) => v)),
l$$__typename,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Query$GetUsers) || runtimeType != other.runtimeType) {
return false;
}
final l$users = users;
final lOther$users = other.users;
if (l$users.length != lOther$users.length) {
return false;
}
for (int i = 0; i < l$users.length; i++) {
final l$users$entry = l$users[i];
final lOther$users$entry = lOther$users[i];
if (l$users$entry != lOther$users$entry) {
return false;
}
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
return true;
}
}
extension UtilityExtension$Query$GetUsers on Query$GetUsers {
CopyWith$Query$GetUsers<Query$GetUsers> get copyWith =>
CopyWith$Query$GetUsers(
this,
(i) => i,
);
}
abstract class CopyWith$Query$GetUsers<TRes> {
factory CopyWith$Query$GetUsers(
Query$GetUsers instance,
TRes Function(Query$GetUsers) then,
) = _CopyWithImpl$Query$GetUsers;
factory CopyWith$Query$GetUsers.stub(TRes res) =
_CopyWithStubImpl$Query$GetUsers;
TRes call({
List<Query$GetUsers$users>? users,
String? $__typename,
});
TRes users(
Iterable<Query$GetUsers$users> Function(
Iterable<CopyWith$Query$GetUsers$users<Query$GetUsers$users>>)
_fn);
}
class _CopyWithImpl$Query$GetUsers<TRes>
implements CopyWith$Query$GetUsers<TRes> {
_CopyWithImpl$Query$GetUsers(
this._instance,
this._then,
);
final Query$GetUsers _instance;
final TRes Function(Query$GetUsers) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? users = _undefined,
Object? $__typename = _undefined,
}) =>
_then(Query$GetUsers(
users: users == _undefined || users == null
? _instance.users
: (users as List<Query$GetUsers$users>),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
));
TRes users(
Iterable<Query$GetUsers$users> Function(
Iterable<CopyWith$Query$GetUsers$users<Query$GetUsers$users>>)
_fn) =>
call(
users: _fn(_instance.users.map((e) => CopyWith$Query$GetUsers$users(
e,
(i) => i,
))).toList());
}
class _CopyWithStubImpl$Query$GetUsers<TRes>
implements CopyWith$Query$GetUsers<TRes> {
_CopyWithStubImpl$Query$GetUsers(this._res);
TRes _res;
call({
List<Query$GetUsers$users>? users,
String? $__typename,
}) =>
_res;
users(_fn) => _res;
}
const documentNodeQueryGetUsers = DocumentNode(definitions: [
OperationDefinitionNode(
type: OperationType.query,
name: NameNode(value: 'GetUsers'),
variableDefinitions: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'users'),
alias: null,
arguments: [],
directives: [],
selectionSet: SelectionSetNode(selections: [
FieldNode(
name: NameNode(value: 'id'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
FieldNode(
name: NameNode(value: 'name'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
FieldNode(
name: NameNode(value: '__typename'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
]),
),
]);
class Query$GetUsers$users {
Query$GetUsers$users({
required this.id,
required this.name,
this.$__typename = 'User',
});
factory Query$GetUsers$users.fromJson(Map<String, dynamic> json) {
final l$id = json['id'];
final l$name = json['name'];
final l$$__typename = json['__typename'];
return Query$GetUsers$users(
id: (l$id as String),
name: (l$name as String),
$__typename: (l$$__typename as String),
);
}
final String id;
final String name;
final String $__typename;
Map<String, dynamic> toJson() {
final _resultData = <String, dynamic>{};
final l$id = id;
_resultData['id'] = l$id;
final l$name = name;
_resultData['name'] = l$name;
final l$$__typename = $__typename;
_resultData['__typename'] = l$$__typename;
return _resultData;
}
@override
int get hashCode {
final l$id = id;
final l$name = name;
final l$$__typename = $__typename;
return Object.hashAll([
l$id,
l$name,
l$$__typename,
]);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Query$GetUsers$users) || runtimeType != other.runtimeType) {
return false;
}
final l$id = id;
final lOther$id = other.id;
if (l$id != lOther$id) {
return false;
}
final l$name = name;
final lOther$name = other.name;
if (l$name != lOther$name) {
return false;
}
final l$$__typename = $__typename;
final lOther$$__typename = other.$__typename;
if (l$$__typename != lOther$$__typename) {
return false;
}
return true;
}
}
extension UtilityExtension$Query$GetUsers$users on Query$GetUsers$users {
CopyWith$Query$GetUsers$users<Query$GetUsers$users> get copyWith =>
CopyWith$Query$GetUsers$users(
this,
(i) => i,
);
}
abstract class CopyWith$Query$GetUsers$users<TRes> {
factory CopyWith$Query$GetUsers$users(
Query$GetUsers$users instance,
TRes Function(Query$GetUsers$users) then,
) = _CopyWithImpl$Query$GetUsers$users;
factory CopyWith$Query$GetUsers$users.stub(TRes res) =
_CopyWithStubImpl$Query$GetUsers$users;
TRes call({
String? id,
String? name,
String? $__typename,
});
}
class _CopyWithImpl$Query$GetUsers$users<TRes>
implements CopyWith$Query$GetUsers$users<TRes> {
_CopyWithImpl$Query$GetUsers$users(
this._instance,
this._then,
);
final Query$GetUsers$users _instance;
final TRes Function(Query$GetUsers$users) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? id = _undefined,
Object? name = _undefined,
Object? $__typename = _undefined,
}) =>
_then(Query$GetUsers$users(
id: id == _undefined || id == null ? _instance.id : (id as String),
name: name == _undefined || name == null
? _instance.name
: (name as String),
$__typename: $__typename == _undefined || $__typename == null
? _instance.$__typename
: ($__typename as String),
));
}
class _CopyWithStubImpl$Query$GetUsers$users<TRes>
implements CopyWith$Query$GetUsers$users<TRes> {
_CopyWithStubImpl$Query$GetUsers$users(this._res);
TRes _res;
call({
String? id,
String? name,
String? $__typename,
}) =>
_res;
}
解説
- クエリ[GetUsers]
- サーバーからUserオブジェクトの配列を取得するリクエストを送信します。レスポンスには、idとnameフィールドを持つユーザーの配列が含まれています。
- ミューテーション[AddUser]
- 新しいユーザーを作成するための入力データを受け取ります。レスポンスには、作成されたユーザーのidとnameが含まれています。
- スキーマファイル
- Userタイプを定義し、CreateUserInputという入力タイプを定義しています。この入力タイプは、新しいユーザーを作成するためのフィールドを持ち、ミューテーションの引数として使用されます。
これにより、GraphQLクエリとミューテーションのレスポンスがどのような形式になるかが明確になります。
graphql_codegenを使うメリット
graphql_codegen
を使うことで、以下のようなメリットがあります。
1. 手動コーディングの削減
クエリやミューテーションを手動で書く手間を大幅に減らせます。自動生成されたコードを使用することで、構文エラーを防ぎ、効率的に開発を進めることができます。
2. 型安全性の向上
自動生成されたコードには型が明示的に定義されているため、手書きの場合は実際にAPIを叩いてみないとエラーが検出できなかったのが、コンパイル時にエラーを検出できます。これにより、ランタイムエラーを減らし、バグの少ないコードを作成することができます。
3. メンテナンスの容易さ
GraphQLスキーマが変更された場合でも、自動生成されたコードを再生成するだけで対応できます。これにより、コードのメンテナンスが容易になり、最新のスキーマに追従することができます。
これらのメリットを享受することで、効率的かつ高品質なGraphQLアプリケーションを開発することができます。
実際に使ってみた感想
自分はInput型Type型を理解していない状態で書いてしまい、自動生成が上手くできず大苦戦しました(笑)。
もし自分と同じく苦戦している方がいましたら、もう一度型の定義を見直してみてください
また今後もGraphQLについて新しい発見があれば、記事も更新していこうと思います。
最後まで読んでいただきありがとうございました🙇♂️
告知
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを
募集しております。詳しくは採用情報ページをご確認ください。
みなさまからのご応募をお待ちしております。