Node.js+MongoDB構成でGraphQLのお勉強

#はじめに
現場でGraphQLを使用しているのですが、保守改修段階でプロジェクトに入ったのでスキーマ作成などの基本的なところをやったことがありませんでした。また、NoSQLデータベースも使ったことがなかったので、まとめて学んでみようということでNode.js+MongoDBの構成でGraphQLサーバをたててみることにしました。

今回作成したプロジェクトは<a href="https://github.com/suzuki0430/graphql-server-practice">GitHub</a>にあります。

#GraphQLのよさ
RESTとの比較記事がいたるところにあるので(例えば<a href="https://www.webprofessional.jp/rest-2-0-graphql/">こちら</a>)詳しく書きませんが、単一のエンドポイントであるというのがGraphQLの一番の特徴です。

GraphQLをつかうことで、RESTで必要なすべてのデータを取得しようとするときに発生する以下のような問題を解決することができます。

- 複数のエンドポイントへリクエストを行う必要がある
- 不要なデータも一緒に取得されてしまう

#サーバ構築
適当なフォルダ(```graphql-server-practice```)を作成して```yarn init```します。そのあと、以下の構成でフォルダおよびファイルを作成します。

![スクリーンショット 2021-02-24 20.50.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/918bf7f8-e161-86e9-602e-786f46d2a11e.png)

Schema, Query, Mutationを```Schema.js```に記載していきます。

##Schemaの作成
今回は練習のため、User, Hobby, Postの3つのSchemaをつくります。
Userはそれぞれ複数のHobbyやPostをもてるような関係になっています(One to Many relationship)。

まずは```yarn add graphql```でgraphqlパッケージを導入し、```schema.js```で以下を読み込みます。

```schema.js
const graphql = require('graphql');

const {
  GraphQLObjectType,
  GraphQLID,
  GraphQLString,
  GraphQLInt,
  GraphQLSchema,
  GraphQLNonNull,
  GraphQLList,
} = graphql;
```

UserのSchemaをUserTypeとして作成します。GraphQLObjectTypeでラッピングすることでSchemaのname, description, fieldsを定義することができます。

fieldsのid, name, age, professionをGraphQLID, GraphQLString, GraphQLInt, GraphQLStringというスカラー型で定義します。

```schema.js
const UserType = new GraphQLObjectType({
  name: 'User',
  description: 'Documentation for user...',
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: GraphQLString },
    age: { type: GraphQLInt },
    profession: { type: GraphQLString },
  }),
});
```

HobbyTypeとPostTypeについても同様に作成します。

```schema.js
const HobbyType = new GraphQLObjectType({
  name: 'Hobby',
  description: 'Hobby description',
  fields: () => ({
    id: { type: GraphQLID },
    title: { type: GraphQLString },
    description: { type: GraphQLString },
  }),
});

const PostType = new GraphQLObjectType({
  name: 'Post',
  description: 'Post description',
  fields: () => ({
    id: { type: GraphQLID },
    comment: { type: GraphQLString },
  }),
});
```
3つのSchemaを作成しましたが、この状態ではまだUserとPost、UserとHobbyの関係が定義されていないので、これらのリレーションを考えてあげる必要があります。

UserとPostの例を考えてみます。

```javascript
const UserType = new GraphQLObjectType({
  name: 'User',
  description: 'Documentation for user...',
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: GraphQLString },
    age: { type: GraphQLInt },
    profession: { type: GraphQLString },
    posts: {
      type: new GraphQLList(PostType),
      resolve(parent, args) {
        return postsData.filter((data) => data.userId === parent.id);
      },
    },
  }),
});

const PostType = new GraphQLObjectType({
  name: 'Post',
  description: 'Post description',
  fields: () => ({
    id: { type: GraphQLID },
    comment: { type: GraphQLString },
    user: {
      type: UserType,
      resolve(parent, args) {
        return usersData.find((data) => data.id === parent.userId);
      },
    },
  }),
});
```
User1つに対してPostは複数存在します。そのため、UserTypeのfieldsに新しく作られたpostsはPostTypeの配列型となり、```new GraphQLList(PostType)```と定義されます。

