1
Help us understand the problem. What are the problem?

posted at

updated at

【サーバサイド編】GraphQLを使うぞ

こんにちは!今回はGraphQLを使ってTodoリストの作成をしたいと思います。

※この記事はサーバサイドについての説明となります。
クライアントサイドの記事はこちら

目次

1.今回作るもの
2.使用技術
3.必要なインストール
4.今回登場する主なファイル
5.早速書いていこう
・app.jsを書く
・schema.jsを書く
・MongoDBと接続する
・modelsフォルダ内のファイルを書く
6.メソッドの書き方
7.データの取得
8.データの追加、編集、削除
・データの追加
・データの編集
・データの削除
9.上手くいっているかテストしてみる
10.まとめ

超ざっくりGraphQLとは:
・DBから情報を取得するのに使えるやつ。
・APIのURL(エンドポイント)が1つで済む!
・クライアントサイドでDBから自分で持ってきたいデータを選べる!
 →サーバサイドで用途に合わせて1つ1つAPIを作成する必要なし。最低限で済む

では早速始めましょう。

1. 今回作るもの

Todoアプリを作成します。Todo1つ1つにはカテゴリを持たせて分類出来るようにします。

2. 使用技術

・Windows(10)
・言語:javascript
・DB:MongoDB
・Node.js
・Express
・GraphQL(Nodeの中で使うイメージ)

3. 必要なインストール

必要な方は下記のインストールをお願いします。
空のフォルダを作成、その中にコマンドプロンプトにて移動→npm installしまくる

ターミナルorコマンドプロンプト
//packagejsonの作成(全部エンターでOK)
npm init

//expressのインストール
npm install express

//変更したらnode.jsを自動で再起動させるため
npm install nodemon

//express-graphqlのインストール
npm install express-graphql

//graphqlのインストール
npm install graphql

//クライアントでデータを取得した際にCorsエラーを阻止する
npm install cors

//MongoDBと接続のため繋ぐためにmongooseのインストール
npm install mongoose

※node.jsをインストールしていない場合は追加でインストールして下さい。

4. 今回登場する主なファイル

ファイル名 説明
app.js サーバに繋ぐ設定用ファイル
schema > schema.js GraphQLの設定用ファイル
models > todo.js 型を設定するファイル(Todo用)
models > category.js 型を設定するファイル(カテゴリ用)
.env MongoDBへのアクセスURL     

全体像

image.png

5. 早速書いていこう

■app.jsを書く

ここにはサーバに繋ぐ設定を記載します。

app.js
//インポートのコーナー===================================================
//exporess
const express = require("express");
const app = express();

//Corsエラー対策用
const cors = require("cors");
app.use(cors());

//サーバの設定============================================================
//ポート番号,起動した際に発動するメソッド
app.listen(4000, () => {
  console.log("express起動したよ");
});

この状態でnodemonを通してnode.js(express)を起動させてみます

コマンドプロンプトorターミナル
//appはファイル名
nodemon app

下記のように表示されれば成功!
image.png

■schema.jsを書く

schema.js
//インポートのコーナー===================================================
//GraphQL
const graphql = require("graphql");
//GraphQL独自の型のインポート(後で追加あり)
const { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLBoolean,
GraphQLList, GraphQLSchema } = graphql;

GraphQLで使用する型の設定をします。

schema.js
/**
 * Todo型.
 */
const TodoType = new GraphQLObjectType({
//型の名前
  name: "Todo",
  fields: () => ({
//使用する変数とその型(ここで使用する型名は上でインポートする必要あり)
    id: { type: GraphQLID },
    title: { type: GraphQLString },
    //Todoが完了したかどうか
    finish: { type: GraphQLBoolean },
    //※下記で説明
    category: {
      //下で設定するカテゴリタイプ
      type: CategoryType,
      resolve(parent, args) {
        return Category.findById(parent.categoryId);
      },
    },
  }),
});
※下記で説明部分

情報のネストは少しややこしい!
・検索ワード→args
・大元の検索結果(Todo情報)→parentに入る

