🪐

GraphQLのFragmentについての話

2022/02/02に公開約3,600字

GraphQL における Fragment とは

GraphQL におけるFragmentとは、複数のquerymutationの間で共有することができるロジックのことです。
Fragmentを単体で定義すると下記のように書けます。

fragment NameParts on Person {
  firstName
  lastName
}

Fragmentには関連付けされたタイプに属するフィールドが含まれます。
上記の例だと Person 型は NameParts フラグメントが有効であるため、 firstName フィールドと lastName フィールドを宣言する必要があります。
そして、Person型のオブジェクトを取得するqueryを定義するときは下記のように書くことができます。Fragmentはスプレッド構文で記載できます。
実際にはfirstName,lastNameが展開されてフィールドにアクセス可能になっています。

query getPerson($input: InputType) {
  getPerson(input: $input) {
    ...NameParts
    avatar {
      imageURL
    }
  }
}

Fragmentは定義するとキャッシュされ、複数のquerymutationで再定義やimportすることなく使用することが可能です。
また、あるフィールドに対して返却される可能性のある型が複数存在する場合、クライアント側では下記のように記載することができます。

## この時点ではBook型とMovie型のどちらが実際に返ってくるかわからない
query getMedia($input: Input) {
  getMedia(input: $input) {
    ... on Book {
      title
      page
    }
    ... on Movie {
      title
      screeningTime
    }
  }
}

ただし、上記のqueryを正常に機能させるにはクライアントが型間のポリモーフィズムな関係を理解する必要があります。
スキーまでどのように実装する必要があるかについて、UnionInterfaceを使用する方法を見ていきます。

Union

Unionは、複数のオブジェクト型のうち 1 つを返すことができる GraphQL 型です。
Unionに含まれる型はオブジェクトである必要があります。

union Media = Book | Movie

type Query {
  allMedia: [Media] # Book 型と Movie 型を持つ配列
}

上記を実際にどのように使えるか見てみましょう。

query getMedia($input: Input) {
  getMedia(input: $input) {
    __typename
    ... on Book {
      title
      page
    }
    ... on Movie {
      title
      screeningTime
    }
  }
}

フィールドの戻り値にUnion型を使うと、GraphQLクライアントは戻り値がUnion型のうち、どの型を返すのかわかりません。
そのため、返却される可能性のある型を全てFragmentで定義し、受け取れるようにします。
queryを実行すると下記のような結果になります。

{
  "data": {
    "search": [
      {
        "__typename": "Book",
        "title": "The Complete Works of William Shakespeare",
        "page": 460
      },
      {
        "__typename": "Movie",
        "title": "William Shakespeare",
        "screeningTime": "02:30"
      }
    ]
  }
}

Interface

Interface は、複数のオブジェクト型に含めることができる一連のフィールドを指定します。
オブジェクトタイプがInterfaceimplementsする場合、そのInterfaceのすべてのフィールドを含める必要があります。
フィールドは、そのリターンタイプとしてInterfaceを持つことができます。この場合、そのInterfaceimplementsする任意のオブジェクト型を返すことができます。
下記の場合Textbookを含めることができます。

interface Book {
  title: String!
  author: Author!
}

type Textbook implements Book {
  title: String!
  author: Author!
  text: String!
}

type PhotoBook implements Book {
  title: String!
  author: Author!
  place: String!
}

type Query {
  books: [Book!] # Textbook, PhotoBook オブジェクトを持つ
}

クライアント側では、Interfaceに含まれていないサブフィールドをFragmentで参照することができます。

query getBooks {
  getBooks {
    __typename
    title
    ... on Textbook {
      text
    }
    ... on PhotoBook {
      place
    }
  }
}

query を実行すると下記のような結果が返ってきます。

{
  "data": {
    "books": [
      {
        "__typename": "Textbook",
        "title": "Wheelock's Latin",
        "text": "the sign of author"
      },
      {
        "__typename": "PhotoBook",
        "title": "Oops All Water",
        "place": "Spain"
      }
    ]
  }
}

Union と Interface

両者は、query発行時には同様の書き方で使うことができますが、スキーマ定義上での役割は別物だということがわかりました。
Unionは全く関係ないオブジェクトを使いたい時に使い、Interfaceはある特定のフィールドセットをいろんな型で使いたいときに使うんだろうなということをふんわり理解しました。

GraphQL Code Generater で Fragment 定義を取り込みたい

@graphql-codegen/fragment-matcherを使う必要がある。

https://www.graphql-code-generator.com/plugins/fragment-matcher

https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/
https://www.apollographql.com/docs/react/data/fragments/#defining-possibletypes-manually
https://www.apollographql.com/docs/react/data/fragments/#defining-possibletypes-manually
GitHubで編集を提案

Discussion

ログインするとコメントできます