```javascript
    posts: {
      type: new GraphQLList(PostType),
      resolve(parent, args) {
        return postsData.filter((data) => data.userId === parent.id);
      },
    },
```
また、resolveはどのUserに対するPostを表示するのかを定義するものです。parentは親のfields(ここではUserType)を指しており、以上の処理ではUserのidと等しいuserIdをもったPostのデータのみを取得するようになっています。

```javascript
const usersData = [
  { id: '1', name: '山田勝己', age: 36, profession: 'SASUKE' },
];

const postsData = [
  { id: '1', comment: '僕にはSASUKEしかないんです', userId: '1' },
  { id: '2', comment: '完全制覇がしたいんです', userId: '1' },
];
```

##Queryの作成
QueryはSchemaと同様に、GraphQLObjectTypeで定義を行います。試しに指定したidのUserを取得するuserクエリとすべてのUserを取得するusersクエリを作成してみます。

```javascript
const RootQuery = new GraphQLObjectType({
  name: 'RootQueryType',
  description: 'Description',
  fields: {
    user: {
      type: UserType,
      args: { id: { type: GraphQLID } },

      resolve(parent, args) {
        return usersData.find((data) => data.id === args.id);
      },
    },

    users: {
      type: new GraphQLList(UserType),
      resolve(parent, args) {
        return usersData;
      },
    },
  },
});
```

userクエリではidを引数(args)としてとるので、fieldsでargsの型定義を行っています。resolveではargsのidと同じデータだけ取得するような処理を書いています。

一方、usersクエリでは、すべてのデータを取得するだけなのでargsは必要ありません。

##ローカルサーバをたててQueryの動作確認
Mutation作成とDB接続の前に、ローカルサーバをたててQueryの挙動を確認します。

作成したRootQueryをnew GraphQLSchemaでラッピングしてエクスポートします。

```schema.js
module.exports = new GraphQLSchema({
  query: RootQuery
});
```

```yarn add express express-graphql```で必要なパッケージを導入し、以下の設定を行います。

```app.js
const express = require('express');
const { graphqlHTTP } = require('express-graphql');

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

const app = express();

app.use(
  '/graphql',
  graphqlHTTP({
    graphiql: true,
    schema,
  })
);

app.listen(4000, () => {
  console.log('Listening for requests on my awesome port 4000');
});
```

```node app```で4000ポートにサーバが立ち上がるのですが、ソースコードの修正をリアルタイムで反映させるために```yarn global add nodemon```で```nodemon```を導入します。

```nodemon app```でサーバを立ち上げ、```http://localhost:4000/graphql```　を開くと以下の画面が現れます。

![スクリーンショット 2021-02-25 8.04.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/7ecb7e0d-fd0a-55ee-fbec-041eccce06ef.png)

userクエリを試しに実行すると右側に取得データが表示されます。postsのデータも問題なく表示されています。
![スクリーンショット 2021-02-25 8.10.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/3ff4426f-1772-e522-ef4d-d98f614f9bdd.png)

#MongoDBとの接続
DBと接続してMutationを実装します。

##MongoDB Atlasの設定
MongoDB Atlasのアカウントを作成します。Googleアカウントがあれば大丈夫です。
![スクリーンショット 2021-02-25 21.26.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e2136362-d07e-ab1c-1b7e-9a3c47ed8a53.png)

適当な名前でProjectsを新規で作成します。
![スクリーンショット 2021-02-25 21.29.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e16efa15-41c9-4fdf-8a1a-dd08d621b22c.png)

Projects内でClusterを作成します。今回、クラウドにはAWSを使用し、DB性能に関わるCluster Tierには無料のM0 Sandboxを使用します。
![スクリーンショット 2021-02-25 21.31.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/1063bda7-5b83-3e5d-4950-6240520d9376.png)

作成したClusterのCONNECTボタンを押して、"Connect using MongoDB Compass"を選択します。CompassはMongoDB用のGUIツールです。
![スクリーンショット 2021-02-25 21.41.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/13ed2aec-afb6-e011-0a44-021dd3500bb2.png)

Compassをダウンロードし、DB接続用のコードをコピーします。
![スクリーンショット 2021-02-25 21.43.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/f2947427-dd2c-a863-0028-6354aa65bc8b.png)

