Introduction to GraphQL
このサイトを見てください
https://graphql.org/learn
How to GraphQL
このサイトを見てください
https://www.howtographql.com
知見が沢山です。
rubyでのおすすめ実装、nodeでのおすすめ実装等のチュートリアルが沢山あります。
TL;TD; about "Introduction to GraphQL"
話が長すぎるよって人向けへの纏めです。
What is GraphQL ?
GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. GraphQL isn't tied to any specific database or storage engine and is instead backed by your existing code and data.
様は、GraphQLはapi用のquery言語です。DSLやIDLとも呼ばれます。
queryを書いて、apiの/graphql (※1)に全てのqueryをpostします。 個人的にはmysql等のDBと似ているなと思っています。
※1 パスは自由に変えられます
GraphQLという言語でAPIの構造を定義し、それに沿ってGUIもサーバーも作ろうって事です。
Advantages of GraphQL
GraphQLのメリットは、APIの構造を定義でき、且つGraphQLの言語仕様が柔軟でイケてることに尽きると思います。
APIの構造を柔軟に定義できるという事は、その構造に沿って(※2)クライアントのコードやサーバーのコードをある程度自動生成できます。
その際に、型付けが柔軟で強力な言語程、その辺の検査を自動で強くできるので、コンパイルさえ通れば定義された(※3)構造通りのレスポンスを返せている安心感を得られ、例えば型が緩い言語でよく見る in outで実行時エラーがでないかだけ等の余分なテストを無くせ、本当に必要な開発やテストに開発リソースを振れます。
※2 この自動生成できるコードのクオリティはGraphQLの運営組織やコミュニティ、生成対象言語やフレームワークの活発度に依存しています。
※3 仕様通りに実装できているかはまた別の話
graphql-code-generator
GraphQLのcode generatorの紹介です。
@mizchi さんの記事がわかりやすいのですが、一応ここでもざっくり最小限の解説だけします。
overwrite: true
schema:
- ./graphql/schema.graphql # or - http://localhost:3000/graphql
generates:
./client/gen/graphql-client-api.ts:
plugins:
- typescript
schemaの項目にGraphQLの仕様に沿って定義されたschemaファイルのパス又は、GraphQLサーバーのurlを指定します。
で、generatorsの項目に生成するファイルのパスを書いて、schemaファイルに適用するプラグインを直列に指定していきます(余談: webpackの設定を書いてる気分になりました)。
例えば、
type Author {
id: Int!
firstName: String!
lastName: String!
posts(findTitle: String): [Post]
}
type Post {
id: Int!
title: String!
author: Author!
}
type Query {
posts: [Post]
}
を食わせて
$ graphql-codegen --config codegen.yml
を実行すると、
以下のファイルが生成されます。
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string,
String: string,
Boolean: boolean,
Int: number,
Float: number,
};
export type Author = {
__typename?: 'Author',
id: Scalars['Int'],
firstName: Scalars['String'],
lastName: Scalars['String'],
posts?: Maybe<Array<Maybe<Post>>>,
};
export type AuthorPostsArgs = {
findTitle?: Maybe<Scalars['String']>
};
export type Post = {
__typename?: 'Post',
id: Scalars['Int'],
title: Scalars['String'],
author: Author,
};
export type Query = {
__typename?: 'Query',
posts?: Maybe<Array<Maybe<Post>>>,
};
色々とわかりやすく、便利な型が出てきましたね!
Validate Operations
WebやIOS、Androidでgraphqlサーバーに問い合わせる際には以下の様なqueryを書いてHTTPのPOSTで問い合わせるのが (※4) 標準的な方法です。
以下のファイルを作成します。
※4
パフォチュでhttp2コネクションを維持してやりとりしたり、web socketを利用したりする手法もありますが、ここでは割愛します
query {
posts {
id
title
author {
id
firstName
lastName
}
}
}
このqueryって果たして本当に正しい操作なのでしょうか?
安心してください、この操作が正しいかどうかはcodegenに読み込ませて解析させればランタイムではなく、静的に検査ができます。
overwrite: true
schema:
- ./graphql/schema.graphql
documents: # このdocumentsを追加して、queryを読み込ませます
- ./graphql/queries/*.graphql
generates:
./client/gen/graphql-client-api.ts:
plugins:
- typescript
このconfigを使ってcodegenをします。
上手く動きましたね。
※ ちなみに生成されたtypescriptのコードは先程と同じです。
それでは実際にschema違反である、定義されていないfieldであるfullNameをqueryに定義します。
query {
posts {
id
title
author {
id
firstName
lastName
fullName
}
}
}
codegenを実行します。
このようにエラーを出します。
エラーメッセージを一応貼っておきます。
$ yarn graphql-codegen --config codegen.yml
yarn run v1.17.3
warning ../../package.json: No license field
$ /Users/k-okina/Documents/graphql-patterns/basic/node_modules/.bin/graphql-codegen --config codegen.yml
(node:13656) ExperimentalWarning: Readable[Symbol.asyncIterator] is an experimental feature. This feature could change at any time
✔ Parse configuration
❯ Generate outputs
❯ Generate ./client/gen/graphql-client-api.ts
✔ Load GraphQL schemas
✔ Load GraphQL documents
✖ Generate
→ at /Users/k-okina/Documents/graphql-patterns/basic/graphql/queries/posts.graphql:9:7
Found 1 error
✖ ./client/gen/graphql-client-api.ts
AggregateError:
GraphQLDocumentError: Cannot query field "fullName" on type "Author". Did you mean "firstName" or "lastName"?
at /Users/k-okina/Documents/graphql-patterns/basic/graphql/queries/posts.graphql:9:7
at Object.checkValidationErrors (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/graphql-toolkit/dist/commonjs/utils/validate-documents.js:57:15)
at Object.codegen (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/core/dist/commonjs/codegen.js:41:27)
at process._tickCallback (internal/process/next_tick.js:68:7)
AggregateError:
GraphQLDocumentError: Cannot query field "fullName" on type "Author". Did you mean "firstName" or "lastName"?
at /Users/k-okina/Documents/graphql-patterns/basic/graphql/queries/posts.graphql:9:7
at Object.checkValidationErrors (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/graphql-toolkit/dist/commonjs/utils/validate-documents.js:57:15)
at Object.codegen (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/core/dist/commonjs/codegen.js:41:27)
at process._tickCallback (internal/process/next_tick.js:68:7)
Something went wrong
error Command failed with exit code 1.
fullName
field が Author
にないよ!エラー原因のファイルは graphql/queries/posts.graphql
だよ!って教えてくれていますね。
とても親切です。これならCIでの検査も簡単にできますね。
Operation types for Typescript
例えば Query をTypescriptでgraphql clientやapollo client、fetch api等を利用して発行する際に、そのQueryのレスポンスがどんな型なのかをどの様に把握すれば良いのでしょうか?
自分で型を1つ1つ書くのはめんどくさいですよね。
graphql-codegenにはtypescript-operationsというpluginがあり、これを設定することにより、queryやmutationの型を自動生成できます。
overwrite: true
schema:
- ./graphql/schema.graphql
documents:
- ./graphql/queries/*.graphql
generates:
./client/gen/graphql-client-api.ts:
plugins:
- typescript
- typescript-operations # これを追加しました
ではcodegenをします。
動きましたね。
生成された client/gen/graphql-client-api.ts
を確認します。
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string,
String: string,
Boolean: boolean,
Int: number,
Float: number,
};
export type Author = {
__typename?: 'Author',
id: Scalars['Int'],
firstName: Scalars['String'],
lastName: Scalars['String'],
posts?: Maybe<Array<Maybe<Post>>>,
};
export type AuthorPostsArgs = {
findTitle?: Maybe<Scalars['String']>
};
export type Post = {
__typename?: 'Post',
id: Scalars['Int'],
title: Scalars['String'],
author: Author,
};
export type Query = {
__typename?: 'Query',
posts?: Maybe<Array<Maybe<Post>>>,
};
export type Unnamed_1_QueryVariables = {};
export type Unnamed_1_Query = (
{ __typename?: 'Query' }
& { posts: Maybe<Array<Maybe<(
{ __typename?: 'Post' }
& Pick<Post, 'id' | 'title'>
& { author: (
{ __typename?: 'Author' }
& Pick<Author, 'id' | 'firstName' | 'lastName'>
) }
)>>> }
);
Unnamedが追加されていますね。
一応 $ git diff
で差分を確認するとこんな感じになります。
$ git diff
diff --git a/basic/client/gen/graphql-client-api.ts b/basic/client/gen/graphql-client-api.ts
index 9872345..7e8fba4 100644
--- a/basic/client/gen/graphql-client-api.ts
+++ b/basic/client/gen/graphql-client-api.ts
@@ -32,3 +32,18 @@ export type Query = {
__typename?: 'Query',
posts?: Maybe<Array<Maybe<Post>>>,
};
+
+export type Unnamed_1_QueryVariables = {};
+
+
+export type Unnamed_1_Query = (
+ { __typename?: 'Query' }
+ & { posts: Maybe<Array<Maybe<(
+ { __typename?: 'Post' }
+ & Pick<Post, 'id' | 'title'>
+ & { author: (
+ { __typename?: 'Author' }
+ & Pick<Author, 'id' | 'firstName' | 'lastName'>
+ ) }
+ )>>> }
+);
diff --git a/basic/codegen.yml b/basic/codegen.yml
index 91bbfd2..7f44df1 100644
--- a/basic/codegen.yml
+++ b/basic/codegen.yml
@@ -7,3 +7,4 @@ generates:
./client/gen/graphql-client-api.ts:
plugins:
- typescript
+ - typescript-operations
良さそうです。
Naming for query
UnnamedのqueryはJavascriptの無名関数と同じ感じでして、エラーが出た際やログを取る際に追いづらいです。
なので先程のqueryに重複しない名前を付けます。
名前はpostsにしようと思います。
query posts {
posts {
id
title
author {
id
firstName
lastName
}
}
}
差分としてはこうです。
$ git diff
diff --git a/basic/graphql/queries/posts.graphql b/basic/graphql/queries/posts.graphql
index 15b6e8a..25710a2 100644
--- a/basic/graphql/queries/posts.graphql
+++ b/basic/graphql/queries/posts.graphql
@@ -1,4 +1,4 @@
-query {
+query posts {
posts {
id
title
この状態でcodegenをします。
そうすると、以下のようにUnnamedがPostsに置き換わっています。
大分わかりやすくなりましたね。
$ git diff
diff --git a/basic/client/gen/graphql-client-api.ts b/basic/client/gen/graphql-client-api.ts
index 7e8fba4..026ab75 100644
--- a/basic/client/gen/graphql-client-api.ts
+++ b/basic/client/gen/graphql-client-api.ts
@@ -33,10 +33,10 @@ export type Query = {
posts?: Maybe<Array<Maybe<Post>>>,
};
-export type Unnamed_1_QueryVariables = {};
+export type PostsQueryVariables = {};
-export type Unnamed_1_Query = (
+export type PostsQuery = (
{ __typename?: 'Query' }
& { posts: Maybe<Array<Maybe<(
{ __typename?: 'Post' }
Duplicate naming
例えば以下のようなqueryがあるとします。
query {
posts {
id
title
}
}
これに先程と同じ名前を付けます。
query posts {
posts {
id
title
}
}
これだと中々まずいですよね。。。
どのqueryがどれかわからなくなってしまいます。
この時に codegen をすると、こんな感じにgraphql-codegenはエラーを吐いてくれます。
エラー本文は以下の通りです。
$ yarn graphql-codegen --config codegen.yml
yarn run v1.17.3
warning ../../package.json: No license field
$ /Users/k-okina/Documents/graphql-patterns/basic/node_modules/.bin/graphql-codegen --config codegen.yml
(node:17039) ExperimentalWarning: Readable[Symbol.asyncIterator] is an experimental feature. This feature could change at any time
✔ Parse configuration
❯ Generate outputs
❯ Generate ./client/gen/graphql-client-api.ts
✔ Load GraphQL schemas
✔ Load GraphQL documents
✖ Generate
→ Not all operations have an unique name: posts
Found 1 error
✖ ./client/gen/graphql-client-api.ts
Not all operations have an unique name
* posts found in:
- /Users/k-okina/Documents/graphql-patterns/basic/graphql/queries/posts.graphql
- /Users/k-okina/Documents/graphql-patterns/basic/graphql/queries/posts2.graphql
Error: Not all operations have an unique name: posts
at validateDuplicateDocuments (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/core/dist/commonjs/codegen.js:149:15)
at Object.codegen (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/core/dist/commonjs/codegen.js:13:9)
at process (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/cli/dist/commonjs/codegen.js:212:69)
at Array.map (<anonymous>)
at Listr.task.wrapTask (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/cli/dist/commonjs/codegen.js:219:63)
at process._tickCallback (internal/process/next_tick.js:68:7)
Error: Not all operations have an unique name: posts
at validateDuplicateDocuments (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/core/dist/commonjs/codegen.js:149:15)
at Object.codegen (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/core/dist/commonjs/codegen.js:13:9)
at process (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/cli/dist/commonjs/codegen.js:212:69)
at Array.map (<anonymous>)
at Listr.task.wrapTask (/Users/k-okina/Documents/graphql-patterns/basic/node_modules/@graphql-codegen/cli/dist/commonjs/codegen.js:219:63)
at process._tickCallback (internal/process/next_tick.js:68:7)
Something went wrong
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
posts という operation がこの2つから見つかりました。
- graphql/queries/posts.graphql
- graphql/queries/posts2.graphql
と丁寧に教えてくれていますね。
これで安心して命名ができそうです。
clientで直接使える関数を自動生成する
例えば React + Apollo + React hooksで開発していたとします。
typescriptの開発において、 typescript-operations
pluginを使って自動生成した型を利用して作成するhooksは凄く重要です。
typescript-react-apollo
pluginを利用すると、typescript-operations
pluginで自動生成した型を利用して、型付きのReact Hooksを自動生成してくれます。
codegen.ymlを以下のように編集します。
$ git diff
diff --git a/basic/codegen.yml b/basic/codegen.yml
index 7f44df1..cd7e17a 100644
--- a/basic/codegen.yml
+++ b/basic/codegen.yml
@@ -8,3 +8,8 @@ generates:
plugins:
- typescript
- typescript-operations
+ - typescript-react-apollo
+ config:
+ withHooks: true
+ withComponent: false
+ withHOC: false
configではhooksでだけ使いますよ〜と教えています。
この状態でcodegenを実行し、生成したファイルは以下の通りです。
import gql from 'graphql-tag';
import * as ApolloReactCommon from '@apollo/react-common';
import * as ApolloReactHooks from '@apollo/react-hooks';
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string,
String: string,
Boolean: boolean,
Int: number,
Float: number,
};
export type Author = {
__typename?: 'Author',
id: Scalars['Int'],
firstName: Scalars['String'],
lastName: Scalars['String'],
posts?: Maybe<Array<Maybe<Post>>>,
};
export type AuthorPostsArgs = {
findTitle?: Maybe<Scalars['String']>
};
export type Post = {
__typename?: 'Post',
id: Scalars['Int'],
title: Scalars['String'],
author: Author,
};
export type Query = {
__typename?: 'Query',
posts?: Maybe<Array<Maybe<Post>>>,
};
export type PostsQueryVariables = {};
export type PostsQuery = (
{ __typename?: 'Query' }
& { posts: Maybe<Array<Maybe<(
{ __typename?: 'Post' }
& Pick<Post, 'id' | 'title'>
& { author: (
{ __typename?: 'Author' }
& Pick<Author, 'id' | 'firstName' | 'lastName'>
) }
)>>> }
);
export const PostsDocument = gql`
query posts {
posts {
id
title
author {
id
firstName
lastName
}
}
}
`;
/**
* __usePostsQuery__
*
* To run a query within a React component, call `usePostsQuery` and pass it any options that fit your needs.
* When your component renders, `usePostsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = usePostsQuery({
* variables: {
* },
* });
*/
export function usePostsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<PostsQuery, PostsQueryVariables>) {
return ApolloReactHooks.useQuery<PostsQuery, PostsQueryVariables>(PostsDocument, baseOptions);
}
export function usePostsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<PostsQuery, PostsQueryVariables>) {
return ApolloReactHooks.useLazyQuery<PostsQuery, PostsQueryVariables>(PostsDocument, baseOptions);
}
export type PostsQueryHookResult = ReturnType<typeof usePostsQuery>;
export type PostsLazyQueryHookResult = ReturnType<typeof usePostsLazyQuery>;
export type PostsQueryResult = ApolloReactCommon.QueryResult<PostsQuery, PostsQueryVariables>;
useなんたらが追加されていますね。
git diff
して以前との差分を見てみましょう。
$ git diff
diff --git a/basic/client/gen/graphql-client-api.ts b/basic/client/gen/graphql-client-api.ts
index 026ab75..e4cf007 100644
--- a/basic/client/gen/graphql-client-api.ts
+++ b/basic/client/gen/graphql-client-api.ts
@@ -1,3 +1,6 @@
+import gql from 'graphql-tag';
+import * as ApolloReactCommon from '@apollo/react-common';
+import * as ApolloReactHooks from '@apollo/react-hooks';
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
@@ -47,3 +50,43 @@ export type PostsQuery = (
) }
)>>> }
);
+
+
+export const PostsDocument = gql`
+ query posts {
+ posts {
+ id
+ title
+ author {
+ id
+ firstName
+ lastName
+ }
+ }
+}
+ `;
+
+/**
+ * __usePostsQuery__
+ *
+ * To run a query within a React component, call `usePostsQuery` and pass it any options that fit your needs.
+ * When your component renders, `usePostsQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = usePostsQuery({
+ * variables: {
+ * },
+ * });
+ */
+export function usePostsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<PostsQuery, PostsQueryVariables>) {
+ return ApolloReactHooks.useQuery<PostsQuery, PostsQueryVariables>(PostsDocument, baseOptions);
+ }
+export function usePostsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<PostsQuery, PostsQueryVariables>) {
+ return ApolloReactHooks.useLazyQuery<PostsQuery, PostsQueryVariables>(PostsDocument, baseOptions);
+ }
+export type PostsQueryHookResult = ReturnType<typeof usePostsQuery>;
+export type PostsLazyQueryHookResult = ReturnType<typeof usePostsLazyQuery>;
+export type PostsQueryResult = ApolloReactCommon.QueryResult<PostsQuery, PostsQueryVariables>;
これは凄く便利ですね。
ちなみにuseLazyQuery等に関してはこのドキュメントを読んでください。
Infinite nested
GraphQLで制限なくネストできるschemaを定義できたらどうなるのでしょうか?
例えば以下のschema定義があったとします
type Author {
id: Int!
firstName: String!
lastName: String!
posts(findTitle: String): [Post]
}
type Post {
id: Int!
title: String!
author: Author!
}
type Query {
posts: [Post],
authors: [Author]
}
type Mutation {
addPost (title: String!, authorId: ID!): Post
}
これに対してはこの様な問い合わせができます。
この問い合わせを使うと記事一覧と一緒にユーザー名も出せそうですね。
query {
posts {
id
title
author {
id
firstName
lastName
}
}
}
しかし、以下のような問い合わせもこのschema定義からだと制限されていないので注意が必要です。
query posts {
posts {
id
title
author {
id
firstName
lastName
posts {
id
title
author {
id
firstName
lastName
posts {
id
title
author {
id
firstName
lastName
posts {
id
title
author {
id
firstName
lastName
}
}
}
}
}
}
}
}
}
これに対する良くある対策が、
- schema定義の見直し
- n階層を超える問い合わせを超えたらエラーをcompiler or serverが出す
- 循環の回数がn回を超えたらエラーをcompiler or serverが出す
この深さ無制限のschemaを許可リリースするとフロント側でかなり色々できるqueryが書けるのですが、それが使いやすく、スピードがでるかはまた別の話です。