LoginSignup
6
8

More than 3 years have passed since last update.

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

Posted at

最初に

これは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.js
  User.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側とのつなぎ込みを行っているようでした。

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: { models }
})

ちなみにGraphQLには Query, Mutations, Subscriptions がありますが、このチュートリアルではQuery, Mutationsに焦点が当てられています。

schemaを作成していく(GraphQL)

次に src/schema.js を作成していきます。
記述を見ると、設定したassociateを反映させた構成になっているのが分かります。

  type User {
    id: Int!
    name: String!
    email: String!
    recipes: [Recipe!]!
  }

  type Recipe {
    id: Int!
    title: String!
    ingredients: String!
    direction: String!
    user: User!
  }

queryは3つ設定されているようです。

  • user IDを引数にしてユーザ情報を取得するもの
  • すべてのレシピを取得するもの
  • recipe IDを引数にしてレシピ情報を取得するもの
  type Query {
    user(id: Int!): User
    allRecipes: [Recipe!]!
    recipe(id: Int!): Recipe
  }

mutation は2つ設定されています。

  • name, email, password を引数にしてユーザを作成するもの
  • userId, title, ingredients, direction を引数にしてレシピを作成するもの
  type Mutation {
    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の場合ならば、下記のように実装されています。

async createUser(root, { name, email, password }, { models }) {
  return models.User.create({
    name,
    email,
    password: await bcrypt.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 を指定すると渡せるようです。

const models = require('../models');

const server = new ApolloServer({
  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: {
    async recipes(user) {
      return user.getRecipes();
    },
  },
  Recipe: {
    async user(recipe) {
      return recipe.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"    
  ) {
    id
    name
    email
  }
}

すると下記のような反応が返ってきます。

{
  "data": {
    "createUser": {
      "id": 1,
      "name": "テストユーザ1",
      "email": "text@example.com"
    }
  }
}

サーバのログを見ると、SQLが発行されているのも確認できます。

Executing (default): INSERT INTO `Users` (`id`,`name`,`email`,`password`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?,?);

次にレシピを作成します。
先ほど作成したテストユーザ1に紐づくレシピを作成します。

mutation {
  createRecipe(
    userId: 1
    title: "サンプルレシピ1"
    ingredients: "Salt, Pepper"
    direction: "Add salt, Add pepper"
  ) {
    id
    title
    ingredients
    direction
    user {
      id
      name
      email
    }
  }
}

下記のようなレスポンスが返ります。

{
  "data": {
    "createRecipe": {
      "id": 1,
      "title": "サンプルレシピ1",
      "ingredients": "Salt, Pepper",
      "direction": "Add salt, Add pepper",
      "user": {
        "id": 1,
        "name": "テストユーザ1",
        "email": "text@example.com"
      }
    }
  }
}

サーバのログには下記のようなSQLが発行されているのが確認できます。

INSERT INTO `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 {
    id
    title
    user {
      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)

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8