GoでGraphQLのMutationを実装する


このエントリーではGolang + MongoDBでGraphQLを使ってみるに書きつぐ形で、GraphQLのMutationについての説明を行います。

一度改めて、まとめを兼ねたGraphQLの説明をします。

GraphQLとは

GraphQLでは、これまでRESTfulで作ってきたように複数のエンドポイントを作成して通信を行う方法を行いません。
エンドポイントは最低一つだけで動くようになるのがGraphQLです。

基本的にはGraphQLの利点は、

・エンドポイント一つだけで良い
・型安全にデータベースとやりとりができる

の二点を心に留めておけば問題ないかと思います(補足があれば喜んでいただきます)。

さらっとした説明ですが、ここからは実装に関するお話。
と言ってもコードをつらつら書くわけではなく、概念的なものを考えていきます。

GraphQLの実装の考え方

まず、GraphQLの実装の流れは以下のような雰囲気で感じ取ってもらえればと思います(この流れはGolangに限らず一般的なものだと思いますたぶん)。

  1. GraphQLで扱うTypeの定義(GraphQLが扱うデータの設定)
  2. Fieldをデータの取得や作成の用途に合わせて定義(ここでTypeを利用する)
  3. Fieldをまとめてスキーマを作る
  4. GraphQLのリクエストを作成(GraphQLがデータベースに問い合わせる要求)
  5. スキーマとリクエストを合わせてGraphQLを実行
  6. 指定したデータが戻ってくる

Golang + MongoDBでGraphQLを使ってみるにも書きましたが、このリクエストには二つの概念が含まれており、その一つがこのエントリーのタイトルとなっているMutation(以下ミューテーション)なのです。
*リクエストは公式で使われている言葉ではありませんが、この方が伝わりやすいかなと思ったのでこの表現を使っています。
*流れの4に「データベースと問い合わせる要求」と書いていますが、GraphQLが直接的にデータベースとやりとりすることはありません(実装の仕方によってはデータベースと直接やりとりするかもしれませんが。。。)。GraphQLは飽くまで、データベースから取り出されたデータ群に対して実行されます。例えば、特定のUserを取得するGraphQLのクエリーを実施すると、それはすでにデータベースから取得されたUserテーブルの全てのレコード(ここでは取り出されてから構造体の配列に変換しています)に対して行われます。

リクエストにはクエリーとミューテーションが含まれます。

クエリー(いままでのGETのようなもの)・・・データの取得要求
ミューテーション(いままでのPOSTのようなもの)・・・データの更新要求

前回の記事ではクエリーを利用したデータの取得に関するGraphQLの使い方を解説しましたが、ここではミューテーションを扱って、データをInsertする方法を解説します。
(あとあとUpdateやDeleteに関しても書きつぐだろうと思います)

Golang + GraphQLでMutationを実装する

データベースはNoSQLのMongoDBを使います。

前回の記事では、GraphQLのスキーマの定義箇所のコードが以下のようになっていました。

rootQuery := graphql.ObjectConfig{
  Name: "RootQuery",
  Fields: fields.UsersField,
}
schemaConfig := graphql.SchemaConfig{
  Query: graphql.NewObject(rootQuery), // Query
}
schema, err := graphql.NewSchema(schemaConfig)
if err != nil {
  log.Fatalf("failed to create new schema, error: %v", err)
}

これがミューテーションを使う場合には以下のようになります。

rootQuery := graphql.ObjectConfig{
  Name: "RootQuery",
  Fields: fields.UsersField,
}
rootMutation := graphql.ObjectConfig{
  Name: "RootMutation",
  Fields: fields.CreateUserField,
}
schemaConfig := graphql.SchemaConfig{
  Query: graphql.NewObject(rootQuery), // Query
  Mutation: graphql.NewObject(rootMutation), // Mutation
}
schema, err := graphql.NewSchema(schemaConfig)
if err != nil {
  log.Fatalf("failed to create new schema, error: %v", err)
}

