Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8691

Node.jsを用いて、ORMにSequelize, DBはRDS(MySQL)という構成でApollo Serverを用いて、GraphQL Serverを構成する【勉強メモ】

$
0
0

最初に

これは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に対する文字コード関連の設定を記述する

また、上記 migrationmodelsには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-servergraphqlに依存するため、こちらも併せてインストールしています。
また 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

Model | Sequelize(findByPk)


Viewing all articles
Browse latest Browse all 8691

Trending Articles