ハウテレビジョンブログ

『外資就活ドットコム』『Liiga』『Mond』を開発している株式会社ハウテレビジョンのブログです。

GraphQL Fragments を導入してクエリの可読性・保守性を高めた際の学びと落とし穴

こんにちは、知の共有プラットフォーム Mond 開発チーム所属、新卒エンジニアの id:chima91 です。

最近は空気がかなり乾燥してきたので、風邪を引かないよう体調管理に気をつけています。 運動・睡眠・食事、あとはお風呂上がりの保湿ケア、どれも欠かせないですね。 皆勤賞を取るんだ、くらいの気持ちで頑張っております。

はじめに

さて、MondではデータのCRUDにおける手段の一部としてGraphQLを使用しています。GraphQLでは既存のクエリに影響を与えることなく、取得するフィールドを追加したり削除したりすることができるため、APIを柔軟に保守することが可能です。

今回はそのGraphQLにおいて、複数のクエリやミューテーション間でフィールドを共有するための、Fragmentsというロジックを導入した際の学びと落とし穴について紹介したいと思います。

GraphQL Fragments とは

Queries and Mutations | GraphQL

Fragments let you construct sets of fields, and then include them in queries where you need to.

GraphQLのドキュメントでは、Fragmentsは必要な時にクエリに含めるフィールドのセットであると定義されています。

単純に言い換えると、Fragmentsは変数のようなものだと思います。 関連付けられた型(Mondでいうと、TopicCommentなど)に関するフィールドの固まりを予め定義しておき、それをTopicオブジェクトやCommentオブジェクトを参照するクエリやミューテーションに含めて利用します。

fragment commentFields on Comment {
  id
    status
  content_length
  content_html
  created_at
    updated_at
}

query Hoge(
  $limit: Int = 30
) {
  topic (
    limit: $limit
  ) {
    id
    content
    comments(
      where: { status: { _eq: active } }
    ) {
      ...commentFields
    }
  }
}

query Fuga(
  $sub_comment_id: String!
) {
  sub_comment_by_pk (
    id: $sub_comment_id
  ) {
    id
    content
    comments {
      ...commentFields
    }
  }
}

異なるクエリ間で同じデータ構造を要求する場合に、その構造を一貫して保つことができます。これにより、クライアントサイドのキャッシュ管理が容易になり、データの取得効率が向上する可能性があります。

Mondでの導入背景

Mondでは、同じ型に対する複数のクエリで共通のフィールドを取得している場合でも、これまではFragmentsを使用していませんでした。各クエリでバラバラに、必要であろうフィールドを書いてデータを取得していました。また、各コンポーネントごとに必要なデータを取得するクエリを発行しているため、パフォーマンス面でも良くありません。

そこで今回は手始めに、GraphQL周りのリファクタリングの一環として、今後のリファクタリングにおける前座の意味合いでFragmentsを導入することにしました。

導入して良かったこと

なんといってもまずは、可読性と保守性の向上です。クエリ内の一部のフィールドセットを再利用可能な単位とした定義により、クエリ間で共通して取得しているフィールドを簡単に把握できるようになりました。また、Fragmentsのフィールドを編集することで、各クエリを編集することなく取得したいフィールドを変更可能になったのが大きな変化でした。

さらに、Fragmentsを導入するにあたり、普段はメンテナンスできていないクエリに目を通す機会を得ました。その結果、使われていないのに取得しているフィールドがあるクエリがいくつか見つかりました。副次的効果ではありますが、それらをメンテナンスすることでパフォーマンスの向上に繋げることができて良かったです。

Fragmentsの落とし穴

ただし、Fragmentsの導入時には注意すべき点が2つあります。

multiple definitions for fragment

1つ目は、気づかずに一つのクエリの中で同じFragmentsを呼んでしまう可能性がある点です。

以下はfragmentの定義と、それを利用しているクエリの例です。

fragment subCommentFields on sub_comment {
  id
  status
  author {
    id
        display_name
  }
  comment {
    ...commentFields
    topic {
      id
      content
    }
  }
  created_at
}
query GetCommentDetailById($comment_id: String!) {
  comment_by_pk(id: $comment_id) {
    author {
      id
      display_name
    }
    topic {
      id
      content
    }
    sub_comments(order_by: { created_at: asc }) {
      ...subCommentFields
    }
        ...commentFields
  }
}

GetCommentDetailById というクエリでは、commentFieldssubCommentFieldsという2つのfragmentを利用しています。しかし、subCommentFields の中でcommentFields を利用しているため、一つのクエリの中で commentFields というfragmentが2回呼ばれてしまっています。そのため、multiple definitions for fragment "commentFields" というエラーが起こります。

このエラーが発生するのは以下のような場合です。

  • 同じクエリまたはミューテーション内で、同じ名前のFragmentsを2回以上定義している。
  • 異なるクエリやミューテーションであっても、同じ名前のFragmentsが複数存在する。

オーバーフェッチ

2つ目はオーバーフェッチに繋がりうる点です。

GraphQL利用のメリットとして、オーバーフェッチ(リクエスト元で不要なリソースまでフェッチしてしまうこと)を防げることがよく挙げられます。1つのFragmentsを多用すると、クエリによっては Comment 型の一部のフィールドだけ必要であるのにも関わらず、他のフィールドまでフェッチさせることになりかねません。同じ Comment 型に対するFragmentsであったとしても、多くのフィールドをまとめたFragmentsと少しのフィールドを集めたFragmentsに分割することで、帯域を節約する必要があります。

今後の取り組み

今後の取り組みとして、Fragment Colocationも導入したいと考えています。名称からお察しかもしれないですが、これはFragmentsとそれを利用するコンポーネントを「同じ場所に配置する」やり方です。

www.youtube.com

これにより、コンポーネントで必要となるデータが変わった場合でも、同じ場所にあるFragmentsに変更を加えるだけなので、保守性が高まります。Mondでもこれを導入し、さらなる開発体験の向上を進めていこうと考えています。

最後に

以上のように、GraphQL Fragmentsを導入することで、複数のクエリやミューテーション間でフィールドを再利用することが可能になり、コードの可読性や保守性を向上させることができました。ただし、同一クエリ内で同名のFragmentsが複数回呼ばれるとエラーになる点には注意が必要で、オーバーフェッチにも気をつけたいところです。

これを理解し、適切に利用することでGraphQLの効果を最大限発揮することができると思います。

P.S. 弊社(Mond開発チーム)では エンジニアを絶賛募集中 です!まずはカジュアル面談からよろしくお願いいたします。