##Node.jsの設定
GraphQLとMongoDBと連携するために、Node.jsのmongooseというパッケージを使用します(```yarn add mongoose```)。
app.jsファイルを以下のようにします。```mongoose.connect```でMongoDBとの接続、```mongoose.connection.once```で接続が成功したことを確認するためのコンソールログを行っています。

```app.js
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const mongoose = require('mongoose');
const schema = require('./schema/schema');

const app = express();

mongoose.connect(
  'mongodb+srv://dbUser:<password>@cluster0.gjo5x.mongodb.net/test', // <password>には自分で設定したものを入力
  { useNewUrlParser: true }
);
mongoose.connection.once('open', () => {
  console.log('we are connected.');
});

app.use(
  '/graphql',
  graphqlHTTP({
    graphiql: true,
    schema,
  })
);

app.listen(4000, () => {
  console.log('Listening for requests on my awesome port 4000');
});
```

##Modelの作成
DBのSchemaにあたるModelを作成していきます。GraphQLのSchemaとModelを関連付けることで、DBからデータを取得(Query)したり、登録・削除(Mutation)などを行うことができます。
modelフォルダ以下に新しいファイルを作成します。
![スクリーンショット 2021-02-25 22.03.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e0134580-82e8-769c-3cad-214c4529bb81.png)

UserのModelは以下のようになります。userSchemaは後ほどschema.jsで読み込むので、最後にエクスポートします。

```user.js
const mongoose = require('mongoose');
const MSchema = mongoose.Schema;

const userSchema = new MSchema({
  name: String,
  age: Number,
  profession: String,
});
module.exports = mongoose.model('User', userSchema);
```
##Mutationの作成
Userデータの作成(CreateUser)、更新(UpdateUser)、削除(RemoveUser)のMutationsを作成します。

```schema.js
const graphql = require('graphql');

const User = require('../model/user');

~中略~

const Mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    CreateUser: {
      type: UserType,
      args: {
        name: { type: new GraphQLNonNull(GraphQLString) },
        age: { type: new GraphQLNonNull(GraphQLInt) },
        profession: { type: GraphQLString },
      },

      resolve(parent, args) {
        let user = new User({
          name: args.name,
          age: args.age,
          profession: args.profession,
        });

        return user.save();
      },
    },

    UpdateUser: {
      type: UserType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLString) },
        name: { type: new GraphQLNonNull(GraphQLString) },
        age: { type: GraphQLInt },
        profession: { type: GraphQLString },
      },

      resolve(parent, args) {
        return (updatedUser = User.findByIdAndUpdate(
          args.id,
          {
            $set: {
              name: args.name,
              age: args.age,
              profession: args.profession,
            },
          },
          { new: true }
        ));
      },
    },

    RemoveUser: {
      type: UserType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLString) },
      },
      resolve(parent, args) {
        let removedUser = User.findByIdAndRemove(args.id).exec();

        if (!removedUser) {
          throw new 'Error'();
        }

        return removedUser;
      },
    },
}

~中略~

module.exports = new GraphQLSchema({
  query: RootQuery,
  mutation: Mutation,
});

```
resolve内でUserのModelを読み込み、データ登録用のメソッド(save)や更新用のメソッド(findByIdAndUpdate)を使用します。これらのメソッドについては、mongooseの<a href="https://mongoosejs.com/docs/api/model.html">公式Docs</a>に使い方の詳細な説明が記載されています。

また、更新や削除では処理を行うデータを指定するため、argsのidはNon-nullとなります。Non-nullにしたいカラムについては、GraphQLNonNullをラッピングします。

最後にMutationのエクスポートも忘れずに行います。

##Mutationの実行

```localhost:4000/graphql``` を開き、CreateUserを試しに実行してみます。
![スクリーンショット 2021-02-25 22.47.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/e346741b-fd64-6273-80c4-f75ce281227c.png)

MongoDB Compassでデータが登録されたことを確認することができました。
![スクリーンショット 2021-02-25 22.50.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/063c4b0c-5b39-35e8-72b6-4cce74440b2e.png)

#おわりに
今回のサーバ構築作業には結構時間がかかったのですが、AWS AppSyncを使ったら一瞬で構築できました。なかなか衝撃的な体験だったので、別記事で書こうと思います。
また、PostやHobbyのModelやMutationなど、記載を省略した部分についてご興味がありましたら、GitHubをご確認いただければと思います。