まくろぐ
更新: / 作成:

GraphQL フラグメントでクエリをパーツ化する

フラグメントの基本

GraphQL クエリの中で、同じようなフィールドの指定(選択セット)を複数回使用する場合、それを フラグメント (Fragments) という再利用可能な選択セットとして切り出して定義しておくことができます。

例えば次の GraphQL クエリは、GitHub から自分のユーザー情報 (viewer) と、特定のユーザーの情報 (user) を一度に取得しています。

GraphQL クエリ
query QueryTwoUsers {
  viewer {
    login       # ログインID
    name        # ユーザー名
    url         # ユーザーの GitHub ホームページ
    websiteUrl  # ユーザーの Web サイト
    avatarUrl   # ユーザーのアバター画像
  }

  user(login: "ログインID") {
    login
    name
    url
    websiteUrl
    avatarUrl
  }
}

viewer フィールドと user フィールドは、両方とも User 型 のフィールドで、しかも、上記の例では User オブジェクトの中の同じフィールドを参照しています。 明らかに冗長な書き方です。

このようなケースでは、あるオブジェクトの特定のフィールドを参照するための選択セット (selection set) を、フラグメントの形で定義することができます。 次の例では、User オブジェクトの特定のフィールドを選択するための userFragment というフラグメントを定義し、クエリ内で参照しています。

GraphQL クエリ
query QueryTwoUsers {
  viewer {
    ...userFragment
  }

  user(login: "ログインID") {
    ...userFragment
  }
}

# User オブジェクトの中のフィールドを選択するためのフラグメント
fragment userFragment on User {
  login       # ログインID
  name        # ユーザー名
  url         # ユーザーの GitHub ホームページ
  websiteUrl  # ユーザーの Web サイト
  avatarUrl   # ユーザーのアバター画像
}

フラグメントを使用する場所では、...userFragment のようにドットを 3 つ付けて参照します。 これを GraphQL 用語で フラグメント・スプレッド(fragment spread) といいます。 その場所にフラグメントの内容を「展開する」という意味です。 JavaScript でオブジェクトを展開するときのスプレッド構文 (...obj) と同様のフォーマットが採用されています。

fragment の定義は、query の定義と同じ階層に記述することに注意してください。 また、on User というのは、User オブジェクトのフィールドを選択するフラグメントであることを示しており、このフラグメントは User オブジェクトのフィールド部分でしか使えません。 上記の例では、vieweruserUser 型のフィールドなので、その中で問題なく ...userFragment と参照できています。

フラグメントの使用例

上記のサンプルコードはあまり実用的じゃなかったので、もう少し実際のアプリで使いそうなクエリを載せておきます。 次の GraphQL クエリでは、ログイン中のユーザー情報と、そのユーザーの Issue 情報を取得しています。 Issue がアサインされているユーザーの情報を取得するために、userFragment フラグメントを使っています。

GraphQL クエリ
query QueryLoginUser {
  viewer {
    ...userFragment
    issues(states: OPEN, last: 10) {
      nodes {
        id
        title
        assignees(last: 10) {
          nodes { ...userFragment }
        }
      }
    }
  }
}

fragment userFragment on User {
  login name url websiteUrl avatarUrl
}

ちなみに、GraphQL ドキュメントの中でフラグメントを定義した場合は、必ずどこかでフラグメント・スプレッドを使って参照しなければいけないというルールがあります。 これは、フラグメントの定義だけを GraphQL サーバーに送ることが無駄だからです。 例えば、GitHub の GraphQL API では、フラグメントの定義だけして参照しないと、次のようなエラーが返されます。

“Fragment userFragment was defined, but not used”

フラグメント定義内から変数を参照する

クエリ内でフラグメント・スプレッドを使用した場合、そのクエリに渡された変数 (variablse) をフラグメントの定義内からも参照できます。 次の公式サイトの例では、クエリ変数 $first の値(デフォルト値は 3)を、フラグメントの定義内から参照しています。 フラグメント・スプレッドを記述する場所で、変数をたらい回しにしなくてよいということですね。

query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

インライン・フラグメント (Inline fragments)

インライン・フラグメント (Inline fragments) は、インタフェース型や union 型として定義されたデータの中から、特定の型のオブジェクトのフィールドを参照するときに使用します(オブジェクト指向言語におけるダウンキャストと同じ概念です)。 インライン・フラグメントを使用するときは、クエリの選択セットの中に、次のように名前なしのフラグメントを定義するような構文で記述します。

... on 型名 {
  フィールド名
  フィールド名
  フィールド名
}

下記は、GitHub の GraphQL API を使って、特定のリポジトリの Issue と PullRequest の一覧を取得するクエリの例です。

