学习GraphQL服务器端(第五部分)-引入自定义标量类型

你好。

第五部分讲述的是关于“引入自定义Scala类型”的内容。

关于这一章

在GraphQL查询中,返回的字段必须全部为标量类型。

query {
  user(name: "hsaki") {
    id # ID型(スカラ型)
    name # 文字列型(スカラ)
    projectV2(number: 1) {
      title # 文字列型(スカラ)
    }
  }
}

在Go语言中,内置了五个变量,每个变量都对应着相应的合适数据类型。

    • Int: 符号あり32bit整数

 

    • Float: 符号あり倍精度浮動小数点数

 

    • Boolean: trueまたはfalse

String: UTF‐8の文字列
ID: 実態としてはStringですが、unique identifierとしての機能を持ったフィールドであればこのID型にするのが望ましいです。

但是,除了这个之外,还会有其他需要自己定义和准备标量类型的场景存在。
在本章中,我们将解释如何引入这些自定义标量类型。

自定义标量的具体例。

我将从当前使用的GraphQL架构中介绍使用自定义标量的地方。

具体例一:日期时间

在定义了DateTime类型的情况下,它被用于Repository对象的createdAt字段等,表示日期和时间信息。

schema.graphqls的翻译: 模式.graphqls

scalar DateTime

type Repository implements Node {
	createdAt: DateTime!
}

如果在 gqlgen上自動生成代碼時並不進行任何設置,對於 DateTime 型別的 Go 結構體字段將變為 string 型別。

type Repository struct {
	CreatedAt    string    `json:"createdAt"`
}

如果能在Go语言中将这个转换为time.Time类型将非常便利。这就是引入自定义标量类型的动机。

具体例2:统一资源标识符(URI)

除了DateTime类型之外,还定义了一种称为URI类型的自定义标量类型,用于表示Issue和PR的URL。

schema.graphqls 的另一种说法是什么?

scalar URI

type Issue implements Node {
  url: URI!
}

这种情况下,如果没有特别的设置,这也会在Go端被处理为字符串类型。我希望将其转换为url.URL类型。

type Issue struct {
	URL    string    `json:"url"`
}

自定义Scala的实现

要修改Go类型以与在GraphQL模式中定义的自定义标量类型相对应,有以下三种方法:

    • サードパーティライブラリに用意されたカスタムスカラ対応ロジックを使う

 

    • 独自型にMarshalGQL/UnmarshalGQLメソッドを実装する

MarshalXxx/UnmarshalXxx関数を定義する

选项1 – 使用第三方库中提供的自定义标量兼容类型的方法

实施方式 (shí shī shì)

在 github.com/99designs/gqlgen 上,已经提供了常用的自定义标量的逻辑处理。
包括将自定义定义的标量与 Go 的 time.Time 类型相匹配的逻辑也不例外。

为了将当前模式中的DateTime类型转换为Go的time.Time类型,您可以在gqlgen的配置文件gqlgen.yml中添加以下设置。

models:
  ID:
    model:
      - github.com/99designs/gqlgen/graphql.ID
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.Int64
      - github.com/99designs/gqlgen/graphql.Int32
  Int:
    model:
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.Int64
      - github.com/99designs/gqlgen/graphql.Int32s
+  DateTime:
+    model:
+      - github.com/99designs/gqlgen/graphql.Time

在修改了gqlgen.yml之后,执行gqlgen generate命令,应该可以确认生成的模型类型在models_gen.go中已经发生了以下的变化。

type Repository struct {
-	CreatedAt    string                 `json:"createdAt"`
+	CreatedAt    time.Time              `json:"createdAt"`
}

在github.com/99designs/gqlgen上提供的自定义标量支持类型。

类似于 `github.com/99designs/gqlgen/graphql.Time` 的方式,可以通过在 gqlgen.yml 中指定来改变自定义标量的 Go 类型,还有其他的机制存在。

Screenshot 2023-11-06 at 8.18.36 PM.png