rootMutation変数が作成され、SchemaConfigのMutationにこれを新しいGraphQLのオブジェクトとして追加しています。
*前回の記事では同じファイルにfieldsを定義していましたが、今回はファイルを分けているので、fields.UsersFieldやfields.CreateUserFieldのように書いています。

それでは、rootMutationの中身を説明していきます。
それほど複雑ではないので、楽に感じるかもしれません。

rootMutationはfields.CreateUserField(ここではfields/user.goにあるCreateUserField変数)をFieldsとして設定しています。

// fields/user.go
var CreateUserField = graphql.Fields{
  "createUser": &graphql.Field{
    Type: types.UserType,
    Description: "Create new user",
    Args: graphql.FieldConfigArgument{
      "name": &graphql.ArgumentConfig{
        Type: graphql.NewNonNull(graphql.String),
      },
      "email": &graphql.ArgumentConfig{
        Type: graphql.NewNonNull(graphql.String),
      },
      "password": &graphql.ArgumentConfig{
        Type: graphql.NewNonNull(graphql.String),
      },
    },
    Resolve: func(params graphql.ResolveParams) (interface{}, error) {
      name, _ := params.Args["name"].(string)
      email, _ := params.Args["email"].(string)
      password, _ := params.Args["password"].(string)

      database.CreateNewUser(name, email, password)

      return nil, nil
    },
  },
}

クエリーで特定のユーザーを取得するときは、冒頭が"user"で始まっていましたが、ここでは"createUser"となっています。
ここの名前はあとでリクエストを送るときに利用します。

Argsについてもクエリーの時と同じように理解していただければ良いです。リクエストを出すときに指定できる要素です。
コードでは、name, email, passwordとなっているので、リクエストを出すときにはこれらの要素を設定できるということです。POSTのリクエストとしては普通のことですね。

最後から六行目のdatabase.CreateNewUser(name, email, password)は、データベースとやりとりする箇所です。
ここではファイルを外出ししているので、外にあるdatabaseディレクトリのuser.goファイルにデータベースとのやりとりをしているコードがあるということです。
database/user.goは以下の通りです。

// database/user.go
func CreateNewUser(name string, email string, password string) {
  // connection to mongoDB
  session, _ := mgo.Dial("mongodb://localhost/test")
  defer session.Close()
  db := session.DB("test")
  // insert
  newUser := &User{
    Id: bson.NewObjectId(),
    Name: name,
    Email: email,
    Password: password,
  }
  col := db.C("users")
  if err := col.Insert(newUser); err != nil {
    log.Fatalln(err)
  }
}

ここはMongoDBでのInsert方法を理解していれば問題なく読み取れると思います。
func (*Collection) Insert

はい、これでミューテーションの処理は終わりです。
ミューテーションの中の動きは以上となります

実装されたMutationを利用する

ミューテーションの機能を利用するには、それを実行するためのリクエストを送る必要があります。

クエリーのリクエストは以下のようなものでした。

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

これをミューテーションのリクエストに書き換えると、

request := `
  mutation {
    createUser(name: "name", email: "email", password: "pass") {
      name
      email
      password
    }
  }
`

fields/user.goのcreateUserFieldは"createUser"という名前で定義したので、ここでcreateUserでミューテーションを書いています。
そしてその引数で作成する新しいデータの要素を指定します。

それらの要素はcreateUserFieldのArgsのそれぞれに対応しており、それらがResolveの関数に渡ってInsertが実施されるわけです。
案外簡単な話だと思います。

mongoでデータベースに入ってdb.users.find()とかで中身を覗くと、作成したドキュメントが追加されていると思います。
入ってなかったらちょっとググってみてください笑

まあ、GraphQLでは動き方がわかればだいたいの実装の方法がわかってくるようになると思います。
それと、実際にコードを書いて動かしてみることも大切ですね

間違った表現や説明をしている箇所があれば、忌憚なくご指摘をお願いいたします。