query QueryRecentActivities {
  search(type: ISSUE, query: "repo:graphql/graphql-spec", last: 10) {
    nodes {
      ... on Issue {
        number
        title
      }
      ... on PullRequest {
        title
        baseRefName
      }
    }
  }
}

検索結果は nodes フィールドで配列として参照できるのですが、その中の各要素は SearchResultItem 型のオブジェクトになっています。 SearchResultItem は次のような union 型として定義されており、実際に各要素がどの型のオブジェクトとして返されるかは、クエリを実行してみるまで分かりません。

"""
The results of a search.
"""
union SearchResultItem = App | Discussion | Issue | MarketplaceListing | Organization | PullRequest | Repository | User

それぞれの型のフィールドは当然異なっているので、そのまま汎用的な SearchResultItem のフィールドとして参照することはできないようになっています。 そこで次のようにインライン・フラグメントを使うことで、そのオブジェクトが Issue 型の場合は numbertitle フィールドを参照し、PullRequest 型の場合は titlebaseRefName フィールドを参照することを宣言します。

... on Issue {
  number
  title
}
... on PullRequest {
  title
  baseRefName
}

参照しようとしたオブジェクトが Issue 型だったときは、GraphQL サーバーは ... on PullRequest で指定されたフィールドを返しません(その逆も同様です)。 つまり、インライン・フラグメントは、実際に返されるオブジェクトの型によって参照するフィールドを場合分けする 役割を持っています。 インライン・フラグメントはこのように union 型のフィールドを参照する場合や、インタフェースのフィールドを参照する場合に使用しますが、union 型のフィールドを参照する場合は、次のように名前付きのフラグメントを使用することもできます。

query today {
  agenda {
    ...workout
    ...study
  }
}

fragment workout on Workout {
  name
  reps
}

fragment study on StudyGroup {
  name
  subject
  students
}

ちなみに、union 型のフィールドを要求したときに、実際にどの型のオブジェクトが返されたかは、次のように typename メタフィールドを参照すると分かります。 これは、GraphQL のイントロスペクション機能のひとつです。

query GetIssueOrPullRequest {
  repository(owner: "facebook", name: "graphql") {
    issueOrPullRequest(number: 3) {
      __typename
      ... on Issue {
        closed
        closedAt
      }
      ... on PullRequest {
        merged
        mergedAt
      }
    }
  }
}

Apollo Client で GraphQL のフラグメントを使用する

React を使った Web アプリでは、GraphQL ライブラリとして Apollo Client が使用されることが多いと思います。 もちろん、Apollo Client で使用する GraphQL クエリの中でもフラグメントを使用できます。

TypeScript (JavaScript) では、クエリ文字列の中から変数展開することができるので、フラグメントの定義は文字列変数(定数)の形で定義しておくと便利です。 次の例では、rateLimitFragment という GraphQL フラグメントを、RATE_LIMIT_FRAGMENT という文字列定数として定義しています。

queries.ts
import {gql} from '@apollo/client'

const RATE_LIMIT_FRAGMENT = gql`
  fragment rateLimitFragment on Query {
    rateLimit {
      cost
      remaining
    }
  }
`

// 自分にアサインされた PullRequest の一覧
export const QUERY_MY_PULLS = gql`
  ${RATE_LIMIT_FRAGMENT}
  query {
    ...rateLimitFragment
    rateLimit { cost remaining }
    search(type: ISSUE, last: 100, query: "is:open is:pr review-requested:@me") {
      issueCount
      nodes {
        ... on PullRequest { number title bodyText url }
      }
    }
  }
`

文字列定数 RATE_LIMIT_FRAGMENT で定義されたフラグメントを使用するために、QUERY_MY_PULLS の中で ${RATE_LIMIT_FRAGMENT} と展開しています。 つまり、そこに fragment の定義を記述したのと同じ振る舞いになります。 あとは適切な場所で ...rateLimitFragment と参照すれば OK です。

このようにフラグメント定義部分を文字列定数として切り出しておけば、別の GraphQL クエリの中からも同様に参照することができます。

一応、QUERY_MY_PULLS の使用例も。

components/MyPullRequests.tsx
import { FC } from 'react'
import { useQuery } from '@apollo/client'
import { PullRequestLink } from './PullRequestLink'
import { QUERY_MY_PULLS } from '../queries'

export const MyPullRequests: FC = () => {
  const {loading, error, data} = useQuery(QUERY_MY_PULLS)
  if (loading) return <p>Loading ...</p>
  if (error) return <p>Error: {error.message}</p>
  const pulls = createPullRequests(data)

  return <>
    <h3>自分のレビュー待ち PR</h3>
    <ul>
      {pulls.map(x => <PullRequestLink key={x.number} pr={x}/>)}
    </ul>
  </>
}

// ...

関連記事

まくろぐ
サイトマップまくへのメッセージ