これを読めばGraphQL全体がわかる。GraphQLサーバからDB、フロントエンド構築
本文書は今後GraphQLを利用する機会があるかもしれないのでGraphQLを一通り一度に学習したいというGraphQLの入門者の方に向けて作成したチュートリアルです。
GraphQLのサーバの構築(Apollo Server)、GraphQLサーバからREST APIを使って外部リソースからのデータ取得(JSONPLACEHolder)、データベースの接続(Prisma)、クライアント(React, Vue)からGraphQLサーバにアクセスしてデータを取得するまで流れ実際に手を動かしながら動作確認を行なっていくのでGraphQLの全体像を掴むのに必要な基本的な知識を習得することができます。
GraphQLの本質とは離れた場所でできるだけ悩まないようにシンプルな構成で動作確認を行なっています。
GraphQLとは
GraphQLという名前がどのような意味を持っているか確認していきます。GraphQLという名前はGraphとQLの2つに分けることができます。最初の単語GraphはGraphQLで扱うデータが以下の図のようにnodeとedgeを使ってグラフのように表されることからGraphという名前が利用されています。
アプリケーション内のデータはオブジェクトから構成され通常オブジェクト間はリレーションを持っています。下記の図のTrackオブジェクトはauthor idを通してAuthoerオブジェクトと繋がっていることを表しており、オブジェクトをnode、リレーションショップをedgeとして描くと下記のようなグラフとして考えることができます。グラフと言えば縦軸と横軸のあるものを思い浮かべるのでここでいうGraphのイメージはなかなか理解し難いかもしれません。
後半のQLはQuery Language(クエリー言語)の略です。クエリーと言えばデータベースを思い浮かべる人も多いと思いますがデータベースとは関係がなくSQLのようにデータベースをクエリーで操作するための言語ではなくAPIのためのクエリー言語です。
GraphとQLを合わせたGraphQL自体はREST APIの代替となる規格です。
クライアントからサーバに対してCRUD(Create, Delete, Update, Delete)を行う際に利用するREST APIと同様にGraphQLもクライアントからサーバに対してCRUDする際に利用することができます。GraphQLはREST APIの代替ということでREST APIを使って比較して説明されます。例えばREST APIではデータを取得する際に複数のエンドポイント(例:ユーザ一覧から/users, ブログの記事一覧なら/posts,…)を利用します。GraphQLでは1つのエンドポイントのみ持ちクエリーを設定(問い合わせなのか更新または削除なのか、どのデータが欲しいかを指定)することでサーバからデータを取得することができます。クエリーの設定によって一度のHTTPリクエストで一括でユーザ情報、ブログ記事情報を取得することも可能な上、ユーザ情報の中からはemailのみ選択して取得するといったことも可能です。REST APIでは/usersにアクセスしてデータを取得する際はユーザ情報全体を取得するためemailのみ選択して取得することはできません。(/usersにアクセスした場合にバックエンド側でemailのみを返すように設定している場合は除きます)
GraphQLはREST APIが抱える2つの問題を解決することを目的に開発されています。一つはUnderfetchingで必要なデータを取得する際に1つのエンドポイントから十分なデータが取得できない場合に複数のエンドポイントに対してリクエストを送信しなければならないことです。もう一つはOverfetchingと呼ばれ先ほど説明しましたがユーザデータの中からemailのみ取得するといったことができず必ず一緒に必要でない情報(ブラウザ上に描写しないデータ)を取得しなければならないことです。
言葉で説明するよりも実際に動作確認を行うとGraphQLがどのようなものかすぐに納得できるかと思います。現時点でGraphQLのイメージがわからない人も本文書を読み終えた時はGraphQLドキュメントに記載されている下記の説明(これまで説明したきた内容の要約)が理解できるようになるかと思います。
“GraphQL is a new API standard that provides a more efficient, powerful and flexible alternative to REST.At its core, GraphQL enables declarative data fetching where a client can specify exactly what data it needs from an API. Instead of multiple endpoints that return fixed data structures, a GraphQL server only exposes a single endpoint and responds with precisely the data a client asked for.”
GraphQLサーバの構築
REST APIにプロダクトが存在しないようにGraphQLという名前のプロダクトが存在するわけではないのでGraphQLサーバを構築する方法はいくつかあります。本文書ではGraphQLサーバの構築はApollo Serverを利用して行います。ここからの構築作業では動作確認環境にNodeがインストールされている必要があります。もし環境にNodeがインストールされていない場合はインストールを行っておく必要があります。
Apollo Serverの構築
任意の名前のプロジェクトフォルダgraphql-apollo-serverを作成します。作成後、作成したフォルダに移動します。npm init -yコマンドでpackage.jsonファイルを作成します。
% mkdir graphql-apollo-server
% cd graphql-apollo-server
% npm init -y
Apollo Severにはapollo-serverとgraphqlのライブラリをインストールする必要があります。graphqlはGraphQLのスキーマとスキーマに対してクエリーを実行するために利用するライブラリで、graphqlはサーバだけではなくクライアント側でもインストールします。
% npm install apollo-server graphql
index.jsファイルを作成します。
% touch index.js
index.jsファイルの更新を検知でき更新した内容が自動反映できるようにnodemonもインストールを行っておきます。インストール後にnpx nodemon index.jsコマンドを実行するとindex.jsファイルの中身が実行されファイルの変更の監視が開始されます。
% npm install --save-dev nodemon
% npx nodemon index.js
[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
[nodemon] clean exit - waiting for changes before restart
作成したindex.jsファイルにGraphQLサーバの設定を記述していきます。index.jsファイルを開いて、apollo-serverからApollo Serverの設定に必要なモジュールApolloServerをrequireします。gqlはスキーマを定義する際に利用します。
const { ApolloServer, gql } = require('apollo-server');
動作確認としてApollo Serverに対してhelloというクエリーを実行すると”Hello World”という文字列を戻す設定を行なっていきます。Node.jsのExpress.jsを設定したよりも”Hello World”という文字列を戻すための設定が必要となりますが我慢して読み進めてください。
schema(スキーマ)の定義
GraphQLではスキーマを使って型の定義を行う必要があります。GraphQLは独自のスキーマ定義言語(SDL: Schema Definition Language)を持っており、SDLを使って型を定義していきます。クライアントがアクセスするためのデータの定義を行うだけではなく問い合わせ(Query)、更新、削除(Mutation)などにも型定義を行なっていきます。
スキーマでは主にクライアントがアクセスを行うオブジェクトタイプのスキーマを定義していきます。オブジェクトタイプはGraphQLを利用したことがなくてもデータベースを利用した経験がある人であればイメージが湧きやすいタイプです。ユーザに関するオブジェクトタイプであれば下記のような形を持ちます。各フィールドにはフィールド名と型を設定するだけです。
type User {
id: ID!
name: String!
email: String!
}
スキーマタイプにはオブジェクトタイプの他にQueryタイプ、Mutationタイプ、Subscriptionタイプがあります。
QueryタイプはRead Operation(問い合わせ)を行うクエリーの定義に利用します。MutationタイプはWrite Operations(作成、更新、削除)に利用します。SubscriptionタイプはChatアプリなどのようなリアルタイムのRead Operationに利用します。Subscriptionは本文書では扱いません。
Queryタイプについてはクライアントが実行するクエリーを定義していきます。フィールド毎に名前とクエリーの実行後に戻される型を設定します。
では実際に動作確認を行うhelloクエリーをQueryタイプに追加していきましょう。スキーマはgqlタグの中に記述していきます。Queryタイプの中でhelloのクエリーが実行されるとStingのタイプが戻されるということを定義しています。Queryタイプの中に実際のクエリーの処理を記述することはありません。他のクエリー定義を追加したい場合はQueryタイプの中に追加していくことになります。
const typeDefs = gql`
type Query{
hello: String
}
`
resolvers(リゾルバ)の設定
スキーマの中でアプリケーション内で扱うデータがどのようなフィールドを持ち、そのフィールドがどのような型を持っているのか定義することがわかりました。しかしどのように定義した型通りのデータを設定するかが記述されていません。helloをQueryタイプに追加し、helloはStringの型を戻すことはわかりましたがどのような文字列が戻されるのかもわかりません。Queryタイプで定義した戻り値の型通りにどのように処理を行うのかを記述するのがresolver(リゾルバ)です。リゾルバは関数を使って記述します。
Queryタイプに記述したhelloクエリーは戻り値がStringになることが定義されています。そのためリゾルバでは関数を使って以下のように記述することができます。戻り値はhelloクエリーで定義したStringになっています。もし文字列を戻さない場合には型が異なるためエラーになります。リゾルバの中ではスキーマの定義を元にデータ処理を記述していきます。
const resolvers = {
Query: {
hello: () => 'Hello World',
},
}
サーバの起動
GraphQLサーバの起動に作成したスキーマとリゾルバを設定します。
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
ここまでの全体のコードの記述は下記の通りです。クライアントからのクエリーからデータを戻すことができる最もシンプルなGraphQLサーバです。
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type Query{
hello: String
}
`
const resolvers = {
Query: {
hello: () => 'Hello World',
},
}
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
ここまでの設定で本当に”Hello World”が戻されるのか確認したいと思います。nodemonを使ってindex.jsファイルを記述していくとnodemonを実行したコンソールにGraphQLサーバのURLが表示されます。
[nodemon] starting `node index.js`
🚀 Server ready at http://localhost:4000/
ブラウザでhttp://localhost:4000にアクセスすると下記の画面が表示されます。”Query your server”ボタンをクリックしてください。
Apollo Studioが起動します。クラウドベースのツールでApollo Studio Explorerを利用することでブラウザ上からクエリーを実行し動作確認を行うことができます。画面の左側を見るとindex.jsで設定したhelloを確認することができます。
Operationsを空にした状態から左側のDocumentation(ドキュメンテーション)のQueryとFieldsの下にあるhelloをクリックしてください。Operationsにクエリーが表示されます。これがGraphQLに送信するクエリーの中身です。右上の”Query”ボタンをクリックしてください。
Responseに”Hello World”が表示されます。GraphQLを対してクエリーを実行し、Queryタイプで設定した型で、リゾルバで設定した文字列が戻されることが確認できました。
ここまでの動作確認でGraphQLでは”スキーマ”と”リゾルバ”というものが重要な構成要素だということが理解できました。
さらにhelloを使って動作確認を行います。helloクエリーに引数を設定することもできます。クエリーに引数を設定する場合は引数にも型の設定を行う必要があります。nameという引数の型をStringに設定しています。文字以外の数字を入れるとエラーになります。
const typeDefs = gql`
type Query {
hello(name: String): String
}
`;
引数を設定後、Apollo Studio Explorerでクエリーを実行しても戻される値にも変化はありません。次に必須を表わす”!”を引数のタイプのStringの後に追加します。
const typeDefs = gql`
type Query {
hello(name: String!): String
}
`;
引数が必須になったことでApollo StudioのOperationsの中にメッセージが表示されるようになります。型の設定によって間違いがある場合にすぐに気がつくことができます。
Queryタイプで引数を設定した場合にはリゾルバでも引数が取得できるように設定を行う必要があります。リゾルバの関数にはparent, args, context, infoの4つ引数を受け取ります。各引数については今後利用する場合に説明をしていきます。ここで知りたいのはQueryタイプの引数で設定したnameの値です。argsはargumentの略で引数を持っているのでargsにどのように値が入るか確認を行います。
const resolvers = {
Query: {
hello: (parent, args) => {
console.log('parent', parent);
console.log('args', args);
return 'Hello World';
},
},
};
Apollo Studio上ではクエリーは下記のように引数を設定することができます。
”Query”ボタンをクリックするとGraphQLサーバを起動しているコンソールにオブジェクトとして”John Doe”が入っていることがわかります。parentは”undefined”です。parentについては後ほど出てくるのでここで”undefined”になっていることを覚えていてください。
parent undefined
args { name: 'John Doe' }
argsにオブジェクトとして引数が入っていることがわかったのでnameの値を取得するために下記のように更新することができます。
const resolvers = {
Query: {
hello: (parent, args) => `Hello ${args.name}`,
},
};
再度クエリーを実行するとResponseに引数に設定した”John Doe”が含まれて表示されます。引数の設定方法を理解することができました。
スキーマの追加(User)
Queryタイプではhelloクエリーを定義しましたが次はオブジェクトタイプのスキーマを定義する方法を確認していきます。オブジェクトタイプには特別なタイプであるQuery、Mutation、Subscriptionという名前はつけることができませんがそれ以外の任意の名前をつけることができます。
オブジェクトタイプは通常複数のフィールドで構成されます。Userタイプがidとnameとemailの3つのフィールドで構成されている場合下記のように記述することができます。各フィールドにもそれぞれ型の設定を行います。フィールドタイプにはスカラータイプとオブジェクトタイプがあります。スカラータイプにはString, Int, ID, Booleanなどがあります。idにはID型を設定しています。ID型はString型と同じ文字列ですが一意である必要があります。nameとemailにはString型を設定しています。”!”がついているのでどのフィールドも値が必須です。オブジェクトタイプは定義済みのオブジェクトタイプを型として利用する場合に使用します。後ほど別のオブジェクトタイプを追加した時に確認します。
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
}
type Query {
hello(name: String!): String
}
`;
スキーマで定義したUserタイプの型を元に実際に利用するユーザ情報のデータを作成します。
const users = [
{ id: '1', name: 'John Doe', email: 'john@test.com' },
{ id: '2', name: 'Jane Doe', email: 'jane@example.com' },
];
クライアントからクエリーを使ってユーザ情報を取得できるようにQueryタイプのフィールドにクエリーの名前と戻り値の型を設定する必要があります。戻り値については空の配列か先ほど定義したオブジェクトタイプUserが配列が入るため[User]と記述します。usersのクエリーを実行するとオブジェクトタイプのUserの型を持つ配列データが戻されることを表しています。
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
}
type Query {
hello(name: String!): String
users: [User]
}
`;
Queryタイプの設定に基づいて実際のデータを取得するための処理はリゾルバの設定で行います。追加したusersの関数では作成したユーザ情報usersのデータを戻す処理を行なっています。
const resolvers = {
Query: {
hello: (parent, args) => `Hello ${args.name}`,
users: () => users,
},
};
Apollo Studioを利用してusersクエリーを実行します。先ほどまでのhelloとは異なり左側のDocumentationにスキーマの中で定義したUserタイプのフィールド名とその型が表示されます。クエリーではUserを構成するフィールドを選択して指定することができます。”Query”ボタンをクリックするとReponseにユーザの情報が表示されます。
もしemailだけ情報が欲しい場合は下記のようにフィールドemailだけ指定することでemailのみ取得することができます。
あるidを持つユーザのみ取得したい場合にもQueryタイプとリゾルバに追加を行います。userクエリーでは引数を利用するためにargsを利用しています。
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
}
type Query {
hello(name: String!): String
users: [User]
user(id: ID!): User
}
`;
const resolvers = {
Query: {
hello: (parent, args) => `Hello ${args.name}`,
users: () => users,
user: (parent, args) => {
const user = users.find((user) => user.id === args.id);
return user;
},
},
};
設定後Apollo Studioを利用してクエリーを実行するとidに指定したユーザのみ結果に表示されます。型のIDは文字列なので渡す変数も文字列にしておく必要があります。
外部データソースからのデータ取得
ユーザ情報をindex.jsの中に直接記述しましたが外部のデータソースを利用してデータを取得する方法を確認していきます。GraphQLサーバがクライアントからのクエリーを受け取り、そのクエリーの内容を元に外部のデータリソースにアクセスを行いデータを取得して指定された情報のみクライアントに戻すという流れになります。
外部データリソースには無料で利用可能なJSONPlaceHolderを利用します。決められたURLにGETリクエストを送信するとJSONでデータを取得することができます。
データを取得するためにaxiosのライブラリを利用するためaxiosライブラリのインストールを行います。fetch関数を利用するためにnode-fetchをインストールすることもできます。
% npm install axios
ユーザ情報一覧を取得するためhttps://jsonplaceholder.typicode.com/usersにアクセスを行います。
データリソースの場所が変更になるだけなので戻り値の変更はないためスキーマのQueryタイプの更新はありません。リソルバのusers関数でのデータの取得方法を変更します。データの取得には非同期関数のasync, awaitを利用しています。
const resolvers = {
Query: {
hello: (parent, args) => `Hello ${args.name}`,
users: async () => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
},
user: (parent, args) => {
const user = users.find((user) => user.id === args.id);
return user;
},
},
};
リゾルバを更新後、Apollo Studioでクエリーを実行するとJSONPlacehoderから10名分のユーザ情報を取得することができます。
リゾルバのuser関数の変更も行います。個別のユーザの情報を取得したい場合にはURLはhttps://jsonplaceholder.typicode.com/users/{id}でidを変更することでidが一致するユーザのみ情報を取得することができるので下記のように更新することができます。
const resolvers = {
Query: {
hello: (parent, args) => `Hello ${args.name}`,
users: async () => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
return response.data;
},
user: async (parent, args) => {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${args.id}`
);
return response.data;
},
},
};;
クライアント側からはどこからデータを取得していることは意識することなくGraphQLサーバからユーザ情報を取得することができます。取得する際にGraphQLはエンドポイントはいつも同じなのでクライアント側の変更は必要ありません。
スキーマの追加(POST)
スキーマに新たにオブジェクトタイプのPOSTを追加します。
POSTはuserId, id, title, bodyの4つのフィールドを持っておりuserIdはUserのidが入ります。UserとPOSTはリレーションを持っているのでUserのmyPostsフィールドにオブジェクトタイプのPostを設定します。
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
myPosts: [Post]
}
type Post {
id: ID!
title: String!
body: String!
userId: ID!
}
type Query {
hello(name: String!): String
users: [User]
user(id: ID!): User
}
`;
Queryタイプにフィールドpostsを追加して記事一覧の情報が取得できるようにresolversにも追加したフィールドpostsとその関数を追加します。記事一覧もJSONPlaceholderから取得しURLはhttps://jsonplaceholder.typicode.com/postsです。
type Query {
hello(name: String!): String
users: [User]
user(id: ID!): User
posts: [Post]
}
//略
const resolvers = {
Query: {
//略
posts: async () => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts'
);
return response.data;
},
},
Apollo Studioからクエリーを実行すると100件分の記事一覧が表示されます。
posts一覧の取得はusersの取得と同じなので問題はないと思います。ではUserに追加したフィールドmyPostsはどのように取得するのか疑問に思う人もいるかと思います。
myPostsをUserタイプに追加したことでApollo Studio上では下記のようなクエリーを作成することができます。クエリーが作成できるのでGraphQLサーバが自動でスキーマのリレーションを解釈してpostsの情報を取得できるのではと思った人もいるかもしれませんが残念ながらそのようなことは起こらず”User”ボタンをクリックしてクエリーを実行してResponseを見るとユーザ情報は表示されますがmyPostsの値はnullです。
ではどのようにすればmyPostsに記事情報が保存されるのでしょう。答えはリゾルバにmyPostsを取得するための処理を追加することです。
更新した関数の中では最初にidを利用してユーザ情報を取得します。JSONPlaceholderにはuserのidを元に一括で記事一覧を取得するURLは提供されていないので一度すべての記事一覧を取得します。取得した記事一覧の中からfilter関数を利用してuserのidが一致する記事のみ取得します。最後にObject.assignを利用して取得したデータをマージしてそれを戻しています。
user: async (parent, args) => {
let response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${args.id}`
);
let user = response.data;
response = await axios.get('https://jsonplaceholder.typicode.com/posts');
const myPosts = response.data.filter((post) => post.userId == args.id);
user = Object.assign({}, user, {
myPosts: myPosts,
});
return user;
},
再度Apollo Studioで先ほどmyPostsがnullであったクエリーを実行すると今度はmyPostsが含まれた情報を取得することができます。
指定したユーザのidが持つ記事のタイトルのみ表示したい場合は下記のように実行することでタイトルのみ取得することができます。
このようにスキーマで定義したタイプになるようにリゾルバの関数の中で処理を記述していく必要があります。
parentの利用したリゾルバ設定
ユーザに紐づいた記事一覧の取得のクエリーをApollo StudioのOperationsを見るとuser→myPosts→titleという風に階層になっていることがわかります。
上記の階層から順番に処理が行われることになります。リゾルバの関数の引数にはparent, args, context, infoというものがあったことを思い出してください。helloクエリーではparentの値はundefinedでした。これは上記の階層を見るとhelloに対応するuserの上には親(parent)が存在しないためです。myPostsから見るとuserが親に当たりparentを利用することで親であるuser関数で実行した内容を取得することができます。
myPostsを別の関数として定義するためにリゾルバにUserを追加してその中にmyPostsの関数を追加します。Queryの中ではありません。
const resolvers = {
Query: {
//略
},
User: {
myPosts: (parent) => {
console.log(parent);
},
},
};
Apollo Studioでクエリーを実行するとusers関数で実行されて取得したデータがGraphQLサーバを起動したコンソールに表示されます。つまり親からusers関数で実行したデータを受け取ることができるのです。
その中には記事の情報も含まれているのでuserの関数を更新します。ユーザ情報のみ渡せるようにします。
user: async (parent, args) => {
let response = await axios.get(
`https://jsonplaceholder.typicode.com/users/${args.id}`
);
return response.data;
},
再度Apollo Studioでクエリーを実行するとコンソールにユーザ情報のみ表示されます。
受け取ったparentのデータを利用してそのデータに含まれるユーザidが含まれる記事のみ取得するコードに更新します。
User: {
myPosts: async (parent) => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts'
);
const myPosts = response.data.filter((post) => post.userId == parent.id);
return myPosts;
},
},
再度Apollo Studioを実行するとmyPostsを分ける前と同じ結果になりました。
myPostsを分ける前はusersの中でpostsの値を設定するためにObject.assignを利用してデータの形を調整する必要がりましたがその処理はGraphQLサーバが行ってくれるため追加処理が必要がなくなりました。またクライアントからのクエリーによってはmyPostsのフィールドが必要ない場合にはmyPostsが実行されません。userにすべての処理が含まれる場合はmyPostsの中のフィールドが必要ない場合も取得するための処理は実行されることになります。パフォーマンスとコードのクリーンさを考えるとmyPostsを分けたほうがいいことがわかります。
リゾルバの関数の引数のparentを利用して親からデータを受け取れることがわかりました。
Data sourcesの設定
axiosライブラリを利用してJSONPlaceholderからデータを取得していましたがApollo ServerはREST API用のデータソースライブラリが提供されており、利用することでデータのキャッシュ等を自動で行う機能やfetchの途中でinterceptすることでfetchのheaderに情報を追加することもできます。これは利用が必須の機能ではありませんが動作確認を行います。
REST用のデータソースを利用するためにはapollo-datasource-restパッケージのインストールが必要となります。REST API以外にデータベース用のパッケージも提供されています。
% npm install apollo-datasource-rest
リゾルバで行っていたaxiosによるデータ取得のコードをデータソースで行えるように変更します。インストールしたapollo-datasource-restパッケージからRESDATASourceを読み込みます。
const { RESTDataSource } = require('apollo-datasource-rest');
RESTDataSourceを継承したjsonPlaceAPIクラスを作成します。任意の名前をつけてください。RESTDataSourceではgetメソッドを利用することができるためaxiosは必要ありません。中身はnode-fetchです。コンストラクターにはsuperが必須でbaseURLを設定します。これはREST APIでデータを取得するサービスのbaseURLです。
getUsers, getUser, getPostsの3つの関数を追加しています。
class jsonPlaceAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://jsonplaceholder.typicode.com/';
}
async getUsers() {
const data = await this.get('/users');
return data;
}
async getUser(id) {
const data = await this.get(`/users/${id}`);
return data;
}
async getPosts() {
const data = await this.get('/posts');
return data;
}
}
作成したjsonPlaceAPIはApolloServerの引数に設定を行う必要があります。このように設定することでdataSourcesをリゾルバの中でcontextから取得できるようになります。
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => {
return {
jsonPlaceAPI: new jsonPlaceAPI(),
};
},
});
リゾルバの中でデータソースで追加した関数を利用することができますがdataSourcesについてはリゾルバの3番目の引数であるcontextからアクセスすることができます。
Query: {
hello: (parent, args) => `Hello ${args.name}`,
users: async (parent, args, { dataSources }) => {
},
user: async (parent, args, { dataSources }) => {
return dataSources.jsonPlaceAPI.getUser(args.id);
},
posts: async (parent, args, { dataSources }) => {
},
},
User: {
myPosts: async (parent, args, { dataSources }) => {
const posts = await dataSources.jsonPlaceAPI.getPosts();
const myPosts = posts.filter((post) => post.userId == parent.id);
return myPosts;
},
},
parentやargsなど利用しない場合は慣例でparentは_, argsは__と記述します。
Query: {
hello: (_, args) => `Hello ${args.name}`,
users: async (_, __, { dataSources }) => {
return dataSources.jsonPlaceAPI.getUsers();
},
user: async (_, args, { dataSources }) => {
return dataSources.jsonPlaceAPI.getUser(args.id);
},
posts: async (_, __, { dataSources }) => {
return dataSources.jsonPlaceAPI.getPosts();
},
},
User: {
myPosts: async (parent, __, { dataSources }) => {
const posts = await dataSources.jsonPlaceAPI.getPosts();
const myPosts = posts.filter((post) => post.userId == parent.id);
return myPosts;
},
},
axiosからデータソースに変更をしてもこれまで通りJSONPLaceHolderからデータを取得することはできます。データソースの設定方法を理解することができました。
データベースへの接続
JSONPlaceHolderはデータの取得はできますがデータの作成、更新、削除の処理を行うことができません。データの作成、更新、削除を行うためにデータベースを利用します。Prismaを利用してSQLiteデータベースを利用します。
Prismaとは
PrismはORM(オブジェクト-リレーショナルマッピング)でPrismを利用することで接続するデータベースの種類を意識する必要がなくなりSQLを記述しなくても共通のオブジェクトメソッドを利用してデータベースの操作を行うことができます。テーブルの作成もスキーマファイルを作成してマイグレートコマンドで行うことができます。テーブル作成時にSQLを記述することはありません。
Prismaを使ったことがない人だと少し不安になるかもしれませんが本文書であつかう範囲では難しい箇所はありません。手順通りに進めていけばデータベースを利用することができます。またTypeScriptも利用しません。
Prismaのインストール
Prismaを利用するためにはライブラリのインストールが必要になります。
% npm install prisma --save-dev
npx prism initコマンドを実行すると.envファイルとprismaフォルダが作成されその中にshema.prismaファイルが作成されます。
% npx prisma init
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
SQLiteデータベースを利用するのでprismaフォルダに空のdev.dbファイルを作成します。作成後に.envファイルの環境変数DATABASE_URLに作成したファイルのパスを設定します。
DATABASE_URL="file:./dev.db"
prismaフォルダのschema.prismaファイルにはデータベースへの接続情報とデータベースのスキーマを記述します。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
body String?
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
schema.prismaファイルを更新したらmigrateコマンドを実行してデータベースにテーブルの作成を行います。実行するとmigrationファイルにつける名前を聞かれます。空白にしても進むことができますが任意の名前をつけてください。
% npx prisma migrate dev
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
✔ Enter a name for the new migration: …
Applying migration `20220105101749_`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20220105101749_/
└─ migration.sql
Your database is now in sync with your schema.
Running generate... (Use --skip-generate to skip the generators)
added 2 packages, and audited 236 packages in 5s
19 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
✔ Generated Prisma Client (3.7.0 | library) to ./node_modules/@prisma/client in 148ms
実行が完了するとデータベースの作成は完了です。
本文書ではSQLiteへの接続にTablePlusを利用しています。無料の範囲では制限はありますがデータベースをGUI画面で操作することができます。PrismaにはPrisma Studioという機能がありこちらもGUIでデータベースの中身を確認するつようがあります。Prismaを利用する際はぜひPrisma Studioを利用してください。
データベースに直接接続するためPrismaを意識する必要はありません。接続する際には作成したdev.dbを指定する必要があります。接続が完了すると下記のようにschema.prismaに記述した定義に沿ってテーブルが作成されます。
次にPrisma Clientの作成を行うために以下のコマンドを実行します。実行すると作成したPrisma Clientのimport方法など利用方法のメッセージも表示されます。
% npx prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
✔ Generated Prisma Client (3.7.0 | library) to ./node_modules/@prisma/client in 589ms
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
```
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
```
Apollo Serverからの接続確認
Apollo Serverから接続して情報が取得できるか確認を行うためにTablePlusを利用してユーザデータの追加を行います。
index.jsファイルでPrisma Clientを読み込んでPrismaClientのインスタンスを作成します。
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
リゾルバではData Sourceを利用してREST API経由でデータを取得していましたがPrismを使ってデータベースからユーザデータを取得します。
prisma.user.findManyでSQLiteデータベースからユーザデータを一括で取得することができます。
const resolvers = {
Query: {
users: () => {
return prisma.user.findMany();
},
//略
TablePlusで登録を行なったユーザのJohn Doeの情報が取得できていることが確認できます。またidが1のためuserIdが1を持つ記事一覧も取得することができています。1つはデータベース、2つはREST APIの2つのデータソースから取得できることも確認できました。
Mutationによるユーザ作成
データベース内のデータをGraphQLサーバを利用して取得することができたので次はデータベースへの新規ユーザの作成をGraphQLサーバで行います。
これまではクエリーによるread operationsであったためスキーマはQueryタイプを利用していましたがここからは作成、更新、削除を行うのでwite operationsのMutationタイプを利用します。
スキーマにMutationタイプを追加してcreateUserを追加します。引数にはnameとemailと取ります。作成後にはUserが戻されます。フィールに名前を設定して戻り値を設定をするのでQueryタイプと同じです。
const typeDefs = gql`
//略
type Mutation {
createUser(name: String!, email: String!): User
}
`;
Mutationタイプにフィールドを追加して定義を行ったのでcreateUserで行う処理をリゾルバに記述します。
const resolvers = {
Query: {
//略
},
Mutation: {
createUser: (_, args) => {
return prisma.user.create({
data: {
name: args.name,
email: args.email,
},
});
},
},
Apollo Studioを利用してQueryではなくMutationを実行します。Operationsの中でこれまでqueryと表示されていましたがmutationになっていることを確認してください。”Mutation”ボタンを押して実行すると作成したユーザがResponseに表示されます。idが2として作成されていることがわかります。
作成したユーザはTablePlusを利用してデータベースから直接確認することができますがusersクエリーを利用してApollo Studioから確認してみましょう。usersクエリーを実行することで追加したユーザを確認することができます。
Mutationによるデータの作成を行うことができました。
Mutationによるユーザ更新
既存のユーザに対する更新をMutationを使って行います。Mutationではユーザ名の更新を行います。MutationタイプにupdateUserを追加します。戻り値のタイプはUserとします。引数にはidとnameを指定します。idを利用してユーザを取得して取得したユーザの名前を更新します。
type Mutation {
createUser(name: String!, email: String!): User
updateUser(id: Int!, name: String!): User
}
リゾルバでMutationタイプに追加したupdateUserの関数を追加します。
Mutation: {
createUser: (_, args) => {
return prisma.user.create({
data: {
name: args.name,
email: args.email,
},
});
},
updateUser:(_, args) => {
return prisma.user.update({
where: {
id: args.id,
},
data: {
name: args.name',
},
});
},
},
Apollo Studioを利用して追加したMutationのupdateUserを実行します。idを1のユーザの名前の先頭のアルファベットが小文字だったので大文字にしています。更新を行う場合は引数の設定も必要になります。
Mutationによるユーザの削除
Mutationを利用したユーザの削除を行います。MutationタイプのdeleteUserを追加します。
type Mutation {
createUser(name: String!, email: String!): User
updateUser(id: Int!, name: String!): User
deleteUser(id: Int!): User
}
リゾルバでdeleteUserの関数を追加します。
Mutation: {
createUser: (_, args) => {
return prisma.user.create({
data: {
name: args.name,
email: args.email,
},
});
},
updateUser: (_, args) => {
return prisma.user.update({
where: {
id: args.id,
},
data: {
name: 'John Doe',
},
});
},
deleteUser: (_, args) => {
return prisma.user.delete({
where: { id: args.id },
});
},
},
Apollo Studioを利用して追加したMutationのdeleteUserを実行します。引数のidには1を指定します。
削除できているか確認するためのusersクエリーを実行します。削除が行われているので1名分のユーザ情報が表示さされます。
ここまでの動作確認でMutationを利用することで作成、更新、削除が行えることができました。Queryの動作確認は完了したのでCURD(Create, Update, Read, Delete)の操作がGraphQLで行えるようになりました。
フロントエンドからの接続
これまではApollo Studioを経由してデータの取得(Query)、データの更新(Mutation)を行なっていましたがここからはフロントエンドのReactとVueからのGraphQL上のデータの操作方法について動作確認を行なっていきます。
vanilla JavaScriptの場合
GraphQLサーバからデータを取得する際に何か特別なライブラリやパッケージが必須なわけではありません。JavaScriptのfetch関数のPOSTリクエストを利用してデータの取得を行います。
任意の場所にindex.htmlファイルを作成して以下のコードを記述します。fetch関数の中にGraphQLサーバから取得するために必要となる情報を入れています。クエリーのデータを送信するためにPOSTメソッドを利用しています。headersにはContent-Typeでapplication/jsonを指定する必要があります。bodyにクエリーを記述していますが
<!DOCTYPE html>
<html lang="ja"">
<head>
<meta charset="UTF-8" />
<title>fetch関数を利用したApollo Serverからのデータ取得</title>
</head>
<body>
<div id="app"></div>
</body>
<script>
async function fetchBooks() {
const response = await fetch("http://localhost:4000", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `query{
users{
id
name
email
}
}`,
}),
});
const data = await response.json();
document.getElementById('app').innerHTML = `<h1>名前:${data.data.users[0].name}</h1>`;
}
fetchBooks();
</script>
</html>
index.htmlファイルをブラウザで開くと画面上にGraphQLサーバから取得したユーザの名前が表示されます。vanilla JavaScriptでもGraphQLサーバからデータが取得できることが確認できました。
Apollo Client
先ほどvanilla JavaScriptで動作確認をしたようにfetch関数を利用してもGraphQLサーバにアクセスすることは可能ですがApollo Clientを利用します。Apollo Clientを利用することでuseQueryなどの実装する際に便利なHooksを利用することできます。Hooks以外にはキャッシュ機能や状態管理機能を持っています。Apollo ClientはReact、VueだけではなくSvelteなどの他のフレームワークで利用することができるライブラリです。
ブラウザにChromeやFirefoxを利用している場合はApollo Client Devtoolsをインストールします。Apollo Clientを利用するとデベロッパーツールのコンソールには下記のメッセージが表示されるのでリンクをクリックするとインストール画面に移動します。
Download the Apollo DevTools for a better development experience: https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm
Apollo Client DevtoolsではQuery, Mutationの中身、キャッシュの中身を確認することができます。
Reactの場合
Reactを利用するためにプロジェクトの作成を行います。任意の名前のプロジェクトを作成してください。
% npx create-react-app react-graphql
プロジェクト作成後は作成したフォルダに移動して開発サーバを起動します。
% cd react-graphql
% npm start
ライブラリのインストール
Apollo Clientのインストールを行います。
% npm install @apollo/client graphql
インストールしたApollo Clientを利用してindex.jsファイルで動作確認を行います。ApolloClientの引数にはuriにGraphQLサーバが起動しているURLを設定します。本文書ではApollo Serverが起動しているURLを設定しています。Apollo Clientのインスタンス化ではキャッシュを指定するのでcacheには利用するInMemoryCacheインスタンスを設定しています。作成したApollo Clientのインスタンスのqueryメソッドを利用してクエリーを実行します。クエリーの内容はgqlを利用した“(バッククオート)で囲みます。
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000',
cache: new InMemoryCache(),
});
client
.query({
query: gql`
query GetUsers {
users {
id
name
email
}
}
`,
})
.then((result) => console.log(result));
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
設定後でデベロッパーツールのコンソールにGraphQLから取得したユーザデータ表示されます。
上記のデータをvanilla JavaScriptのfetch関数で取得した時のデータと比較してみます。データ以外にloadingとnetworkStatusという情報が追加されていることが確認できます。これだけを見てもfetch関数とは異なる追加機能があることがわかります。
Apollo Clientを利用することでGraphQLからデータを取得することができました。Reactアプリケーション全体でApollo Clientを利用できるようにApolloProviderを利用します。ApolloProviderで<App />をラップしpropsでclientを渡します。
import ReactDOM from 'react-dom';
import App from './App';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000',
cache: new InMemoryCache(),
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
ApolloProviderを設定後は他のコンポーネントでもApollo Clientを利用できるようになったのでApp.jsファイルを使って動作確認を行なっていきます。App.jsは以下のように記述します。
function App() {
return (
<div style={{ margin: '3em' }}>
<h1>GraphQL</h1>
</div>
);
}
export default App;
useQuery Hookの利用
ここからはGraphQLにクエリーを送信する際にuseQuery Hookを利用します。useQueryの引数には実行するクエリーを指定するためGET_USERSという名前のクエリーを設定します。クエリーの名前は慣例ですべて大文字で設定しています。
useQuery Hookは自動で実行され、実行が完了するとApollo Clientからオブジェクトが戻されます。戻されるオブジェクトの中には関数なども含まれていますが、ここではその中からdata, loading, errorを利用しています。
import { gql, useQuery } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function App() {
const { data, loading, error } = useQuery(GET_USERS);
if (loading) return '<p>ローディング中です</p>';
if (error) return '<p>エラーが発生しています。<p>';
return (
<div style={{ margin: '3em' }}>
<h1>GraphQL</h1>
{data.users.map((user) => (
<div key={user.id}>Name: {user.name}</div>
))}
</div>
);
}
useQuery Hookを利用することでGraphQLサーバからデータを取得してブラウザ上に表示することができました。
Vueの場合
Vueを利用するためにVueのプロジェクトを作成します。Vue CLIを利用することもできますがViteを利用してプロジェクトの作成を行います。ViteではVueだけではなくReactもプロジェクトを作成することができます。
% npm init vite@latest vue-graphql -- --template vue
プロジェクト作成後は作成したフォルダに移動してnpm installコマンドでJacaScriptライブラリのインストールを行い開発サーバを起動します。
% cd vue-graphql
% npm install
% npm run dev
Vueのバージョンは3でComposition APIを利用します。
ライブラリのインストール
Apollo Clientのインストールを行います。VueでApollo Clientを利用してクエリーを実行するためには3つのライブラリをインストールする必要があります。
% npm install graphql graphql-tag @apollo/client
graphql-tagからgql関数をimportします。
インストールしたApollo Clientを利用してmain.jsファイルで動作確認を行います。ApolloClientの引数にはlinkとcacheを設定します。linkの値はcreateHttpLink関数にGraphQLサーバが起動しているuriを指定して作成します。本文書ではApollo Serverが起動しているURLを設定しています。Apollo Clientのインスタンス化ではキャッシュを指定するのでcacheには利用するInMemoryCacheインスタンスを設定しています。作成したApollo Clientのインスタンスのqueryメソッドを利用してクエリーを実行します。クエリーの内容はgqlを利用した“(バッククオート)で囲みます。
import { createApp } from 'vue';
import App from './App.vue';
import {
ApolloClient,
createHttpLink,
InMemoryCache,
} from '@apollo/client/core';
import gql from 'graphql-tag';
const httpLink = createHttpLink({
uri: 'http://localhost:4000/',
});
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
link: httpLink,
cache,
});
apolloClient
.query({
query: gql`
query GetUsers {
users {
id
name
email
}
}
`,
})
.then((result) => console.log(result));
createApp(App).mount('#app');
設定後でデベロッパーツールのコンソールにGraphQLから取得したユーザデータ表示されます。
上記のデータをvanilla JavaScriptのfetch関数で取得した時のデータと比較してみます。データ以外にloadingとnetworkStatusという情報が追加されていることが確認できます。これだけを見てもfetch関数とは異なる追加機能があることがわかります。
Apollo Clientを利用することでGraphQLからデータを取得することができました。Vueアプリケーション全体でApollo Clientを利用できるようにmain.jsで設定を行います。
コンポーネント内でuseQueryなどを利用することができるように@vue/apollo-composableをインストールします。
% npm install --save @vue/apollo-composable
@vue/apollo-composableのインストール後にmain.jsファイルを以下のように更新します。
import { createApp, provide, h } from 'vue';
import { DefaultApolloClient } from '@vue/apollo-composable';
import App from './App.vue';
import {
ApolloClient,
createHttpLink,
InMemoryCache,
} from '@apollo/client/core';
const httpLink = createHttpLink({
uri: 'http://localhost:4000/',
});
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
link: httpLink,
cache,
});
const app = createApp({
setup() {
provide(DefaultApolloClient, apolloClient);
},
render: () => h(App),
});
app.mount('#app');
main.jsを設定後は他のコンポーネントでもApollo Clientを利用できるようになったのでApp.vueファイルを使って動作確認を行なっていきます。App.vueは以下のように記述します。
<script setup></script>
<template>
<h1>GraphQL</h1>
</template>
useQueryの利用
ここからはGraphQLにクエリーを送信する際にuseQueryを利用します。useQueryの引数には実行するクエリーを指定するためGET_USERSという名前のクエリーを設定します。クエリーの名前は慣例ですべて大文字で設定しています。
useQuery Hookは自動で実行され、実行が完了するとApollo Clientからオブジェクトが戻されます。戻されるオブジェクトの中には関数なども含まれていますが、ここではその中からdata, loading, errorを利用しています。
<script setup>
import { useQuery } from '@vue/apollo-composable';
import gql from 'graphql-tag';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
const { result, loading, error } = useQuery(GET_USERS);
</script>
<template>
<h1>GraphQL</h1>
<div v-if="loading"><p>ローディング中です</p></div>
<div v-else-if="error">
<p>エラーが発生しています。</p>
<p></p>
</div>
<div v-else>
<div v-for="user in result.users" :key="user.id">Name: {{ user.name }}</div>
</div>
</template>
useQuery Composableを利用することでGraphQLサーバからデータを取得してブラウザ上に表示することができました。
今後も更新していきます。