image.png
上図のようにまずはTodo情報を検索して、そこに入っているカテゴリIDと同じIDのカテゴリ情報をカテゴリリストから持ってくるイメージ。

schema.js
/**
 * Category型.
 */
const CategoryType = new GraphQLObjectType({
//型の名前
  name: "Category",
//使う変数とその型
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: GraphQLString },
   //下記で説明
    todoList: {
      //上で設定したTodoタイプのリスト
      type: new GraphQLList(TodoType),
      resolve(parent, args) {
        return Todo.find({ categoryId: parent.id });
      },
    },
  }),
});

※Todoのネストと同じ原理だが、1つのカテゴリに対して紐づくTodoは複数あるのでGraphQLListとして「Todoのリスト」を作成してあげる必要がある。
(例)カテゴリ「読書」の中に「本1」「本2」「本3」というTodoが入っている

■MongoDBと接続する

.env
//URLのPWとDB名を自分で設定した内容に書き換える必要アリ
DB_URL = MongoDBで発行したURLを記載

※.gitignoreに.envをGitHubにあげないよう、設定を書いておきましょう。

app.jsにMongoDBとの接続設定を追記していきます。

app.js
//インポートゾーンに追加
//MongoDBに接続のためにmongooseインポート
const mongoose = require("mongoose");
//.envを使用するためにインポート
require("dotenv").config({ debug: true });

/**
 * mongoDBに接続.
 */
mongoose.connect(process.env.DB_URL);

/**
 * mongoDBに接続したら際に発動するメソッド.
 */
mongoose.connection.once("open", () => {
  console.log("mongoDB接続完了");
});

コマンドプロンプト(ターミナル)でnodemon appして「mongoDB接続完了」と表示されていればOK

※MongoDBに上手く繋がらない際の可能性

(1).envに記載したURLにPW、DB名を入れていない
(2)DB名をURLに入れるためDBを一旦作成する必要があるが、それがない
(3)IPアドレスの設定が上手くいっていない
私は特に(3)で何度も苦戦しました。wifiが変わるたびにIPアドレスを「ADD CURRENT IP ADDRESS」にする必要があるようです。

■modelsフォルダ内のファイルを書く

ここでは型の設定を書いていきます。
先程schema.jsでもGraphQLの型設定したじゃん!という感じですが、GraphQLに情報を送る際にデータを型になぞらえて整えてあげる等のために設定しているような認識です。

models>todo.js
//DBを使う設定
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

/**
 * Todoモデルの作成.
 */
const todoSchema = new Schema({
  title: String,
  finish: Boolean,
//カテゴリIDだけ持たせればカテゴリ情報取得可能なのでIDのみ持たせる
  categoryId: String,
});

//ここで設定した「todoSchema」を「Todo」という名前で使うよ!という宣言
module.exports = mongoose.model("Todo", todoSchema);

models>category.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

/**
 * Categoryモデルの作成.
 */
const categorySchema = new Schema({
  name: String,
});

module.exports = mongoose.model("Category", categorySchema);

※ここで設定する変数はこれだけで大丈夫なの?と思うかと思いますが、ここでは送る情報を整えるために型を設定しているので、下記の「追加時」に送る情報として使っていないものは不要なのかと認識しています。
8.データの追加編集削除

6. ファイル同士をつなぐ

下記のようにファイル同士をエクスポート&インポートして繋ぎます。
image.png

schema.js
//インポートゾーンに追加
//モデルのインポート
const Todo = require("../models/todo");
const Category = require("../models/category");
app.js
//インポートゾーンに追加
//schema.jsの情報をインポート
const schema = require("./schema/schema");
const graphqlHTTP = require("express-graphql").graphqlHTTP;

/**
 * schemaの使用設定.
 */
app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    graphiql: true,
  })
);

これで準備完了!ではメソッドを書いていきます。

