👌

GraphQL 入門

2023/05/14に公開

今回は、GraphQLについて解説していきます。

かなり初歩的な内容から説明していくので、GraphQLを触ったことがないという方は特に参考になるかと思います。

そもそもGraphQLとは?

そもそもGraphQLとは、APIで使用するクエリ言語です。

要はSQLと同じように、データの問い合わせをすることができます。

これだけ聞いても良くわからないと思うので、RESTのAPIと比べましょう。

例えば、QiitaのAPIなどで記事一覧とそれに紐づくユーザーの情報両方が欲しいとなった時は、記事の取得のAPIを叩いて、その後にそれぞれ紐づくユーザー情報のAPIを叩く必要があります。

このように複数のリソース情報が欲しい時は、複数回APIを叩く必要があります。

また、返ってくるデータが決まっているので、不要なデータまで取得することになります。

逆に、GraphQLはエンドポイントは1つに決まっていて、どのデータを取ってくるかはクエリ言語で指定することになります。

つまり、GraphQLの方が柔軟にデータを取得することができるのです。

GraphQLの実装方法

次に、GraphQLの実装方法について解説していきます。

GraphQLサーバーの用意

GraphQLを使用するためには、サーバー側にそれ用のライブラリを使用する必要があります。

例えば、Apollo Serverが有名なので、今回はApollo Serverを使用していきます。

まずは必要なパッケージをインストールしておきましょう。

npm init -y

npm install apollo-server graphql

Schemaの定義

次に、schemaを定義していきます。

要は、使用するデータの型を書いていくことになります。

そして、schemaには次の4つの種類があります。

  • Object
  • Query
  • Mutation
  • Subscription

これらは順を追ってそれぞれ解説していきます。

まず、Objectタイプのschemaを定義します。

これは、その名の通りユーザーなどのオブジェクトを表現するためのshcemaです。

今回は、Bookというtitleとauthor2つのフィールドを持つオブジェクトを定義します。

次にQueryタイプを定義します。

Queryタイプは、HTTPのGetメソッドと同じように、データの取得を行うための型です。

まずは、この2つを定義して動作確認をしていきましょう。

書き方は次のとおりです。

touch server.js
import { gql } from "@apollo/server";

const typeDefs = gql`
  type Book {
    title: String!
    author: String
  }

  type Query {
    books: [Book]
  }
`;

型の末尾に!を付けることで、この値はnullにならないことを示しています。

Scalar型

ちなみに、GraphQLでデフォルトで用意されている型は次の6つです。

  • Int: 整数
  • Float: 符浮動小数点数
  • String: 文字列
  • Boolean: trueまたfalse
  • ID: 一意な識別子

また、Date型など、独自で型を定義したい場合は定義することも可能です。

resolverの定義

次に、resolverを設定していきます。

Queryのshcemaを作ったことで、どのようなデータを返すかなどは定義できました。

けれど、実際にどのような処理をしてそのデータを返すかは全く書けていません。

それを設定するためには、resolver関数を設定する必要があります。

具体的には以下のような、記述となります。

const books = [
  {
    title: "Harry Potter and the Chamber of Secrets",
    author: "J.K. Rowling",
  },
  {
    title: "Jurassic Park",
    author: "Michael Crichton",
  },
];

const resolvers = {
  Query: {
    books: () => books,
  },
};

これで、booksというQueryが呼ばれた時に、どのような処理を行うか設定することができました。

今回の例でいうと、事前に用意しておいた配列のデータを返しています。

サーバーのインスタンス化

最後に、ApolloServerをインスタンス化して使えるようにします。

引数としては、shcemaとresolverを設定するだけでOKです。

import { ApolloServer } from "@apollo/server";

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

これで、node server.jsを実行することで、ローカルサーバーが起動されます。

ローカルサーバーでは、Playgroundと言って実際にQueryなどを試すことができます。

実際の下記のクエリを投げると、データが返ってくるかと思います。

query{
  books {
    author
    title
  }
}

mutationの実装

ある程度のGraphQLの理解が進んできたかと思うので、次にmutationを実装していきます。

mutationはHTTPメソッドで言う、POST,PUT,DELETEを担当します。

今回は、bookを1つPOSTするmutationを作成しようと思います。

const typeDefs = gql`
  type Book {
    title: String!
    author: String
  }

  type Query {
    books: [Book]
  }
  
  type Mutation {
    createBook(title: String!, author: String!): Book
  }
`;

const resolvers = {
  Query: {
    books: () => books,
  },
  Mutation: {
    createBook: (_, { title, author }) => {
      const book = { title, author };
      books.push(book);
      return book;
    },
  },
};

今回は面倒なのでDBを用意してませんが、実際はDBの操作などをresolverとして設定することになるかと思います。

実際にPlaygroundを開いて、次のmutationを叩きましょう。

mutation($title: String!, $author: String!) {
  createBook(title: $title, author: $author) {
    title
    author
  }
}

もう一度bookのQueryを実行すると、データが増えているのが分かるかと思います。

また、今回は実装しませんが、同じようにデータの更新や削除も行うことができます。

resolverの引数

ここでresolverの引数について解説します。

resolverには、次の4つの引数が渡ってきます。

  • parent
  • args
  • context
  • info

parentは少し複雑なので、最後に解説します。

まず、argsですがこちらは先ほど使用したように、クライアント側で指定した値が入ってきます。

