コンセプト、アイデア、戦略
コンセプト、アイデア、戦略ネストされたミューテーションの解説

ネストされたミューテーションの解説

ミューテーションは、GraphQLサーバー上のデータを変更できる操作です。たとえば、投稿の作成、ユーザー名の更新、投稿へのコメント追加などが該当します。

GraphQLでは、ミューテーションは次のようにMutationRoot型のみに公開されます。

type MutationRoot {
  createPost(id: ID!, title: String!, content: String): Post!
  updateUserName(userID: ID!, newName: String!): User!
  addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}

(このガイドのGraphQLスキーマは例を説明するためのものであり、プラグインが提供するスキーマとは異なります。)

このスキーマを使用すると、ユーザー名の変更は次のように実現されます。

mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

ミューテーションがmutation root object typeのみに公開されているのは、GraphQL仕様で説明されているように、直列実行を強制するためです。

It is expected that the top level fields in a mutation operation perform side‐effects on the underlying data system. Serial execution of the provided mutations ensures against race conditions during these side‐effects.

「直列実行」という用語は「並列実行」の対義語であり、並列実行はフィールドの解決において推奨される動作です。

たとえば、以下のクエリでは、GraphQLサーバーがどのフィールド(nameまたはemail)を最初に解決するかは重要ではなく、これらは並列に解決できます。

query {
  user(by: { id: 37 }) {
    name
    email
  }
}

しかし、ミューテーションはデータを変更するため、フィールドが解決される順序が重要となり、直列実行が必要です(そうしないと、レースコンディションが発生する可能性があります)。

たとえば、以下の2つのクエリは異なる結果を生成します。

# クエリ1: 実行後、ユーザー名は "John" になります
mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
  updateUserName(userID: 37, newName: "John") {
    name
  }
}
 
# クエリ2: 実行後、ユーザー名は "Peter" になります
mutation {
  updateUserName(userID: 37, newName: "John") {
    name
  }
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

ミューテーションをMutationRootのみを通じて公開することの結果として、この型は肥大化してしまいます。直列実行が必要という技術的な事情以外には共通点のないフィールドが混在することになり、これはインターフェース設計の決定ではなく技術的な問題です。

ネストされたミューテーションの必要性

上記のミューテーションの中で、createPostだけが真にMutationRoot型に属します。なぜなら、これは何もないところから新しい要素を作成するからです。一方、updateUserNameaddCommentToPostは、別の型の既存エンティティに適用される同等の操作を持つことが十分可能です。

type User {
  updateName(newName: String!): User!
}
 
type Post {
  addComment(comment: String!, userID: ID): Comment!
}

このスキーマを使用すると、ユーザー名の変更は次のように実現できます。

mutation {
  user(ID: 37) {
    updateName(newName: "Peter") {
      name
    }
  }
}

この機能を「ネストされたミューテーション」と呼びます。クエリまたはミューテーションである別の操作の結果に対してミューテーションを適用することです。

ネストされたミューテーションを使用することで、GraphQLスキーマがいかにエレガントになるかに注目してください。

  • MutationRoot.updateUserName操作はユーザーのIDを受け取る必要がありますが、同等のUser.updateName操作はユーザーエンティティ上で実行されるため、IDを受け取る必要がありません
  • フィールド名がupdateUserNameからupdateNameに短縮されます

さらに、グラフ内のエンティティ間をナビゲートしてデータをクエリするのと同じ方法でデータを変更できるため、GraphQLサービスはよりシンプルで理解しやすくなります。

ネストされたミューテーションは複数のレベルに渡ることができます。たとえば、単一のクエリ内で新しく作成した投稿にコメントを追加することができます。

mutation {
  createPost(ID: 37, title: "Hello world!", content: "Just another post") {
    id
    addComment(comment: "Lovely post") {
      id
    }
  }
}

このことから、ネストされたミューテーションは、複数の要素をミューテートするために複数のクエリを実行することから、単一のクエリを実行することへと、ラウンドトリップのレイテンシーを削減することでパフォーマンスを向上させることもできます。

ネストされたミューテーションが仕様に含まれていない理由

GraphQL仕様は、あらゆる言語のGraphQLサーバーのすべての実装に対して機能することを目的としています。しかし、その推進力はリファレンス実装であるgraphql-jsを通じたJavaScriptです。

言い換えると、graphql-jsでサポートできない機能は仕様の一部にはなりません。

JavaScriptがpromisesをサポートしているため、フィールドの並列解決が実現可能となり、並列処理はgraphql-jsを最初に設計する際の基本原則の一つとなりました。これは、バッチング関数がJavaScript promisesを返すデータフェッチ層であるDataLoaderに表れています。

並列実行のパフォーマンス上の利点は非常に多く、ネストされたミューテーションは並列処理とともに機能することができません。並列実行とネストされたミューテーションを引き換えにする価値はないと判断されました。

ネストされたミューテーションとパフォーマンス

Gato GraphQLプラグインでは、フィールドは常に直列に解決され、解決される順序は決定論的です。(この特性はクエリ解決のパフォーマンスに影響しません。サーバーはまずクエリ内のグラフをコンポーネントモデルに変換し、それを最適な線形時間で解決するからです。)

これは、プラグインがネストされたミューテーションをサポートでき、そのすべての利点を提供しながら、その弊害を受けないことを意味します。

GraphQL仕様

この機能は現在GraphQL仕様の一部ではありませんが、以下で要求されています。