GoとMongoDBでGraphQLを使ってみる

GraphQLとは

すでに紹介されている記事があるので、その辺を参考にしてみてください。

GraphQL入門 - 使いたくなるGraphQL

アプリ開発の流れを変える「GraphQL」はRESTとどう違うのか比較してみた

GraphQLはRESTの置き換えではない

GraphQLは何に向いているか

GraphQLは概念的に難しくはないですが、実際にどうやって動かすのかがわかりにくいかも知れません。かく言う私も。。。

日本語のテキストも多くはないと思うので参考にしていただけたら嬉しいです:pencil:

GolangでGraphQL

今回はGolangでGraphQLを使います。 Goでなんかしてみたいなと思ってGraphQLの使い方を調べてみましたが、あまり日本語で解説されているところ(graphql-goを使っているページ)が多くなかったので、英語記事等も参考にして勉強しました。

GolangもGraphQLも新参者なので、正確ではない部分や改善すべき点もあると思いますが、その時は忌憚なく指摘いただければと思います:sweat_smile:

使うもの

Golang
  go version go1.11.5 darwin/amd64
GraphQL
  v0.7.7
MongoDB
  MongoDB shell version v4.0.3
macOS
  Mojave version 10.14.3

インストール方法とかは特筆する必要もないので省きます。

実装手順

  1. MongoDBに接続してデモ用にデータを作成
  2. GraphQLの導入
  3. GraphQLを利用してデータを取得

GraphQLを使うので、RDBMSは使わずに、NoSQLを利用します。 GraphQLは決してRDBMSとともに利用できないと言うわけではないようですが、利用例が少ないみたいで、基本的にはNoSQLと利用することが多いと言うような話を耳にしました。 ですのでここではドキュメント指向型のNoSQLであるMongoDBを使っています。

ちなみに、HasuraというサービスではpostgresqlでGraphQLを利用できるらしいです。 :point_right_tone2::point_right_tone2::point_right_tone2: Hasura

MongoDBに接続してデモ用にデータを作成

すでにMongoDBがインストールされていると言うことを前提として、作業でディレクトリで$ sudo mongod --dbpath ./を実行します。 するとディレクトリ内に様々なファイルやフォルダが作成され、MongoDBが起動します。

この状態で$ mongoを実行すると、MongoDBとのコネクションを始めることができます。

MongoDBで使えるコマンドは以下を参考にしてみてください。 MongoDB の 基本的 な 使い方

ここでは基本的に、

> db ・・・ データベース参照
> show collections ・・・ コレクションのリストを表示
> db.collection_name.find() ・・・ 指定したコレクションの全件取得
> db.collection_name.drop() ・・・ 指定したコレクションの削除

これらのコマンドを使うことになります。 *MongoDBのようなドキュメント指向型のNoSQLではコレクション=テーブル、ドキュメント=レコードみたいに考えればいいと思います。

GraphQLの導入

GolangでGraphQLを使う場合、graphql-goやGQLgenなどいくつかパッケージの選択肢がありますが、ここではgraphql-goを利用します。

$ go get github.com/graphql-go/graphql
import (
  "github.com/graphql-go/graphql"
)

graphql-go/graphql GoでGraphQLを使う場合の選択肢

// localのデータベースtestに接続
func main() {
  session, _ := mgo.Dial("mongodb://localhost/test")
  defer session.Close()
  db := session.DB("test")
}

MongoDBでのInsertとFind

// insert into users
newUser := &User{
  Id: bson.NewObjectId(),
  Name: "名前",
  Email: "アドレス",
  Password: "パス"
}
col := db.C("users")
if err := col.Insert(newUser); err != nil {
  log.Fatalln(err)
}

// fetch all users
var AllUsers []User
query := db.C("users").Find(bson.M{})
query.All(&AllUsers)
// AllUsersは構造体なので、JSONにする場合はjson.Marshal()を。

好きなようにデータを作成しておいてください。 updateとdeleteはここでは説明を省きます。 公式ドキュメントが一番手っ取り早いです。 mongoDB Documents