今回の例で言うと、titleとauthorになりますね。

次に、contextですがこれはresolver内で使用できる、値になります。

デフォルトだと空ですが、インスタンス生成時に引数と渡すことで、どのresolverからも使用することができるようになります。

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    return {
      ...req,
      db: "db情報",
    };
  },
});

ここにはDBの情報や、認証情報などを登録しておくのが一般的です。

ちなみに、ここにはObjectか関数を登録することができ、関数の場合は引数としてリクエストの内容を取得することができます。

次にinfoですが、ここにはクエリ実行に関する詳細情報(実行中のフィールド名、リターンタイプなど)が入ります。

ただ、使う場面はあまりないので、頭の片隅に置いておけばOKです。

最後に、parentを解説します。

まず、authorsと言う新しいデータを定義します。

const authors = [
  {
    name: "J.K. Rowling",
  },
  {
    name: "Michael Crichton",
  },
];

次に、schemaを新たに定義します。

type Author {
    name: String
    books: [Book]
  }
type Query {
    books: [Book]
    authors: [Author]
  }

最後に、resolverに次の設定を追記します。

Author: {
    books: (author) => {
      return books.filter((book) => book.author === author.name);
    },
  },

こうすることで、以下のクエリが叩かれた際に、自動的にそれに紐づくbooksを取得してくることができます。

query{
  authors {
    name
    books {
      title
      author
    }
  }
}

そして、この時のresolver関数に使われているのがparentです。

要は、親の階層のデータを参照することができるのです。

このような仕組みから、データの関係性などを解決することができます。

ちなみに余談ですが、下記のようにObjectのフィールドに対してresolver関数を設定することもできます。

Book: {
    title: (book) => book.title + "!!",
  },

mutationやqueryの書き方

ここまで見て、イマイチmutationやqueryの書き方が分からんという人もいるかと思います。

なので、もう少し詳しく説明していきます。

queryの書き方

queryの書き方下記を参考に解説します。

query getAuthors{
  authors {
    name
    books {
      title
      author
    }
  }
}

今まで、queryに名前をつけてきませんでしたが、名前をつけることも可能です。

名前をつけた方がメリットがいくつかあるので、実際はつけるのが一般的です。

そして、queryでは指定したデータを取得することができます。

例えば、下記のように書いた場合は、authorのnameのみを取得することができます。

query getAuthors{
  authors {
    name
  }
}

また、fragmentというものを設定でき、これを活用することで簡単にqueryなどを書けるようになります。

mutationの書き方

次にmutationの書き方です。

mutation createBook{
  createBook(title: "SaunanoKyokasyo", author: "Kato") {
    title
  }
}

このようにqueryと同じく名前をつけることができ、取得する値も変更できます。

また、今回のように直接引数を入力することもでき、最初に見せた例の通り変数として渡すこともできます。

Subscription

次に、subscriptionの説明をしていきます。

subscriptionとは、クライアント側へ変更を通知するための仕組みです。

例えば、データが更新されたタイミングでクライアント側にpushすることができ、クライアント側はそのデータを元にリアルタイムの通信を行うことが可能になります。

ただ、現状のApolloServerだけだとSubscriptionを実装することはできず、subscriptions-transport-ws などを使用する必要があります。

そのため少し実装が複雑になるので、今回は実装しません。

フロントエンドの実装

最後に、フロントエンド側の実装をしていきます。

また、今回はReactで実装しようかと思います。

なので、まずはReactのプロジェクトを作成します。

npx create-react-app frontend

次にGraphQL Clientのライブラリをインストールします。

使わなくてもGraphQLを実行できますが、かなり面倒で特にメリットもないので普通は使用されます。

cd frontend
npm install @apollo/client graphql

あとは次のように実装すればOKです。

import ReactDOM from "react-dom/client";
import App from "./App";
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";

const client = new ApolloClient({
  uri: "http://localhost:4000",
  cache: new InMemoryCache(),
});

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

まず、<ApolloProvider>でAppコンポーネントを囲み、GraphQLクライアントをインスタンス化したものを設定します。

import { gql, useQuery } from "@apollo/client";
const GET_POSTS = gql`
  query GetBooks {
    books {
      title
      author
    }
  }
`;

function App() {
  const { data, loading, error } = useQuery(GET_POSTS);

  if (loading) return "<p>ローディング中です</p>";
  if (error) return "<p>エラーが発生しています。<p>";

  return (
    <div style={{ margin: "3em" }}>
      <h1>GraphQL</h1>
      {data.books.map((book) => (
        <div key={book.title}>Name: {book.author}</div>
      ))}
    </div>
  );
}

export default App;

あとは、Queryを作成してuseQueryにセットするだけです。

そうすることで、useQuerydata,loading,errorを良しなに返してくれます。

これで一通りの実装は終わりです。

まとめ

今回はGraphQLについて解説してきました。

かなり便利な技術なので、ぜひこの機会に身につけましょう。

宣伝

0からエンジニアになるためのノウハウをブログで発信しています。
https://hinoshin-blog.com/

また、YouTubeでの動画解説も始めました。
https://www.youtube.com/channel/UCqaBUPxazAcXaGSNbky1y4g

インスタの発信も細々とやっています。
https://www.instagram.com/hinoshin_enginner/

興味がある方は、ぜひリンクをクリックして確認してみてください!

Discussion