※ここまでの状態まとめ
app.js
const express = require("express");
const app = express();
const graphqlHTTP = require("express-graphql").graphqlHTTP;
const mongoose = require("mongoose");
require("dotenv").config({ debug: true });
const schema = require("./schema/schema");
const cors = require("cors");
app.use(cors());

/**
 * schemaの使用.
 */
app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    graphiql: true,
  })
);

/**
 * ポート番号,起動した際に発動するメソッド.
 */
app.listen(4000, () => {
  console.log("express起動したよ");
});

/**
 * mongoDBに接続.
 */
mongoose.connect(process.env.DB_URL);
/**
 * mongoDBに接続したら際に発動するメソッド.
 */
mongoose.connection.once("open", () => {
  console.log("mongoDB接続完了");
});

schema.js
const graphql = require("graphql");
const { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLBoolean, GraphQLList, GraphQLSchema } = graphql;
//モデル
const Todo = require("../models/todo");
const Category = require("../models/category");

/**
 * Todo型.
 */
const TodoType = new GraphQLObjectType({
  name: "Todo",
  fields: () => ({
    id: { type: GraphQLID },
    title: { type: GraphQLString },
    finish: { type: GraphQLBoolean },
    category: {
      type: CategoryType,
      resolve(parent, args) {
        return Category.findById(parent.categoryId);
      },
    },
  }),
});

/**
 * Category型.
 */
const CategoryType = new GraphQLObjectType({
  name: "Category",
  fields: () => ({
    id: { type: GraphQLID },
    name: { type: GraphQLString },
    todoList: {
      type: new GraphQLList(TodoType),
      resolve(parent, args) {
        return Todo.find({ categoryId: parent.id });
      },
    },
  }),
});
todo.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

/**
 * Todoモデルの作成.
 */
const todoSchema = new Schema({
  title: String,
  finish: Boolean,
  categoryId: String,
});

module.exports = mongoose.model("Todo", todoSchema);
category.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

/**
 * Categoryモデルの作成.
 */
const categorySchema = new Schema({
  name: String,
});

module.exports = mongoose.model("Category", categorySchema);

6. メソッドの書き方

GraphQLのメソッドを書きたいので、schema.jsに書いていきます。
GraphQLのメソッドは
 ・データの取得→Query
 ・データの追加、編集、削除→Mutation
に書いていくという感じです。覚えやすい。

7. データの取得

まずはQueryの枠を準備

schema.js
const RootQuery = new GraphQLObjectType({
  name: "RootQueryType",
  fields: {
 //ここにメソッドを追加していく
  },
});

//外部から使用できるようにエクスポートしておく
module.exports = new GraphQLSchema({
  query: RootQuery,
});

fieldsの//ここにメソッドを追加していく部分にメソッドを追加していきましょう。

■情報を1件取得する

schema.js
    /**
     * Todo情報取得.
     */
    //メソッド名
    getTodo: {
     //使用するGraphQL型(上で設定したやつを使用)
      type: TodoType,
     //ユーザが入力する検索ワードとその型
      args: { id: { type: GraphQLID } },
     //検索結果どう処理して何を返すか
      resolve(parents, args) {
       //argsに入れたIDと同じものをTodo全件から探す→当てはまったものを返す
        return Todo.findById(args.id);
      },
    },

同じ要領でカテゴリ情報も1件取得します。

schema.js
    /**
     * category情報取得.
     */
    getCategory: {
      type: CategoryType,
      args: { id: { type: GraphQLID } },
      resolve(parents, args) {
        return Category.findById(args.id);
      },
    },

■Todo情報を全件取得する

全件返す場合は処理が特に必要ないので、こちらは比較的簡単です。

schema.js
  /**
     * 全Todo情報の取得.
     */
    getAllTodo: {
     //返ってくるデータは複数あるので、リストになる
      type: new GraphQLList(TodoType),
      resolve(parent, args) {
        return Todo.find({});
      },
    },

カテゴリも同じようにメソッドを準備

schema.js
    /**
     * 全Category情報の取得.
     */
    getAllCategory: {
      type: new GraphQLList(CategoryType),
      resolve(parent, args) {
        return Category.find({});
      },
    },

