アーキテクチャ
アーキテクチャフィールド解決順序の操作

フィールド解決順序の操作

Multiple Query Execution が提供する @export ディレクティブの目的は、フィールド(またはフィールドの集合)の値を変数にエクスポートし、クエリ内の別の場所で使用することです。

変数への値のエクスポートが行われる前に変数の読み取りが行われると、このディレクティブは機能しません。そのため、エンジンはフィールドの実行順序を制御する方法を提供する必要があります。

Gato GraphQL は、クエリ自体を通じてフィールドの実行順序を操作する方法を提供しています。エンジンは各タイプごとにイテレーションでデータを読み込みます。まずクエリ内で最初に遭遇したタイプのすべてのフィールドを解決し、次に2番目のタイプのすべてのフィールドを解決するという順序で、処理すべきタイプがなくなるまで続きます。

例えば、DirectorFilmActor タイプのオブジェクトが関係する次のクエリを見てみましょう:

{
  directors {
    name
    films {
      title
      actors {
        name
      }
    }
  }
}

...は GraphQL エンジンによって次の順序で解決されます:

タイプをイテレーションで処理する

処理済みのタイプが、未読み込みのデータ(例:追加オブジェクト、または既に読み込まれたオブジェクトの追加フィールド)を取得するためにクエリ内で再び参照された場合、そのタイプはイテレーションリストの末尾に再度追加されます。

例えば、ActorpreferredDirector フィールド(Director タイプのオブジェクトを返します)も次のようにクエリする場合:

{
  directors {
    name
    films {
      title
      actors {
        name
        preferredDirector {
          name
        }
      }
    }
  }
}

...GraphQL エンジンはクエリを次の順序で処理します:

イテレーションでの繰り返しタイプ

1つのクエリで @export を実行する場合にこれがどのように機能するかを見てみましょう。最初の試みとして、フィールドの実行順序を考慮せず、通常どおりクエリを作成します:

query GetPostsAuthorNames {
  user(by: { id: 1 }) {
    name @export(as: "authorName")
  }
  posts(filter: { search: $authorName }) {
    id
    title
  }
}

クエリを実行すると、次のレスポンスが返されます:

変数を使用するクエリの実行

...次のエラーが含まれています:

{
  "errors": [
    {
      "message": "Expression 'authorName' is undefined",
    }
  ]
}

このエラーは、変数 $authorName が読み取られた時点でまだ設定されておらず、undefined であったことを意味します。

なぜこれが起こるのかを見てみましょう。まず、クエリに登場するタイプを以下のコメントとして分析します:

# Type: Root
query GetPostsAuthorNames {
  # Type: User
  user(by: {id: 1}) {
    # Type: String
    name @export(as: "authorName")
  }
  # Type: Post
  posts(filter: { search: $authorName }) {
    # Type: ID
    id
    # Type: String
    title
  }
}

タイプを処理してデータを読み込むために、データ読み込みエンジンはクエリタイプ Root を FIFO(First-In, First-Out:先入れ先出し)リストに追加します。これによりアルゴリズムに渡される初期リストは [Root] となり、その後タイプを順次イテレーションします:

#操作リスト
0FIFOリストを準備する[Root]
1aリストの最初のタイプ(Root)を取り出す[]
1bRoot タイプからクエリされたすべてのフィールドを処理する:
user(by: {id: 1})
posts(filter: { search: $authorName })
それらのタイプ(UserPost)をリストに追加する
[User, Post]
2aリストの最初のタイプ(User)を取り出す[Post]
2bUser タイプからクエリされたフィールドを処理する:
name @export(as: "authorName")
スカラー型(String)なので、リストに追加する必要はない
[Post]
3aリストの最初のタイプ(Post)を取り出す[]
3bPost タイプからクエリされたすべてのフィールドを処理する:
id
title
スカラー型(IDString)なので、リストに追加する必要はない
[]
4リストが空になり、イテレーションが終了する。 

ここで問題が明らかになります:@export はステップ 2b で実行されますが、読み取りはステップ 1b で行われていました。

ここでフィールドの実行フローを制御する必要があります。実装された解決策は、エクスポートされた変数が読み取られるタイミングを遅らせることで、Root タイプから self フィールドを人工的にクエリすることで実現されます。

self フィールドは、その名前が示すとおり、同じオブジェクトを返します。Root オブジェクトに適用すると、同じ Root オブジェクトを返します。「すでにルートオブジェクトを持っているのに、なぜ再度取得する必要があるのか?」と思うかもしれません。なぜなら、エンジンのアルゴリズムがこの新しい Root への参照を FIFO リストの末尾に追加する必要があり、それによってクエリされたフィールドをこれらの各イテレーションの前後に意図的に分配できるからです。

そのため、上記のクエリでは posts(filter:{ search: $authorName }) フィールドが self フィールドの内側に配置されており、クエリを実行すると期待どおりのレスポンスが返されます:

query GetPostsAuthorNames {
  user(by: {id: 1}) {
    name @export(as: "authorName")
  }
  self {
    posts(filter: { search: $authorName }) {
      id
      title
    }
  }
}

@export を使用した最初のクエリの実行

このクエリでタイプが処理される順序を確認し、なぜうまく機能するのかを理解しましょう:

#操作リスト
0FIFOリストを準備する[Root]
1aリストの最初のタイプ(Root)を取り出す[]
1bRoot タイプからクエリされたすべてのフィールドを処理する:
user(by: {id: 1})
self
それらのタイプ(UserRoot)をリストに追加する
[User, Root]
2aリストの最初のタイプ(User)を取り出す[Root]
2bUser タイプからクエリされたフィールドを処理する:
name @export(as: "authorName")
スカラー型(String)なので、リストに追加する必要はない
[Root]
3aリストの最初のタイプ(Root)を取り出す[]
3bRoot タイプからクエリされたフィールドを処理する:
posts(filter:{ search: $authorName })
そのタイプ(Post)をリストに追加する
[Post]
4aリストの最初のタイプ(Post)を取り出す[]
4bPost タイプからクエリされたすべてのフィールドを処理する:
id
title
スカラー型(IDString)なので、リストに追加する必要はない
[]
5リストが空になり、イテレーションが終了する。 

これで問題が解決されたことがわかります:@export はステップ 2b で実行され、ステップ 3b で読み取られます。

Multiple Query Execution は クエリの分離 を行う際にまさにこれを実行します:GraphQL ドキュメントを変換して self フィールドを追加することで、各オペレーションのフィールドが前のすべてのオペレーションのすべてのフィールドが解決された後にのみ実行されるようにします。