最初に
これはNode.js環境で, ORMにはSequelize, DBにはMySQLを使った構成で、Apollo Serverを用いてGraphQLサーバを構築してみた際の備忘録となります。
と言っても自身でイチから、これらの構成を構築していったわけではなく、下記のチュートリアルを参照しながらの勉強メモとなります。
(ちょうど同じ構成での例を探していたところ、丁寧に書かれていた下記のドキュメントを見つけました。ありがたい)
How To Set Up a GraphQL Server in Node.js with Apollo Server and Sequelize
ちなみにこのドキュメントの中では、sqlite3を用いているので、そこはこちらでMySQLに置き換えて実践しています。
また細かなところで適宜アレンジを施しています。
大枠自体は変わらないので、この構成(Node.js
, Sequelize
などを Apollo Server
と組み合わせる)に興味ある方は、直接ドキュメントを読まれることをおすすめします。
(というかこのポストを読み進めていく場合は、上の DigitalOcean
のチュートリアル記事とセットで読んでいくことをおすすめします)
自身で実際に実装したコードはGithubに置いています。
shinshin86/graphql-recipe-server
Sequelizeを用いたDB関連のセットアップ
まずはDB関連のセットアップから行っていきます。
(ここらへん Apollo Server
というよりは Sequelize
の基本的なセットアップの流れになります)
インストール&初期化
必要なライブラリをインストール
yarn add sequelize sequelize-cli mysql2
Sequelize関連の初期化処理を実施
yarn sequelize init
テーブルの作成
次に必要なmodelとmigrationを作成・実施していきます。
まずはUserテーブルから作成
yarn sequelize model:create --name User --attributes name:string,email:string,password:string
作成した項目は空の入力を許可しないようにするなどの設定を行います。
※ここについては参照先の記述( Step 2 — Creating Models and Migrations
)を参照してください。
次にRecipeテーブル
yarn sequelize model:create --name Recipe --attributes title:string,ingredients:text,direction:text
こちらも同じ用にmodelとmigrationの内容を編集していきます。
また、ここで userId
の追加も実施しています。
※ここについても参照先の記述( Step 2 — Creating Models and Migrations
)を参照してください。
userId:{allowNull:false,type:Sequelize.INTEGER.UNSIGNED,},
ここにはレシピを作成したユーザIDが格納され、後々レシピを作成したユーザ情報を取得するために使われます。
associateの設定
modelも編集したら、UserとRecipeでそれぞれassociateの設定を行っていきます。
// models/user.jsUser.associate=function(models){User.hasMany(models.Recipe)};
// models/recipe.jsから抜粋Recipe.associate=function(models){Recipe.belongsTo(models.User,{foreignKey:'userId'})};
DBに対する文字コード関連の設定を記述する
また、上記 migration
と models
にはDBに対する文字コードの設定も忘れないように記述します。
これを忘れると、日本語で入力した場合に文字コード関連でエラーになります。
(実はすっかり忘れていて、日本語を使ってエラーになったりしていました)
migration
ファイルの場合、 queryInterface.createTable
の第3引数に下記の内容をセットします。
{charset:'utf8',collate:'utf8_general_ci',}
また model
ファイルの場合は、sequelize.define
の第3引数に下記の記述をセットします。
{charset:'utf8',collate:'utf8_general_ci',}
migrationの実施
すでにローカルにMySQLは立ち上がっているものとします。
Sequelizeはセットアップしたデフォルトの状態だと password: null
でアクセスするようになっているかと思いますが、流石にそれは現実味がない気がしたので、一応形だけですが、
root
というユーザ名、password
というパスワード
で接続するように config/config.json
に記述しました。
なので、DB自体もそのような設定で動かしています。
(これも現実味ないといえばないですが...)
ちなみに自身はローカルで動くDocker上に、最新のMySQLを立ち上げて、そちらを使っていきます。
下記のコマンドでDBの作成・migrationを実施していきます
yarn sequelize db:create
yarn sequelize db:migrate
GraphQL Serverを作成する
ここからが本番です。
必要なライブラリをインストールしていきます。
apollo-server
は graphql
に依存するため、こちらも併せてインストールしています。
また bcryptjs
はユーザのパスワードをハッシュ化するために使用します。
yarn add apollo-server graphql bcryptjs
次に src
ディレクトリを作成し、必要なファイルを作成していきます。
mkdir src
nv src/index.js
ソース自体は参照元の Step 3 — Creating the GraphQL Server
を参照してもらうとして、下記の context: { models }
でmodels側とのつなぎ込みを行っているようでした。
constserver=newApolloServer({typeDefs,resolvers,context:{models}})
ちなみにGraphQLには Query
, Mutations
, Subscriptions
がありますが、このチュートリアルではQuery
, Mutations
に焦点が当てられています。
schemaを作成していく(GraphQL)
次に src/schema.js
を作成していきます。
記述を見ると、設定したassociateを反映させた構成になっているのが分かります。
typeUser{id:Int!name:String!email:String!recipes:[Recipe!]!}typeRecipe{id:Int!title:String!ingredients:String!direction:String!user:User!}
queryは3つ設定されているようです。
- user IDを引数にしてユーザ情報を取得するもの
- すべてのレシピを取得するもの
- recipe IDを引数にしてレシピ情報を取得するもの
typeQuery{user(id:Int!):UserallRecipes:[Recipe!]!recipe(id:Int!):Recipe}
mutation
は2つ設定されています。
name, email, password
を引数にしてユーザを作成するものuserId, title, ingredients, direction
を引数にしてレシピを作成するもの
typeMutation{createUser(name:String!,email:String!,password:String!):User!createRecipe(userId:Int!title:String!ingredients:String!direction:String!):Recipe!}
resolverを設定していく(GraphQL)
上に書いたqueryに対応する実際の処理がresolverには書かれています。
実際にどういうロジックが動くのかはこちらを見れば、大体イメージがつくかと思います。resolver
内に実際に書かれるロジックは、特にGraphQL
的なものというのはそれほどなく、実際の取得ロジックなどが書かれる形となるので(今回で言えば、Sequelizeを用いたデータ作成・取得処理など)、結構すぐに馴染める印象でした。
例えば MutationのcreateUser
の場合ならば、下記のように実装されています。
asynccreateUser(root,{name,email,password},{models}){returnmodels.User.create({name,email,password:awaitbcrypt.hash(password,10),});},
Sequelize v5ではfindByIdではなくfindByPkとなる(余談)
ちなみに現時点で sequelize
の最新のversionをインストールした場合、sequelizeのv5系
がインストールされるかと思います。v5
ではsequelizeに実装されていた findById
は廃止され findByPk
に移行しています。
参照しているドキュメントでは findById
で書かれているので、 ここは findByPk
に書き換える必要があります。
(sequelize v4
系をインストールした場合は書き換える必要はありません)
diff --git a/src/resolvers.js b/src/resolvers.js
index 7372e74..0cacc9b 100644
--- a/src/resolvers.js
+++ b/src/resolvers.js
@@ -3,13 +3,13 @@ const bcrypt = require('bcryptjs');
const resolvers = {
Query: {
async user(root, { id }, { models }) {
- return models.User.findById(id);
+ return models.User.findByPk(id);
},
async allRecipes(root, args, { models }) {
return models.Recipe.findAll();
},
async recipe(root, { id }, { models }) {
- return models.Recipe.findById(id);
+ return models.Recipe.findByPk(id);
},
},
Mutation: {
ApolloServerのcontextについて
第3引数として models
が渡っていますが、これは src/index
でcontextに models
を指定すると渡せるようです。
constmodels=require('../models');constserver=newApolloServer({typeDefs,resolvers,context:{models},});
例えば下記のようなコードを書いて、該当の箇所のコードを動かすと、 { hoge: 'hogehoge' }
がログとして出力されます。
diff --git a/src/index.js b/src/index.js
index e1049b7..133f156 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,7 +6,7 @@ const models = require('../models');
const server = new ApolloServer({
typeDefs,
resolvers,
- context: { models },
+ context: { models, hoge: 'hogehoge' },
});
server
diff --git a/src/resolvers.js b/src/resolvers.js
index 0cacc9b..3bc1d24 100644
--- a/src/resolvers.js
+++ b/src/resolvers.js
@@ -8,7 +8,8 @@ const resolvers = {
async allRecipes(root, args, { models }) {
return models.Recipe.findAll();
},
- async recipe(root, { id }, { models }) {
+ async recipe(root, { id }, { models, hoge }) {
+ console.log({hoge})
return models.Recipe.findByPk(id);
},
},
他に書き残しとくべき箇所というのも、あまりないのですが、
下記の user.getRecipes()
, recipe.getUser()
などは Sequelize
側での処理になります。associate
で設定しているゆえに、こういう形で取得ができます。
User:{asyncrecipes(user){returnuser.getRecipes();},},Recipe:{asyncuser(recipe){returnrecipe.getUser();},},
これで、動かすための必要な実装はすべて完了です。
Apollo Playgroundで実際に試してみる
下記コマンドでsevrerを起動します。
node src/index.js
# もしくは "yarn start"
http://localhost:4000/
にアクセスすると、親しみやすいApolloのPlaygroundが表示されます。
とりあえずuser
を作成して見ようと思います。
mutation{createUser(name:"テストユーザ1"email:"text@example.com"password:"password"){idnameemail}}
すると下記のような反応が返ってきます。
{"data":{"createUser":{"id":1,"name":"テストユーザ1","email":"text@example.com"}}}
サーバのログを見ると、SQLが発行されているのも確認できます。
Executing(default):INSERTINTO`Users`(`id`,`name`,`email`,`password`,`createdAt`,`updatedAt`)VALUES(DEFAULT,?,?,?,?,?);
次にレシピを作成します。
先ほど作成したテストユーザ1に紐づくレシピを作成します。
mutation{createRecipe(userId:1title:"サンプルレシピ1"ingredients:"Salt, Pepper"direction:"Add salt, Add pepper"){idtitleingredientsdirectionuser{idnameemail}}}
下記のようなレスポンスが返ります。
{"data":{"createRecipe":{"id":1,"title":"サンプルレシピ1","ingredients":"Salt,Pepper","direction":"Addsalt,Addpepper","user":{"id":1,"name":"テストユーザ1","email":"text@example.com"}}}}
サーバのログには下記のようなSQLが発行されているのが確認できます。
INSERTINTO`Recipes`(`id`,`title`,`ingredients`,`direction`,`createdAt`,`updatedAt`,`userId`)VALUES(DEFAULT,?,?,?,?,?,?);
また、同時レスポンス時に必要となるSQLが発行されているのも分かります。
SELECT`id`,`name`,`email`,`password`,`createdAt`,`updatedAt`FROM`Users`AS`User`WHERE`User`.`id`=1;
User作成時は SELECT
は発行されていませんでしたが、今回は紐づくユーザ情報も返す必要があるため、SELECT
クエリを発行する必要があったということかと想像します
(ソースはまだ読んでいません)
queryについてはあまりここに書かなくても、結構情報はある気がしたので、ざっくりと。
query{allRecipes{idtitleuser{name}}}
ちなみに上のようなqueryを発行した場合、SQL的には下記のように返ってくるようです。
SELECT`id`,`title`,`ingredients`,`direction`,`createdAt`,`updatedAt`,`userId`,`UserId`FROM`Recipes`AS`Recipe`;SELECT`id`,`name`,`email`,`password`,`createdAt`,`updatedAt`FROM`Users`AS`User`WHERE`User`.`id`=1;
テーブルjoinして取得するような動きではありませんが、別にそういうオプションがあるのか、実装的にそういう形になっているのかは今後調べていくこととします。
結果は下記の通り。
{"data":{"allRecipes":[{"id":1,"title":"サンプルレシピ1","user":{"name":"テストユーザ1"}}]}}
ざっくりとではありますが、以上勉強メモとなります。
参照
How To Set Up a GraphQL Server in Node.js with Apollo Server and Sequelize