GraphQL: Global Object Identification

GraphQL のベストプラクティスとして公式サイトに掲載されている Global Object Identification について。

概要

先日 Zenn で記事を見かけて気づいたのだが、GraphQL のベストプラクティスとして公式サイトに掲載されている

Global Object Identification

について完全に無視していたのでできる範囲で沿うようにしてみた。

書かれている内容はざっくり以下の通り。

  1. id という名前の non-null ID だけをフィールドとして持つ Node という interface を定義する
  2. サーバーは node(id: ID!): Node というクエリを実装する
  3. Node を実装するオブジェクトが持つ id: ID! フィールドはグローバルに一意な値とする。

以上により node クエリでオブジェクトを再取得できると共にキャッシュ管理が容易になるとされている。

この内 2. に関しては権限制御などが複雑なので実装を見送った。
キャッシュ管理に ID を使用しているわけでもないのだが、ID がグローバルに一意であること自体は特にデメリットを感じないので 1. と 3. は対応した。

ULID の使用

私はバックエンドの DB に MySQL を使用しており、元々は auto_increment な整数を ID として利用していた。
一応 UUID のような文字列 ID も検討はしたのだが、パフォーマンス的にも文字列を ID にするより整数のほうが有利というのも聞いたため導入を見送っていた。
しかしこれでは異なるテーブルから取得したオブジェクトは ID が重複するので駄目である。

そのため一意な ID を生成するようにしたいが、MySQL の場合は ID が完全にランダムになると性能劣化を引き起こすことが知られている。
UUID を使用する場合、MySQL 8 以降なら UUID_TO_BIN() の swap_flag を有効にすることで時系列ソート可能な形となり、性能劣化を引き起こさずに済む。

ただ今回は ULID を使うことにした。

ULID は文字列にエンコードすると 26 文字の ID で、前半がタイムスタンプ、後半が乱数という構造となっている。
そのため時系列順にソート可能な ID となり、UUID のような性能劣化を防ぐことができる。

ULID はバイナリ型で保持したほうが良いとする意見もあったが、私の今回の使用方法では

  • クライアントは ID を文字列で扱うため、クエリの引数として渡されてくる ID が文字列であり、文字列のまま直接 SQL に値を渡せるほうが便利である
  • ULID の Go 実装は ULID を ULID 型で生成してくれて、その実体は []byte だが、そのままでは []byte として扱うことはできない。結局変換の一手間を挟む必要がある。

ということから、取り扱いの簡便さを優先して文字列で保持することにした。

ULID は ID でソート可能なので、今まで auto_increment な整数として組んでいた SQL はそのまま使い続けることができた。

以上