8. データの追加編集削除

まずはMutationの枠を準備

schema.js

const Mutation = new GraphQLObjectType({
  name: "Mutation",
  fields: {
//ここにメソッドを追加していく
  },
});

//外部から使用できるようにエクスポートしておく(取得の際に書いた部分に追記)
module.exports = new GraphQLSchema({
  query: RootQuery,
  mutation: Mutation,
});

では取得の際と同じように、//ここにメソッド…にメソッドを追記していきましょう。

■データの追加

schema.js
 /**
     * Todoの追加
     */
   //メソッド名
    addTodo: {
     //使用するGraphQL型
      type: TodoType,
     //ユーザが入力の必要がある情報(今回追加のため、追加内容全て)
      args: {
        title: { type: GraphQLString },
        //カテゴリと紐づけるために必要
        categoryId: { type: GraphQLID },
        finish: { type: GraphQLBoolean },
      },
      resolve(parent, args) {
       //Todoモデルの型に沿って、情報を作成する
        let todo = new Todo({
          title: args.title,
          categoryId: args.categoryId,
          finish: false,
        });
        //上で作成したデータtodoをDBに保存する
        return todo.save();
      },
    },
schema.js
    /**
     * categoryの追加.
     */
    addCategory: {
      type: CategoryType,
      args: {
        name: { type: GraphQLString },
      },
      resolve(parent, args) {
        let category = new Category({
          name: args.name,
        });
        return category.save();
      },
    },

■データの編集

こちらも同じくMutation内に書けばOKです。

schema.js
   /**
     * Todoの更新.
     * @remarks 項目名、カテゴリ編集可能
     */
    updateTodo: {
     //使うGraphQL型
      type: TodoType,
     //ユーザが入力する情報
      args: {
        //※注意!
        id: { type: new GraphQLNonNull(GraphQLID) },
        title: { type: GraphQLString },
        categoryId: { type: GraphQLID },
      },
      resolve(parent, args) {
        //一旦空の連想配列を準備
        let updateTodo = {};
        //入力したタイトルがあれば、updateTodoのtitleに入力データを入れる
        args.title && (updateTodo.title = args.title);
        //入力したカテゴリがあれば、updateTodoのcategoryに入力データを入れる
        args.categoryId && (updateTodo.categoryId = args.categoryId);
        //該当のTodoにupdateTodoの情報を上書き
        //findByIdAndUpdateという元々あるメソッドを使用
        return Todo.findByIdAndUpdate(args.id, updateTodo, {
          //更新したデータを返す
          new: true,
        });
      },
    },

//※注意!の部分をご覧ください。新たに「GraphQLNonNull」という型が出てきています。これは「この項目はnullではだめですよ」という設定をしたい際に必要です。(今回はIDがないとどのTodoを更新して良いか分からないので必須にしている)
新たな型が出てきた際は、schema.jsの上部分でインポートしたGraphQLの型インポート文に追加が必要です!

schema.js
const {
  GraphQLObjectType,
  GraphQLID,
  GraphQLString,
  GraphQLBoolean,
  GraphQLSchema,
  GraphQLList,
//追加
  GraphQLNonNull,
} = graphql;

ではカテゴリも同じように更新のメソッドを追加します。

schema.js
    /**
     * categoryの更新.
     */
    updateCategory: {
      type: CategoryType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLID) },
        name: { type: GraphQLString },
      },
      resolve(parent, args) {
        let updateCategory = {};
        args.name && (updateCategory.name = args.name);
        return Category.findByIdAndUpdate(args.id, updateCategory, {
          new: true,
        });
      },
    },

更に、Todoが完了した際にfinishをtrueにしたいので、そのメソッドも追加します。

schema.js
   /**
     * Todoの完了.
     */
    finishTodo: {
      type: TodoType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLID) },
        finish: { type: GraphQLBoolean },
      },
      resolve(parent, args) {
        let finishTodo = {};
        finishTodo.finish = args.finish;
        return Todo.findByIdAndUpdate(args.id, finishTodo, {
          new: true,
        });
      },
    },