GraphQLを利用してデータを取得

今回はサンプルとしてUser構造体を作成し、これをGraphQLの定義に利用します。

type User struct {
    Id bson.ObjectId `bson:"_id"` // MongoDBで自動生成される_idとUser.Idを同一のものとする
    Name string `bson:"name"`
    Email string `bson:"email"`
    Password string `bson:"password"`
}

bsonって何? って感じですが、jsonと同じものと考えれば問題ないです。 MongoDB超入門

次に、スキーマとリクエストを用意します(この2つの正式な名称ではありませんが、わかりやすいと思ったのでこう呼ばせていただきます)。 GraphQLでデータを取って来るためには、これら2つの要素が必要になります。

最終的には以下のようなコードにスキーマとリクエストを突っ込みます。

params := graphql.Params{
  Schema: schema, // スキーマ
  RequestString: request, // リクエスト
}
r := graphql.Do(params) // GraphQLの実行し、rに結果を格納
if len(r.Errors) > 0 { // エラーがあれば
  log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := json.Marshal(r) // 構造体をJSONに変換して、
fmt.Printf("%s \n", rJSON) // プリントする。

スキーマ

以下コードの下から4行目にあるのがスキーマとなります。 スキーマとは、GraphQLが要求を投げかける対象のデータ群やそのデータ群の設定を含めた仕様書のようなものです。 データベースからデータを取り出したり、データ一つ一つの型やリクエストで渡されてくることになるidなどの引数の設定を行います。

GraphQLではまず対象となるデータをすべて、前もってデータベースから引っ張り出しておきます。 ですのでGraphQLが直接的にデータベースとやりとりするわけではありません。 GraphQLが要求するのはすでに取り出されたデータ群であり、いわばそのひと塊をふるいにかけて、必要なデータだけを取り出すのがGraphQLだということです。

// MongoDBと接続してAllUsers(構造体)を用意する
session, _ := mgo.Dial("mongodb://localhost/test")
defer session.Close()
db := session.DB("test")
var AllUsers []User // フィールド定義時にこれを使う
query := db.C("users").Find(bson.M{})
query.All(&AllUsers)

var userType = graphql.NewObject( // GraphQL用の型定義
  graphql.ObjectConfig{
    Name: "User",
    Fields: graphql.Fields{
      "id": &graphql.Field{
        Type: graphql.String,
      },
      "name": &graphql.Field{
        Type: graphql.String,
      },
      "email": &graphql.Field{
        Type: graphql.String,
      },
      "password": &graphql.Field{
        Type: graphql.String,
      },
    },
  },
)
fields := graphql.Fields{ // フィールド(リクエストで使われるデータの扱い方に関する設定)
  "user": &graphql.Field{
    Type: userType,
    Description: "Fetch user by Id",
    Args: graphql.FieldConfigArgument{ // リクエストに渡す引数についての設定
      "id": &graphql.ArgumentConfig{
        Type: graphql.String,
      },
    },
    Resolve: func(param graphql.ResolveParams) (interface{}, error) { // 帰って来るデータの設定
      id, ok := param.Args["id"].(string)
      if ok {
        for _, user := range AllUsers { // AllUsersはmonngoDBから取ってきた全てUserのデータ
          if user.Id.Hex() == id { // Hex()でMongoDBのobjectIdをstringに変換する
            return user, nil
          }
        }
      }
      return nil, nil
    },
  },
  "list": &graphql.Field{
    Type: graphql.NewList(userType),
    Description: "Fetch users list",
    Resolve: func(params graphql.ResolveParams) (interface{}, error) {
      return AllUsers, nil // AllUsersはmonngoDBから取ってきた全てUserのデータ
    },
  },
}
rootQuery := graphql.ObjectConfig{
  Name: "RootQuery",
  Fields: fields,
}
schemaConfig := graphql.SchemaConfig{
  Query: graphql.NewObject(rootQuery),
}
schema, err := graphql.NewSchema(schemaConfig) // スキーマ
if err != nil {
  log.Fatalf("failed to create new schema, error: %v", err)
}

流れとしては、

  1. GraphQL用の型を定義する
  2. フィールドを定義(リクエストを投げた時に帰ってくるデータの設定。ここでは"user"と"list"を定義している)
  3. Schemaの設定としてまとめ(ObjectConfigとSchemaConfigのあたり)、NewSchemaでスキーマを作成する

かなりざっくりとしていますが、だいたいこんな感じです:information_desk_person_tone2:

フィールドについては、 "user"が特定のユーザーを取得する場合に使われる要求 "list"が全てのユーザーを取得する要求 となっています。

だから、"user"のResolveの項目では、リクエストで渡されることになる(あとで説明します)引数の"id"に合致するuser.Idの持ち主をreturnで返しています。

Resolve: func(param graphql.ResolveParams) (interface{}, error) {
  id, ok := param.Args["id"].(string) // リクエストで渡されることになる引数の"id"
  if ok {
    for _, user := range AllUsers { // MongoDBから引っ張って来る全てのuserのデータ
      if user.Id.Hex() == id { // 引数の"id"と合致すれば通る
        return user, nil
      }
    }
  }
  return nil, nil
},

"list"ではAllUser全てをreturnしていることも確認しておいてください。 また、"list"でTypeの定義時にgraphql.NewList(userType)としているのは、この"list"が配列であるためです。 単体であれば"user"と同様にuserTypeのみを設定すれば良いですが、あくまでリストなのでこのように設定しています。

リクエス

リクエストとは、GraphQLによる問い合わせの表現です(ただし、このリクエストという呼び方は公式的な呼び方ではなく、説明しやすかったためにここで私が利用しているだけの言葉です)。

request := `
  {
    user(id: "5c94f4d7e803694b2d09da75") {
      id
      name
      email
      password
    }
  }
`

userの後に()で指定している"id"こそ、先ほどスキーマで説明した「リクエストで渡されることになる引数」そのものです。このように設定された引数が、先ほどのResolve内の関数の中で処理されます。

上記のリクエストでは、idに合致するドキュメント(レコード)のid, name, email, passwordを返します。 なので、nameだけを指定して書くと、もちろん該当のドキュメントのnameだけを返します。

また、このリクエストでは、"user"の情報(user1人の情報)しか引き出せません。 全てのユーザーの情報を取得するためには、先ほどスキーマで作成した"list"を指定する必要があります。

request := `
  {
    list {
      id
      name
      email
      password
    }
  }
`

このリクエストによって全てのユーザーが配列で返って来るようになります。

では、これらスキーマとリクエストを組み合わせてGraphQLを実行します。

GraphQLの実行

先ほどのコードに戻ります。

params := graphql.Params{
  Schema: schema, // スキーマ
  RequestString: request, // リクエスト
}
r := graphql.Do(params) // GraphQLの実行
if len(r.Errors) > 0 { // エラーがあれば
  log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := json.Marshal(r) // 構造体をJSONに変換して、
fmt.Printf("%s \n", rJSON) // プリントする。

schemaをSchemaに、requestをRequestStringに設定して、graphql.DoでGraphQLを実行します。 すると、返ってきた結果が構造体として変数に格納されるので、ここではjsonに変換してからプリントしています。

ファイルを実行すると、

$ go run main.go
{"data":{"user":{"email":"ai@kizuna","id":"ObjectIdHex(\"5c94f4d7e803694b2d09da75\")","name":"キズナアイ","password":"kizunAI"}}} 

こんな感じで帰って来ると思います(ここではクエリーにuser(id: "id_string")で指定して一つだけ取ってきています。listなら配列になります)。

POSTして使う

GraphQLの利点としては、RESTfulAPIのようにエンドポイントを何個も用意する必要がない点が挙げられます。

APIを一つ作っておいて、POSTして要求を渡すようにすれば、都度そのようきゅうに応じた結果を得ることができるようになります。 そうすることで、ユーザーを取って来るにしても、GET:usersやらGET:users/:idやらとエンドポイントを分ける手間が省けるようになります。

まあ、サーバー構築をして利用するところまでは気が向いたら書き継ごうかなと思っています:ramen: とりあえずこの辺で:robot:

あれ、データのfetchしかしてなくね?

ここで紹介したGraphQLの活用方法では、データの取得方法しか解説していません。 ですが、データの取得以外にもGraphQLではInsertやUpdateなども行うことができます(ただ、前述したようにGraphQLが直接的にInsertなどを行うわけではないので厳密にはこの表現は間違っているかもしれません)。もちろんエンドポイントを増やすことなしに。

ではどうするのかという話ですが、これ以上ページを冗長にするのはあまりよろしくないと思われますので、別にページを設けようと思っています。完成次第こちらにリンクを載せるのでそちらも参考にしてみてください。

そんなに長くないですし、コードの方が分量的に文章よりも多いかもしれません。

ただ、一つだけここで説明しておくと、先ほどからリクエストやスキーマで出てきた「要求」という語句には、クエリーとミューテーションという概念を含まれており、

クエリー・・・データの取得要求 ミューテーション・・・データの更新要求

というようにGraphQLでは処理の振り分けを行なっています。

ここで説明してきたリクエストはこのクエリーに該当し、Insertなどの処理を行うときにはこのミューテーションを利用することになります。

ミューテーション実装の記事を書きました! GoでGraphQLのMutationを実装する

英語ですが下記公式ページでも解説していますので、参考にしてみてください。 Queries and Mutations

サンプルコード

このサンプルコードではMongoDBは使っていません。 コピペして実行するだけでGraphQLを実行することができます。

UserのIdをintに変更している点、フィールドの定義も少し異なるので中止してください。

package main

import (
  "encoding/json"
  "fmt"
  "log"
  "github.com/graphql-go/graphql"
)

type User struct {
  Id int
  Name string
  Email string
  Password string
}

var userType = graphql.NewObject(
  graphql.ObjectConfig{
    Name: "User",
    Fields: graphql.Fields{
      "id": &graphql.Field{
        Type: graphql.Int,
      },
      "name": &graphql.Field{
        Type: graphql.String,
      },
      "email": &graphql.Field{
        Type: graphql.String,
      },
      "password": &graphql.Field{
        Type: graphql.String,
      },
    },
  },
)

func generateUsers() []User {
  user := User{
    Id: 1,
    Name: "キズナアイ",
    Email: "ai@kizuna",
    Password: "kizunAI",
  }
  var users []User
  users = append(users, user)
  return users
}

func main() {
  users := generateUsers()

  fields := graphql.Fields{
    "user": &graphql.Field{
      Type: userType,
      Description: "Fetch user by Id",
      Args: graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{
          Type: graphql.Int,
        },
      },
      Resolve: func(param graphql.ResolveParams) (interface{}, error) {
        id, ok := param.Args["id"].(int)
        if ok {
          for _, user := range users {
            if int(user.Id) == id {
              return user, nil
            }
          }
        }
        return nil, nil
      },
    },
    "list": &graphql.Field{
      Type: graphql.NewList(userType),
      Description: "Fetch users list",
      Resolve: func(params graphql.ResolveParams) (interface{}, error) {
        return users, nil
      },
    },
  }
  rootQuery := graphql.ObjectConfig{
    Name: "RootQuery",
    Fields: fields,
  }
  schemaConfig := graphql.SchemaConfig{
    Query: graphql.NewObject(rootQuery),
  }
  schema, err := graphql.NewSchema(schemaConfig)
  if err != nil {
    log.Fatalf("failed to create new schema, error: %v", err)
  }

  request := `
    {
      user(id: 1) {
        id
        name
        email
        password
      }
    }
  `

  params := graphql.Params{
    Schema: schema,
    RequestString: request,
  }
  r := graphql.Do(params)
  if len(r.Errors) > 0 {
    log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
  }
  rJSON, _ := json.Marshal(r)
  fmt.Printf("%s \n", rJSON)
}

これ参考になった Go GraphQL Beginners Tutorial