GraphQLのインラインフラグメントが難しい(難しい)

GraphQLのインラインフラグメントが難しい - yujioramaの日記

GraphQLの「境界条件(コーナーケース)」における挙動を、複数の言語実装(Go, Node.js, Ruby, Perl)とルーターApollo Router, Apollo Gateway, Cosmo)の組み合わせで検証した結果をまとめました。

検証の背景と環境

GraphQLのスキーマが複雑になった際、インラインフラグメントで定義したフィールドが正しくマージされるかは、実装によって差異があるのではないか?という疑問というか実際に困った経験から検証をスタートしました。

検証用に作成した環境は以下の通りです。


実験1:インラインフラグメントにネストしたオブジェクトのマージ

検証内容

インターフェース(AbstractProduct)に対するインラインフラグメントの中で、ルートフィールドと同じオブジェクト(package)をさらに深く選択した場合に、正しくマージされるかを検証します。

クエリ:

query MergeConcreteTypeInRootFieldAndInterfaceFragment {
    productA {
        package {
            id
            name
        }
        ... on AbstractProduct {
            ...ProductDetail
        }
    }
}

fragment ProductDetail on AbstractProduct {
    ... on ProductA {
        package {
            displayOwner: owner { name }
            owners { name }
        }
    }
}

検証結果

期待される動作は、ルートで指定した id, name と、フラグメント内で指定した displayOwner 等が合体して返ってくることです。

構成 結果 備考
バックエンド単体 (Go, Node, Perl, Ruby) ✅ 成功 すべての実装で正しくマージされました
Apollo Router ✅ 成功
Apollo Gateway ❌ 失敗 フラグメント内のネストしたフィールドが消える
WunderGraph Cosmo ❌ 失敗 同上

Apollo GatewayやCosmoでは、インラインフラグメント側のネストしたフィールドが無視されてしまうという興味深い結果になりました。


実験2:リスト型フィールドを含むインラインフラグメントのマージ

検証内容

リスト型のフィールド(productList)を持つインターフェースに対して、フラグメントを介してデータを取得する際の挙動を検証します。

クエリ:

query MergeInlineFragment {
    itemList {
        __typename
        ...ItemA
        ...ItemB
    }
}

fragment ItemA on ItemA {
    name
    productList { package { id } }
}

検証結果

このケースでは、バックエンドの実装レベルで明確な差が出ました。

バックエンド実装 結果 エラー内容
Go (gqlgen) ✅ 成功
Node.js (Apollo) ✅ 成功
Perl (graphql-perl) ❌ 失敗 Can't call method "isa" on an undefined value
Ruby (graphql-ruby) ❌ 失敗 500 Internal Server Error

PerlRubyの実装では、ルーターの有無にかかわらずバックエンド単体で実行しても、リスト型のインターフェース処理中に内部エラーが発生しました。 複雑なフラグメントとリストが組み合わさるケースでは、ライブラリの成熟度が影響するようです。


まとめ

今回の検証を通じて、以下のことが分かりました。

  1. ルーターによる挙動の差: Apollo Routerはマージ処理が強力ですが、Apollo GatewayやCosmoでは一部のネストした選択セットが欠落する場合があります。
  2. 言語実装による制限: GoやNode.jsの実装は安定していますが、PerlRubyの実装では、複雑なリスト・インターフェースの組み合わせでランタイムエラーが発生することがあります。
    • フィールドリゾルバーの実装方法によっては正しい挙動になる可能性があります
    • 各ライブラリのバージョンアップにより挙動が改善される可能性があります

GraphQLは「どこでも同じように動く」ことが期待されますが、Federation構成や複雑なスキーマを扱う際は、事前の互換性検証が重要であると改めて実感しました。

検証コードは GitHub - yujiorama/graphql-test: testing GraphQL corner case に置いてあります。

GraphQLのインラインフラグメントが難しい

