🚥

interface / union を gqlgen で実装してみる

2023/03/19に公開

はじめに

gqlgen を使って union や interface を実装した事例に関する情報があまりなかったので紹介します。

union / interface は GraphQL における型の一種です。

https://graphql.org/learn/schema/#interfaces

https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/

gqlgen によって 生成される型

GraphQL スキーマ上で union や interface を使った定義をしたときにどういった型が生成されるか紹介します。

下記のような、id で商品 (product) を取得する Query を定義した場合を例に挙げます。

extend type Query {
  product(id: ID!): Result!
}

union Result = Product | ErrorNotUnauthorized | ErrorUnauthenticated

interface Node {
  id: ID!
}

"成功レスポンス"
type Product implements Node {
  id: ID!
  name: String!
  price: Int!
}

interface Error {
  code: String!
  message: String!
}

"異常レスポンス - 認証エラー"
type ErrorNotUnauthorized implements Error {
  code: String!
  message: String!
}

"異常レスポンス - 認可エラー"
type ErrorUnauthenticated implements Error {
  code: String!
  message: String!
}

products の返り値 ResultNode ErrorNotUnauthorized ErrorUnauthenticated の union で定義されています。これらの型はすべて interface を実装しています。成功レスポンスの場合は Node interface を、異常レスポンスの場合は Error interface を実装しています。

union

上記の例では Result 型が union で定義されていましたが、これを gqlgen で Go の型とすると下記のような interface として定義されます。

type Result interface {
	IsResult()
}

type Product struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Price int    `json:"price"`
}

func (Product) IsResult() {}

type ErrorNotUnauthorized struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func (ErrorNotUnauthorized) IsResult() {}

type ErrorUnauthenticated struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func (ErrorUnauthenticated) IsResult() {}

Result 型自体は IsResult() というメソッドが実装されている interface として定義されています。union に含まれる Product ErrorNotUnauthorized ErrorUnauthenticated の 3 つの型は何も処理がない IsResult() メソッドが生えているので、すべて Result interface を満たしています。
Go でこのような union の表現方法はあまり見たことがありませんでしたが、個人的には割とシンプルでわかりやすいと感じました。

resolver は下記のようになります。

func (r *queryResolver) Product(ctx context.Context, id string) (model.Result, error) {
	if auth.IsAuthenticated() { // 認証済みかどうかを返す架空の関数
		return model.ErrorUnauthenticated{
			Code:    http.StatusText(http.StatusUnauthorized),
			Message: "認証してください",
		}, nil
	}

	if auth.IsAuthorized() { // 取得権限があるかどうかを返す架空の関数
		return model.ErrorNotUnauthorized{
			Code:    http.StatusText(http.StatusUnauthorized),
			Message: "権限がありません",
		}, nil
	}

	return model.Product{
		ID:    "hoge",
		Name:  "商品1",
		Price: 1000,
	}, nil
}

Product メソッドの返り値が Result なので Product ErrorNotUnauthorized ErrorUnauthenticated いずれかの構造体を返却することができます。

interface

続いて interface です。

ここでは Error interface とそれを実装する型を紹介します。

type Error interface {
	IsError()
	GetCode() string
	GetMessage() string
}

type ErrorNotUnauthorized struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func (ErrorNotUnauthorized) IsError()                {}
func (this ErrorNotUnauthorized) GetCode() string    { return this.Code }
func (this ErrorNotUnauthorized) GetMessage() string { return this.Message }

type ErrorUnauthenticated struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func (ErrorUnauthenticated) IsError()                {}
func (this ErrorUnauthenticated) GetCode() string    { return this.Code }
func (this ErrorUnauthenticated) GetMessage() string { return this.Message }

union と同じように、interface を満たすか判別するための IsError() メソッドが ErrorUnauthenticatedErrorNotUnauthorized 型それぞれに実装されています。また、Error interface を満たす型が必ず持つフィールドである codemessage を取得する GetCode() GetMessage() も実装されています。

ミドルウェア層などで、特定の interface に対して resolver をまたいで共通で処理を挟みたい場合などに何か使えそうな予感がします。

fyi.

gqlgen が用意してくれているミドルウェアについては下記スクラップで軽く触ってみています。

https://zenn.dev/kmtym1998/scraps/d2827a40e89744#comment-27e79a3929134f

ユースケース

実務では Query / Mutation の返り値は共通の interface を実装した type の union で定義するやり方をとっています。このような方法を採用した背景としては、エラーの型を GraphQL スキーマとしてクライアントに提供したかったという背景があります。

経緯やコンテキスト等の詳細はこちらの記事に書いています。よろしければご参照ください。

https://tech.buysell-technologies.com/entry/2023/02/21/000000

GitHubで編集を提案
株式会社BuySell Technologies

Discussion