こんにちわ。 OPENLOGI AdventCalendar 4日目です。
普段は倉庫の管理システム(WMS)の開発をしています。
今回はロジスティクスと全く関係ありませんが、とある社内システムをGraphQLを用いて実装したのでGraphQLについて書きます。
2015年にFacebookがGraphQLを公開してはや2年。公開当時は衝撃を受けた記憶がありますが、まだまだ潮流にはなっていないというところでしょうか。
GitHubを始め少しづつAPIとしてGraphQLを利用しているサービスが増えてきた中、GraphQL自体は何となく何をするか浸透している気がします。でも一方で、実際の実装のイメージを持っている人は割と少ないイメージがあり、ハードルが高いような気がしてます。ということで、このハードル、「どうやって実装すんの?」というところが何となく分かってもらえれれば幸いです。
GraphQL自体については、GraphQL入門 - 使いたくなるGraphQLが非常に秀逸なので、そちらの記事を参照していただけると良いかと思います。
ともあれ、本記事では具体的な実装をざっくりと説明して行こうと思います。
(GraphQLの何が嬉しいの?ってことについては本記事では割愛です。)
さて、ではまずはGraphQLのWebサイトを見て見ましょう。
はい。DescribeしてAskしてGetするみたいです。どうやるんでしょうね。
用意するもの
言語は何でもいいですが、私はnodejsとかpythonでしか実装したことがないので、基本その例での説明がメインです。
アプリケーションサーバー
クライアントからのエンドポイントがいるだけなので何でもいいです。nodejsであればkoaでも使っておきましょう。
GraphQLサーバー
これ。よくGraphQLサーバーって言い方がされてるんですが、紛らわしい!何か特別なサーバーがいるのかと最初思ってました。(自分だけ?)
単にGraphQLを使うためのエンドポイントに来たリクエストを処理するやつのことです。要はGraphQLのクエリをパースして、データの取得ロジックに処理をディスパッチする実装のこと。自分で実装してもいいかもしれませんが、大抵の言語/フレームワークにはすでにライブラリがあるので使いましょう。例えばpythonであれば、graphene。nodejsであればapollo-serverとかfacebookのgraphql-js。
実装例としては、koa × apollo について、上述のアプリケーションサーバーとGraphQLサーバーを合わせてこんな感じになります。
import koa from 'koa';
import koaRouter from 'koa-router';
import koaBody from 'koa-bodyparser';
import { graphqlKoa, graphiqlKoa } from 'apollo-server-koa';
const app = new koa();
const router = new koaRouter();
router.post('/graphql', graphqlKoa({ schema: myGraphQLSchema }));
app.use(router.routes());
app.listen(3000);
はい。/graphql
エンドポイントにPOSTされたら graphqlKoa
(ライブラリ)に渡すだけです。簡単ですね。
この中の myGraphqlSchema
オブジェクトが、型の定義やら、データの取得ロジックやら何やら、つまりアプリケーションの実装をする箇所です。(後述)
データベース
じゃなくてもいいんですが。GraphQLはあくまで遠隔地にあるデータを取得するためのクエリ言語、つまりインターフェースになるだけなので、サーバーサイドでは当然データにアクセスできる必要があります。データベースでもAPIでも、モック的に作るのであればなくてもいいです。RDBを使うのであればお好きなOR Mapperも使いましょう。要はこの部分には一切の制約がない、ということ。上述の myGraphqlSchema
オブジェクトに、データベースアクセスなど実装していきます。
Describe your data
さてようやく本題です。まずはどうやってDescribeするの?ということについて。
これを見ると、さも
type Project {
name: String
tagline: String
contributors: [User]
}
みたいに書かなきゃいけないみたいですが、別にそんなことはありません。これは完全に上述のGraphQLサーバーの実装次第です。
そもそもfacebookのgraphql-jsでは
const Project = new GraphQLObjectType({
name: "Project",
description: "Project description",
fields: () => ({
name: {type: GraphQLString},
tagline: {type: GraphQLString},
})
})
みたいに書くし、pythonのgrapheneだと、
import graphene
class Project(graphene.ObjectType):
name = graphene.String(name=graphene.String())
tagline = graphene.String(name=graphene.String())
.
.
って感じでclass形式で定義します。apollo-serverは
type Project {
name: String
tagline: String
contributors: [User]
}
って書くんですけどね。忠実。
要は何でもいいんです。型を定義できれば。あとは、ライブラリがやってくれるので。
では何のためにこういう定義をするんでしょう?雑に言ってしまえば、クエリをパースするときとか、レスポンスを返すときにに型チェックするためですね。これが非常に大きなGraphQLの特徴です。
そして、この型はデータベースのテーブルと一対一になってないといけないわけではありません。名前を変えてもいいし、全く違うスキーマにしてもいいし、新しく増やしてもいいし、複数のテーブルをまとめて一つの型として表現してもいい。この辺りはそのアプリケーションにどのようなインターフェースを作りたいかというところで考えていく所になります。
とはいえ本記事ではGraphQLの型について詳しい説明をしたいわけではないので、さくっと次に行きます。
Ask for what you want
はい。こういうクエリ言語ってことです。まる。クライアントからはこの文字列をサーバーに投げます。
{
project(name: "Graphql"): {
tagline
}
}
特徴的なのは引数があること。これはルートノードだけにあるわけではなく、どのノードにも引数を指定できます。例えば特定のプロジェクトのコントリビュータを最初の5件だけ取得したい場合はこんな感じ。
{
project(name: "Graphql"): {
tagline
contributers(first: 5) {
name
}
}
}
Get predictable result
さて、ではどうやってデータを返すのか。これももちろん利用するライブラリによるのですが、、イメージしやすいようにapollo-serverを例に実際の実装を紹介します。上述の myGraphqlSchema
の実態です。まずはざっと見てみてください。
const { makeExecutableSchema } = require('graphql-tools');
// 型の定義部分
// projectは引数に必須で数値のidをとり、contributorsは引数に数値のfirstをとる。
const typeDefs = `
type Query {
project(id: Int!): Project
}
type Project {
name: String
tagline: String
contributors(first: Int): [User]
}
type User {
name: String
email: String
}
`;
const resolvers = {
// Query型から返す値を取得する実装。
Query: {
project: (root, args) =>
fetch(`select id, name, tagline from projects where id = ${args.id}`)
},
// Project型から返す値を取得する実装。
Project: {
name: project => project.name,
tagline: project => project.tagline,
contributors: (project, args) =>
fetch(`select * from contributors where project_id = ${project.id} limit ${args.first}`)
},
// User型から返す値を取得する実装
User: {
name: user => user.name,
email: user => user.email
}
};
const schema = makeExecutableSchema({ typeDefs, resolvers });
typeDefs
は、上述の Describe your data にあたる部分です。そして、 resolvers
が実際のデータの取得ロジックを書くところ。 typeDefs
に書いた、自身のプロパティの取得方法を一つ一つ定義していくことになります。
Query
というのが予約語的な型で、クエリのrootノード的な型です。現在 Query
型には project
のみを定義していますが、例えばそれ以外に、ユーザーの一覧が欲しい! アクティブなユーザーの一覧が欲しい!など他のクエリをかきたい場合はQuery型を変更して定義します。
type Query {
project(id: Int!): Project
users: [User]
activeUsers: [User]
}
同様に、 resolvers
も実装します。
const resolvers = {
// Query型から返す値を取得する実装。
Query: {
project: (root, args) => fetch(`select id, name, tagline from projects where id = ${args.id}`),
users: () => fetch('select * from users'),
activeUsers: () => fetch('select * from users where active = 1'),
},
.
.
そうすると、
query {
users {
name
}
activeUsers {
name
}
}
ってクエリ書いて、
{
"users": [
{
"name": "harada"
}, {
"name": "yamada"
}, {
"name": "tanaka"
}
],
"activeUsers": [
{
"name": "harada"
}, {
"name": "yamada"
}
]
}
のようなデータを取得できるようになります。
resolvers
の定義はまさにRESTで実装する際のエンドポイントと同じで、データベースアクセスなどをしてデータを取得する部分です。GraphQLでは、ある型に対して、 その型から付随して取得できるデータだけ について実装することになるので、非常にスコープが限られる事で、誰が見ても理解しやすい実装をしやすいのが特徴です。
おわりに
ということで、ざっと GraphQLを実装するってどういういこと?という事をイメージいただけたでしょうか。
実際には、それぞれの型のモジュール化や、n + 1 問題、セキュリティ、サーバーへ負荷など考慮すべきことが非常にたくさんあります。それはまた次回書く機会がありましたら書こうかなと思っております。