■データの削除

こちらも同じくMutation内に書けばOKです。
IDさえ入れれば消したいデータが分かるので、処理はそんなに難しくないかと思います。

schema.js
 /**
     * Todoの削除.
     */
    deleteTodo: {
      type: TodoType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLID) },
      },
      resolve(parent, args) {
        //findByIdAndRemoveという元々あるメソッドを使用
        return Todo.findByIdAndRemove(args.id);
      },
    },
schema.js
   /**
     * categoryの削除.
     */
    deleteCategory: {
      type: CategoryType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLID) },
      },
      resolve(parent, args) {
        return Category.findByIdAndRemove(args.id);
      },
    },

9. 上手くいっているかテストしてみる

ここまで登録してきたメソッドなどが上手く動作するか確認してみます。
コマンドプロンプト(ターミナル)でnodemon appで起動させている事を確認し、「 http://localhost:4000/graphql 」で検索してみて下さい。GraphiQLという画面が開くかと思います。
image.png

まずは右側部分に登録したメソッドが反映されているかを確認します。
「query:RootQueryType」をクリックして下記のようになっているでしょうか。
image.png
なっていない場合は書き方を失敗している可能性があるのでコードを見直して下さい。また同じようにMutationの方も登録が完了しているかご確認下さい。

次にここの左側、ごちゃごちゃ文章が書いてある部分に下記ルールでコードを入れてみます。(グレーの文章は削除してOKです)

GraphiQL
・取得→queryで囲む/追加、編集、削除→mutationで囲む
・()内→送るデータ(argsで設定したもの)
・{}内→返して欲しいデータ

パターン1:データの全件取得
query{
メソッド名{
 返して欲しいデータ
 }
}

パターン2:データを1件取得(argsに値を入れる必要がある際)
query{
 メソッド名(id:検索IDの値){
  返して欲しいデータ
 }
}
※idがuserIdだったり変数名は自身の設定した名前に変えてください。

パターン3:mutationの場合
mutation{
 メソッド名(送るデータ変数名:送るデータ値){
  返して欲しいデータ
 }
}

入力の後、左上あたり「▸」をクリックして無事にデータが取得出来れば成功です。メソッド1つ1つ試してみて下さい。

GraphiQL
//パターン1の入力例
query{
  getAllTodo{
    title
 }
}

//パターン2の入力例
//※このようにネストさせてTodoのタイトルと対応するカテゴリの名前等を取得も可能
query{
  getTodo(id:"DBで確認したTodoのID"){
    title
    category{
     name  
  }
 }
}

//パターン3の入力例
mutation{
  addTodo(title:"新しいTodo",
  categoryId:"カテゴリID",
  finish:false){
    title
  }
}
※応用

(1)複数メソッド同時発動
並べて発動させればOK

GraphiQL
query{
  getAllTodo{
    title
  }
  getAllCategory{
    name
  }
}

(2)入力データを一旦変数にしておきたい場合

GraphiQL
//query($変数名:GraphQL型){メソッド名(変数名:$変数名){返して欲しいデータ}}
//必須の場合は型!そうでない場合は!不要
query($id:ID!){
 getTodo(id:$id){
  title
  finish
 }
}

左下「QUERY VARIABLES」に具体的な値を入力

GraphiQL(QUERY VARIABLE)
//変数部分を""で囲むことに注意!
{"id":"取得したいTodoリストのID"}

この状態で「‣」をクリックすれば取得できる。

これらの書き方はクライアントサイドで取得する際に知っておくと役立ちます。

ここまで完了したらサーバサイドでの処理は完了です!お疲れさまでした。

10. まとめ

今回はGraphQLを使用してメソッド作成の方法をまとめました。
まだまだ未熟な部分が多いので、何か気になる点や誤りがあればご連絡下さい。

また後日クライアントサイドについてもまとめたいと思います。
ここまでご覧頂きありがとうございました。

今回作成したコードのGitHub

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?