选项1: 实现自定义的MarshalGQL/UnmarshalGQL方法

如果你想让自定义的标量类型适应github.com/99designs/gqlgen没有准备好的类型,比如URI类型,在这种情况下也是存在的。

模式.graphqls

scalar URI

希望使Go语言的结构体与GraphQL的自定义标量类型相对应,必须满足graphql.Marshaler接口和graphql.Unmarshaler接口。

type Marshaler interface {
	MarshalGQL(w io.Writer)
}

type Unmarshaler interface {
	UnmarshalGQL(v interface{}) error
}

要将自定义标量类型URI与本次定义的MyURL类型对应起来,首先需要在MyURL类型中实现MarshalGQL/UnmarshalGQL方法。

type MyURL struct {
	url.URL
}

// MarshalGQL implements the graphql.Marshaler interface
func (u MyURL) MarshalGQL(w io.Writer) {
	io.WriteString(w, fmt.Sprintf(`"%s"`, u.URL.String()))
}

// UnmarshalGQL implements the graphql.Unmarshaler interface
func (u *MyURL) UnmarshalGQL(v interface{}) error {
	switch v := v.(type) {
	case string:
		if result, err := url.Parse(v); err != nil {
			return err
		} else {
			u = &MyURL{*result}
		}
		return nil
	case []byte:
		result := &url.URL{}
		if err := result.UnmarshalBinary(v); err != nil {
			return err
		}
		u = &MyURL{*result}
		return nil
	default:
		return fmt.Errorf("%T is not a url.URL", v)
	}
}

在gqlgen.yml文件中配置使用刚刚创建的MyURL类型,并运行gqlgen generate命令重新生成代码,可以确认利用了URI类型的Issue.URL字段的定义已经发生了变化。

models:
+  URI:
+    model:
+      - github.com/saki-engineering/graphql-sample/graph/model.MyURL
type Issue struct {
-	URL          string                   `json:"url"`
+	URL          MyURL                    `json:"url"`
}

选项1:第三种方法-自定义MarshalXxx/UnmarshalXxx函数。

对于方法2,由于自定义了名为MyURL的类型来映射自定义标量URI类型,因此可以自由地添加MarshalGQL/UnmarshalGQL方法。
然而,如果想将自定义标量URI类型映射到标准包内的url.URL类型,方法2是无法使用的。
这是因为无法为已经在标准包net/url中定义的现有类型url.URL添加方法实现。

在某些情况下,存在着无法根据自己的判断添加MarshalGQL/UnmarshalGQL方法的现有第三方包型和标准包型的模式。在这种情况下,我们需要准备MarshalXxx/UnmarshalXxx函数。

func MarshalURI(u url.URL) graphql.Marshaler {
	return graphql.WriterFunc(func(w io.Writer) {
		io.WriteString(w, fmt.Sprintf(`"%s"`, u.String()))
	})
}

func UnmarshalURI(v interface{}) (url.URL, error) {
	switch v := v.(type) {
	case string:
		u, err := url.Parse(v)
		if err != nil {
			return url.URL{}, err
		}
		return *u, nil
	case []byte:
		u := &url.URL{}
		if err := u.UnmarshalBinary(v); err != nil {
			return url.URL{}, err
		}
		return *u, nil
	default:
		return url.URL{}, fmt.Errorf("%T is not a url.URL", v)
	}
}

在定义了MarshalURI/UnmarshalURI函数后,只需在gqlgen.yml中按照以下设置并重新生成代码,就可以将自定义类型URI与标准包中的url.URL类型成功关联。

models:
+  URI:
+    model:
-      - github.com/saki-engineering/graphql-sample/graph/model.MyURL
+      - github.com/saki-engineering/graphql-sample/graph/model.URI
type Issue struct {
-	URL          string                   `json:"url"`
+	URL          url.URL                  `json:"url"`
}

下一个部分

下面是有关“解决方案的实施-应用篇”的内容。

今天就到这里吧。

请多关照。

广告
将在 10 秒后关闭
bannerAds