Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 5 years have passed since last update.

GraphQL Deep Dive

Posted at

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 さんの記事がわかりやすいのですが、一応ここでもざっくり最小限の解説だけします。

codegen.yml
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の設定を書いてる気分になりました)。

例えば、

./graphql/schema.graphql
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

を実行すると、

以下のファイルが生成されます。

./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>>>,
};

色々とわかりやすく、便利な型が出てきましたね!

Validate Operations

WebやIOS、Androidでgraphqlサーバーに問い合わせる際には以下の様なqueryを書いてHTTPのPOSTで問い合わせるのが (※4) 標準的な方法です。
以下のファイルを作成します。

※4
パフォチュでhttp2コネクションを維持してやりとりしたり、web socketを利用したりする手法もありますが、ここでは割愛します

graphql/queries/post.graphql
query {
  posts {
    id
    title
    author {
      id
      firstName
      lastName
    }
  }
}

このqueryって果たして本当に正しい操作なのでしょうか? :thinking:
安心してください、この操作が正しいかどうかはcodegenに読み込ませて解析させればランタイムではなく、静的に検査ができます。

overwrite: true
schema:
  - ./graphql/schema.graphql
documents: # このdocumentsを追加して、queryを読み込ませます
  - ./graphql/queries/*.graphql
generates:
  ./client/gen/graphql-client-api.ts:
    plugins:
      - typescript

このconfigを使ってcodegenをします。

Image from Gyazo

上手く動きましたね。
※ ちなみに生成されたtypescriptのコードは先程と同じです。

それでは実際にschema違反である、定義されていないfieldであるfullNameをqueryに定義します。

query {
  posts {
    id
    title
    author {
      id
      firstName
      lastName
      fullName
    }
  }
}

codegenを実行します。

Image from Gyazo

このようにエラーを出します。
エラーメッセージを一応貼っておきます。

$ 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をします。

Image from Gyazo

動きましたね。
生成された 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があるとします。

graphql/queries/posts2.graphql
query {
  posts {
    id
    title
  }
}

これに先程と同じ名前を付けます。

graphql/queries/posts2.graphql
query posts {
  posts {
    id
    title
  }
}

これだと中々まずいですよね。。。
どのqueryがどれかわからなくなってしまいます。
この時に codegen をすると、こんな感じにgraphql-codegenはエラーを吐いてくれます。

Image from Gyazo

エラー本文は以下の通りです。

$ 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定義からだと制限されていないので注意が必要です。

graphql/queries/posts.graphql
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が書けるのですが、それが使いやすく、スピードがでるかはまた別の話です。

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?