この記事ははてなエンジニア Advent Calendar 2025 21日目の記事です。 昨日の担当は id:fxwx23 さんの SwiftUI.SectionのFooterの末端要素を画面下端にalignさせるテク - 23's blog でした。


GraphQLを使いこなしていくと、インターフェースや共用体(Union)を扱う場面でインラインフラグメント(... on Typeは避けて通れません。 しかし、フラグメントが複雑に組み合わさると、一見正しいはずのクエリが意図しないレスポンスを返したり、ランタイムエラーを引き起こしたりすることがあります。

この記事では、開発現場で発生した問題をベースに、GraphQL Specification(October 2021)の仕様と照らし合わせながら、その正体を解明していきます。


前提:検証に使用するGraphQLスキーマ

今回の各ケースでは、以下のスキーマ定義を前提としています。AbstractProduct を実装した ProductA/B と、それらをリストで保持する ItemA/B という構造です。

type Package {
     id: ID!
     name: String!
     owner: PackageOwner
     owners: [PackageOwner!]!
 }

interface AbstractProduct {
     package: Package!
}
 type PackageOwner {
     name: String!
 }

 type ProductA implements AbstractProduct {
     package: Package!
 }
 type ProductB implements AbstractProduct {
     package: Package!
 }

 interface AbstractItem {
     name: String!
}

 type ItemA implements AbstractItem {
     name: String!
     productList: [ProductA!]!
}
 type ItemB implements AbstractItem {
     name: String!
     productList: [ProductB!]!
}

type Query {
    productA: ProductA
    itemList: [AbstractItem!]!
}

問題1:インラインフラグメントに定義したエイリアスフィールドが消失する

クエリ

query {
    productA {
        __typename
        package {
            __typename
            id
            name
        }
        ... on AbstractProduct {
            ...ProductDetail
        }
    }
}

fragment ProductDetail on AbstractProduct {
    ... on ProductB {
        __typename
        package {
            __typename
            id
            name
            displayOwner: owner {
                __typename
                name
            }
            owners {
                __typename
                name
            }
        }
    }
    ... on ProductA {
        __typename
        package {
            __typename
            id
            name
            displayOwner: owner {
                name
            }
            owners {
                __typename
                name
            }
        }
    }
}

クエリ構造の視覚化

graph TD
    Root[Query] --> productA
    subgraph "Selection Set for productA"
        productA --> P_Root["package (id, name)"]
        productA --> Fragment["... on AbstractProduct (ProductDetail)"]
        subgraph "Merged Selection Set for package"
            Fragment --> P_Frag["package (id, name, displayOwner: owner, owners)"]
        end
    end
    P_Root -.-> Merge{Merge?}
    P_Frag -.-> Merge
    Merge --> Final["Problem: displayOwner field might disappear"]

解説:Field Selection Merging の落とし穴

このクエリでは、productA 直下の package と、フラグメント内の package が重複しています。GraphQLの仕様上、これらは一つの選択集合としてマージされるべきです。

しかし、開発現場ではエイリアスフィールドの displayOwner が消失しました。 たぶん原因はサーバー側のマージアルゴリズムの問題で、ルートレベルの単純な定義が、フラグメント内の詳細なエイリアス定義を上書き・無視してしまうようでした。


問題2:インラインフラグメントによるルートフィールドのマージと消失

クエリ

query {
    productA {
        __typename
        package {
            __typename
            id
            name
            owner {
                __typename
                name
            }
            owners {
                __typename
                name
            }
        }
        ... on AbstractProduct {
            ...ProductDetail
        }
    }
}

fragment ProductDetail on AbstractProduct {
    ... on ProductB {
        __typename
        package {
            __typename
            id
            name
        }
    }
    ... on ProductA {
        __typename
        package {
            __typename
            id
            name
        }
    }
}

クエリ構造の視覚化

graph LR
    subgraph "Query Root Selection"
        R["package { id, name, owner, owners }"]
    end
    subgraph "Fragment Selection"
        F["package { id, name }"]
    end
    R -- "Merged by Spec" --- F
    F -- "Masked by Implementation" --> Final["Result: package { id, name } (Missing owner)"]

解説:Selection Set Merging とマスキング

ここではルートで owner を要求していますが、フラグメント側では packageidname のみで再定義しています。

仕様上は和集合になるはずですが開発現場では owner が消失しました。 たぶん原因はサーバー側のマージアルゴリズムの問題で、後から定義された「浅い定義」を優先してデータをフィルタリング(マスキング)してしまうようでした。


問題3:インラインフラグメントに同じ名前で異なる型のリストフィールドがあると実行時エラー

クエリ

query {
    itemList {
        ...ItemA
        ...ItemB
    }
}

fragment ItemA on ItemA {
    __typename
    name
    productList {
        __typename
        package {
            __typename
            id
        }
    }
}
fragment ItemB on ItemB {
    __typename
    name
    productList {
        __typename
        package {
            __typename
            id
        }
    }
}

クエリ構造の視覚化

classDiagram
    class productList {
        <<field name>>
    }
    class Type_ItemA {
        productList: [ProductA!]
    }
    class Type_ItemB {
        productList: [ProductB!]
    }
    Type_ItemA --|> productList
    Type_ItemB --|> productList
    productList --|> Conflict : "Same Response Shape Rule Violation"

解説:Same Response Shape の制約

ItemAproductList[ProductA!] 型であり、ItemB のそれは [ProductB!] 型です。

同一フィールド名(productList)でマージされる際、それらは同じレスポンス形状である必要があります。 現場では GraphQL Federation 環境において発生した問題で、通常の GraphQL サーバーでは発生していません。 ゲートウェイが「サブグラフから返ってきた ProductB は、ItemA 側の定義(ProductA)と互換性がない」と判断しているのかな、という感想です。


実際、どの実装なら正しく動くのか?

いろいろ調べたかったのだけど実験が終わってないので、別の機会にまとめようと思います。


参考資料


はてなエンジニア Advent Calendar 2025 明日の担当は id:chaya2z です。

Implementing Google BigQuery REST API (v2) jobs.query

これは はてなエンジニア Advent Calendar 2024 25日目の記事です。

github.com

Google BigQuery REST API (v2)のjobs.query を実装した Perl モジュールを作りました。

おすすめポイント

  • 列の値をフラットな hashref へ変換します
    • ->{f}->[0]->{v} みたいな値の探り方をしなくていいので便利です
    • なぜこういうデータ構造になっているのか全然わからない
  • 列の値のデータ型を JSON Schema で定義できるようになっています
    • データ入出力の境界には型があると安心です

やったこと

MinillaPerlモジュールの雛形を作るだけでした。簡単。

モジュールを作ろうと思った理由

Google::BigQueryDiscovery document という仕組みを使っていて、リクエスト先のURLを実行時に組み立てるようになっています。

しかし、いつからか Discovery document の返り値の path 要素に含まれるパス変数の形式が変化してしまい、動かなくなってしまっていました。

  • 期待値 projects/{projectId}/queries
  • 実際の値 projects/{+projectId}/queries
    •   "query": {
          "description": "Runs a BigQuery SQL query synchronously and returns query results if the query completes within a specified timeout.",
          "flatPath": "projects/{projectsId}/queries",
          "httpMethod": "POST",
          "id": "bigquery.jobs.query",
          "parameterOrder": [
            "projectId"
          ],
          "parameters": {
            "projectId": {
              "description": "Required. Project ID of the query request.",
              "location": "path",
              "pattern": "^[^/]+$",
              "required": true,
              "type": "string"
            }
          },
          "path": "projects/{+projectId}/queries",
          "request": {
            "$ref": "QueryRequest"
          },
          "response": {
            "$ref": "QueryResponse"
          },
          "scopes": [
            "https://www.googleapis.com/auth/bigquery",
            "https://www.googleapis.com/auth/cloud-platform",
            "https://www.googleapis.com/auth/cloud-platform.read-only"
          ]
        }
      

やらなかったこと

CPANへの登録

perlnewmod - 新しいモジュールを配布するには - perldoc.jp を読んで片手間ではやりきれんな、と思ってやめました。

Google::BigQuery に変更を提案

10年前に作られたモジュールで、GitHub リポジトリも消えているので、これ以上時間かけてもな、と思ってやめました。

明日の担当は id:k1s1eee さんです。

主体や目的を具体的に示す+結果をどう扱うかを示す

LLMは最大公約数的な答えを返してくれるので便利だな。 主体や目的を具体的に示すことは意識しているけど、結果をどう扱うかも示すようにしよう。

問いかけ

「コマンドを叩く」「コマンドを打つ」「APIに投げる」「 APIを叩く」「レビュー依頼を投げる」など、責任を押し付けるような言葉使いを憂いているので、どうすれば改善できるか建設的な意見を1000文字以内でまとめて欲しい。

Geminiの答え

責任転嫁を連想させる言葉遣いの改善策

「コマンドを叩く」「APIに投げる」といった表現は、まるでコンピュータに仕事を丸投げしているかのような印象を与え、責任感の欠如や、他者への丸投げを連想させる可能性があります。これらの言葉遣いを改善し、より建設的なコミュニケーションを促進するためには、以下の点に留意することが重要です。

1. 行動主体を明確にする

  • 「私が」コマンドを実行する
  • 「私が」APIを呼び出す
  • 「私が」レビュー依頼を行う このように、主語を明確にすることで、自分が主体的に行動していることを示し、責任感の有無を明確にします。

2. 行為の内容を具体的に説明する

  • 「コマンドを実行して、~という処理を行う」
  • APIを呼び出し、~というデータを取得する」
  • 「レビュー依頼を行い、~という点について確認をお願いする」

単に「コマンドを叩く」と言うよりも、具体的な処理内容を説明することで、何をしているのかを周りに理解してもらいやすくなります。

3. 相手への配慮を示す言葉を選ぶ

  • 「ご協力をお願いして、~を検証していただきたく」
  • 「~について、ご意見を伺えれば幸いです」

一方的な指示ではなく、相手への配慮を示す言葉を選ぶことで、共同で作業を進めるという意識を醸成します。

4. 結果に対する責任を持つことを示す言葉を選ぶ

  • 「この処理を実行した結果、~ということが確認できました」
  • 「このAPIの呼び出しによって、~というデータを取得し、~という結論に至りました」

結果に対する責任を持つことを示す言葉を選ぶことで、単に作業を行っただけでなく、その結果について深く考えていることを示します。

5. 問題発生時の対応について言及する

  • 「もしエラーが発生した場合には、~という対処を行います」
  • 「レビューの結果、修正が必要な箇所が見つかった場合は、~という対応を行います」

問題が発生した場合の対応について言及することで、責任感を持っていることを示し、安心感を与えることができます。

その他

  • 多様な表現を使う: 同じ言葉の繰り返しを避けることで、表現に変化を持たせ、単調さを解消します。
  • 状況に合わせて言葉を選ぶ: 状況や相手に合わせて、適切な言葉を選ぶことが重要です。
  • チームで共通の言葉遣いを決める: チーム内で共通の言葉遣いを決めることで、コミュニケーションの円滑化に繋がります。

これらの提案を参考に、状況に合わせて言葉を選ぶことで、より建設的なコミュニケーションを促進し、チーム全体の士気を高めることに繋がります。

まとめ

責任転嫁を連想させる言葉遣いを改善するためには、行動主体を明確にし、行為の内容を具体的に説明する、相手への配慮を示す言葉を選ぶ、結果に対する責任を持つことを示す言葉を選ぶ、問題発生時の対応について言及するなど、様々な工夫が考えられます。これらの提案を参考に、より良いコミュニケーションを目指しましょう。

ポイント

  • 主語を明確にする
  • 行為の内容を具体的に説明する
  • 相手への配慮を示す
  • 結果に対する責任を持つ
  • 問題発生時の対応について言及する

補足

上記はあくまで一般的な提案であり、具体的な改善策は、チームの状況や個人の性格などによって異なります。

さらに良い表現例

従来の表現 より良い表現
コマンドを叩く コマンドを実行する、スクリプトを実行する
APIに投げる APIを呼び出す、APIリクエストを送信する
レビュー依頼を投げる レビューをお願いする、レビュー依頼を送信する

その他

  • 「投げる」という表現は、物体を投げつけるイメージがあり、やや攻撃的な印象を与えます。
  • 「叩く」という表現は、力任せに行うイメージがあり、丁寧さを欠く場合があります。
  • 「依頼する」や「お願いする」といった言葉を使うことで、相手への敬意を示すことができます。

上記を参考に、状況に合わせて適切な言葉を選ぶようにしましょう。

ChatGPTの答え

現代の技術職や開発現場では、「コマンドを叩く」「APIを叩く」「レビュー依頼を投げる」といった表現が日常的に用いられます。しかし、これらの表現はそのまま受け取ると、曖昧さや責任を転嫁する意識が潜在的に含まれていると感じられる場合があります。そのため、より明確で責任を意識した言葉遣いに改善することが望ましいと考えられます。以下に具体的な改善策を提案します。

1. アクションの主体を明確にする

曖昧な表現はしばしば行為の主体を不明瞭にします。「コマンドを叩く」「APIを叩く」という言葉は単なる行為の発生を指しますが、「〇〇を実行する」「〇〇にリクエストを送信する」といった表現にすることで、何が行われているのかを具体的に説明できます。また、「レビュー依頼を投げる」という表現も、「〇〇さんにレビューを依頼する」と明確にすることで、依頼する人の名前を出し、責任の所在を明らかにすることができます。

2. 行動の目的を意識する

アクションの背景にある目的を言葉に含めると、表現の意味が具体化され、責任感が伴います。「APIに投げる」という表現を、「データを取得するためにAPIにリクエストを送る」とすることで、行動の目的が明確になります。目的を意識することで、行動の価値が共有され、他者にも正確に伝わります。

3. 曖昧な言葉を避ける

「叩く」「投げる」といった言葉は感覚的でカジュアルな印象を与えるため、状況によっては専門的な言語に置き換えるのが有効です。「コマンドを叩く」ではなく、「コマンドを実行する」「コマンドを入力する」と言い換えることで、具体性と共に責任が伝わります。チーム内のやりとりをフォーマルにする必要がある場合には特に有効です。

4. チーム文化の見直し

言葉遣いは文化や習慣から生まれるため、改善にはチーム全体での意識の共有が必要です。定期的に言葉遣いに関するワークショップを開くことで、より建設的で責任感のあるコミュニケーション方法を模索する場を設けるのも有効でしょう。また、コードレビューやフィードバックの場で、意識的に明確な言葉を使うことを推奨し、習慣化を目指します。

5. 誤解を防ぐ意識を持つ

相手に誤解を与えないことも大切です。「レビューを依頼する」という言葉は、「レビューの時間をお願いしている」という意識を伝えるために、「〇〇さんにレビューをお願いしてもよいでしょうか」といった丁寧な表現にすることが考えられます。これにより、相手の時間や負担に配慮する姿勢が伝わります。

このように、言葉を丁寧に選ぶことで責任感を伴ったコミュニケーションが可能になり、チーム全体の信頼感を高めることができます。

【未公開記事の発掘】AWS Lambda ファンクションのインスタンスをウォームアップ状態で維持したい

起動に時間のかかるファンクションで困っていたのかもしれない。


背景

  • HTTP リクエストを処理するだけの Lambda ファンクションがある
  • ほとんどリクエストがこないのでそのたびに新規作成してる
  • リクエストの送信元が設定している応答タイムアウト時間が短くて、エラーになりやすい

目的

  • リクエストの送信元でタイムアウトしないようにしたい
  • リクエストのたびに新規作成しないで済むよう Lambda ファンクションを WarmUp 状態で維持する
  • できるだけ費用がかからないようにする

世の中の知見

ソリューション

Lambda ファンクションのインスタンスVPC で実行するなら15分、そうでないなら5分くらいで廃棄されるようです。

廃棄までの時間内に新しいイベントが到着すれば、継続して動き続けるはず。

できるだけ費用がかからないように、という前提があっても、定期的にリクエストを送信する方法はいろいろ考えられます。

第7回 Data-Oriented Programming 読書会


参加者トピック

  • 引き続き chatGPT 話題
    • 最近は Edge で Bing AI を使うことが増えている
    • ライブラリの選定に使っている人がいたりする
  • ステージマネージャ は便利なのでお勧め
  • Anker が古くなったモバイルバッテリーを引き取るキャンペーンをしている

nextbook

ディスカッション

13.4 Multimethods with dynamic dispatch

  • 実行時のデータに基づいて呼び出す関数を決める仕組みのことだった
  • 汎用データ構造でも静的型付け言語と同じようなことができる、ということなのかな

13.5 Integrating multimethods in a production system

  • 普通はプレゼンテーションに近いところで書式を指定するものだけど、モデルのすぐそばで判断、加工しているのは不思議

13 Summary

14 Advanced data manipulation

14.1 Updating a value in a map with eloquence

  • データを取得して加工して設定するのではなく、データを加工する関数を渡すようにする

14.2 Manipulating nested data

  • flatMapflatten + map のどちらが分かりやすいか議論している

14.3 Using the best tool for the job

  • forEach を撲滅したい

14.4 Unwinding at ease

  • MongoDB の $unwind オペレータは便利

14 Summary

15 Debugging

15.1 Determinism in programming

  • いろんな層の言葉が乱用されているし、基本的な事実を新発見のように説明しているので、読んでいてつらい
  • 経験主義というやつだろうか

15.2 Reproducibility with numbers and strings

15.3 Reproducibility with any data

15.4 Unit tests

15.5 Dealing with external data sources

15 Summary

参考情報

第6回 Data-Oriented Programming 読書会


参加者トピック

  • chatGPT の話ばかりしてる
  • GitHub CopilotOSS メンテナ認定は雑すぎて助かる
  • GPU ボードのメモリは 16 GB ないと厳しい

ディスカッション

11.3 Representing a client request as a map

  • 最初から汎用的なAPIにするのは早すぎる最適化だと思う

11.4 Representing a server response as a map

  • properties にフィールド名を書かなくても required に書いただけでフィールドを生やすらしい

11.5 Passing information forward

特になし

11.6 Search result enrichment in action

特になし

11 Summary

特になし

Part 3. Maintainability

特になし

12 Advanced data validation

特になし

12.1 Function arguments validation

  • 保守を任されたシステムで、最初の仕事がデータの型付けだったら辛すぎる
  • JSON Schema に静的型付け言語と同じような表現力を求めている

12.2 Return value validation

特になし

12.3 Advanced data validation

  • LOCALE=C date の出力を見ると format="date" が受け入れてもらえる気がしない
  • 不安なので正規表現を書いてしまう、と思う

12.4 Automatic generation of data model diagrams

PlantText UML Editor

12.5 Automatic generation of schema-based unit tests

12.6 A new gift

  • JSON Schema はフィールドの値域が条件によって変わるようだと記述量が多くなりがちで困る

12 Summary

  • OpenAPI は JSON Schema のスーパーセットでありサブセットでもあるので OpenAPI を使っていくとよさそう

13 Polymorphism

13.1 The essence of polymorphism

13.2 Multimethods with single dispatch

13.3 Multimethods with multiple dispatch

PlayCode - Javascript Playground

参考情報