メインコンテンツへスキップする

GraphQLインテグレーション

The ent framework provides an integration with GraphQL through the 99designs/gqlgen library using the extension option (i.e. it can be extended to support other libraries).

クイックスタート

自身のプロジェクトで entgql 拡張機能を有効にするには、ここで説明されているように、entc (ent codegen) パッケージを使用する必要があります。 以下の3つのステップを踏むことで、あなたのプロジェクトで有効にすることができます。

1. ent/entc.goという名前の新しいGoファイルを作成し、以下の内容を貼り付けます。

ent/entc.go
// +build ignore

package main

import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/contrib/entgql"
)

func main() {
ex, err := entgql.NewExtension()
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
if err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex)); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

2. ent/generate.goファイルを編集して、ent/entc.goファイルを実行します。

ent/generate.go
package ent

//go:generate go run -mod=mod entc.go

なお、ent/entc.goはbuildタグでは無視され、generate.goファイルを通してgo generateコマンドによって実行されます。 完全なサンプルは、ent/contrib リポジトリにあります。

3. あなたのentプロジェクトでコード生成を実行します。

go generate ./...

コード生成を実行すると、以下のアドオンがプロジェクトに追加されます。

Node API

A new file named ent/gql_node.go was created that implements the Relay Node interface.

新しく生成された ent.Noder インターフェースを GraphQL リゾルバ で使用するためには、Node メソッドをクエリリゾルバに追加します。設定 セクションを見て、使い方を理解してください。

スキーマの移行でUniversal IDsオプションを使用している場合、NodeTypeはidの値から派生し、以下のように使用することができます。

func (r *queryResolver) Node(ctx context.Context, id int) (ent.Noder, error) {
return r.client.Noder(ctx, id)
}

しかし、グローバルな一意の識別子にカスタムフォーマットを使用している場合は、以下のようにNodeTypeを制御することができます。

func (r *queryResolver) Node(ctx context.Context, guid string) (ent.Noder, error) {
typ, id := parseGUID(guid)
return r.client.Noder(ctx, id, ent.WithFixedNodeType(typ))
}

GQL設定

ここでは、ent/contrib/entgql/todoにあるようなTodoアプリの設定例を紹介します。

schema:
- todo.graphql

resolver:
# gqlgenに、スキーマファイルの隣にリゾルバを生成するように指示する
layout: follow-schema
dir: .

# gqlgenは生成されたentパッケージ内のスキーマに含まれる型名を
# 検索する。 一致すればそれを使い、そうでなければ新しいものを使う
autobind:
- entgo.io/contrib/entgql/internal/todo/ent

models:
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
Node:
model:
# ent.Noderは、Nodeテンプレートが生成する新しいインターフェースである
- entgo.io/contrib/entgql/internal/todo/ent.Noder

ページネーション

ページネーションテンプレートは、 Relay Cursor Connections Spec に従ってページネーションサポートを追加します。 Relay Spec に関する詳細情報 は ウェブサイト にあります。

コネクションの順序指定

orderingオプションを用いると、コネクションから返されるエッジに順序を適用することができます。

使用上の注意

  • 生成された型は、命名規則が守られていれば、GraphQLの型にautobindされます (以下の例を参照)。
  • 順序指定は、entのフィールドにのみ定義できます (エッジにはありません)。
  • テーブル全体のDBスキャンを避けるために、順序指定フィールドは通常インデックスを付けるべきです。
  • ページネーションクエリは単一のフィールドでソートすることができます (order by ... then by ... の文法はありません)。

使用例

ここでは、既存のGraphQLタイプに順序指定機能を追加するために必要な手順を説明します。 このコード例は、ent/contrib/entql/todoにあるTodoアプリをベースにしています。

ent/schema内で順序フィールドを定義する

entgql.Annotation でアノテーションを付けることで、任意の比較可能なフィールドに順序を定義できます。 与えられた OrderField の名前は、GraphQL スキーマの 列挙値と一致しなければならないことに注意してください。

func (Todo) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now).
Immutable().
Annotations(
entgql.OrderField("CREATED_AT"),
),
field.Enum("status").
NamedValues(
"InProgress", "IN_PROGRESS",
"Completed", "COMPLETED",
).
Annotations(
entgql.OrderField("STATUS"),
),
field.Int("priority").
Default(0).
Annotations(
entgql.OrderField("PRIORITY"),
),
field.Text("text").
NotEmpty().
Annotations(
entgql.OrderField("TEXT"),
),
}
}

以上で、必要なスキーマの変更は完了です。これらを適用するために、go generateを必ず実行してください。

GraphQLスキーマで順序タイプを定義する

次に、GraphQLスキーマ内の順序タイプを定義する必要があります。

enum OrderDirection {
ASC
DESC
}

enum TodoOrderField {
CREATED_AT
PRIORITY
STATUS
TEXT
}

input TodoOrder {
direction: OrderDirection!
field: TodoOrderField
}

生成されたentタイプにautobindするためには、<T>OrderField / <T>Orderの形式で命名しなければならないことに注意してください。 また、@goModelディレクティブを使用して、手動でタイプバインディングを行うこともできます。

ページネーションクエリに orderBy 引数を追加する

type Query {
todos(
after: Cursor
first: Int
before: Cursor
last: Int
orderBy: TodoOrder
): TodoConnection
}

GraphQLスキーマの変更は以上です。 gqlgen のコード生成を実行しましょう。

基礎となるリゾルバを更新する

Todoリゾルバに向かい、.Paginate()の呼び出しにorderByの引数を渡すように更新します。

func (r *queryResolver) Todos(ctx context.Context, after *ent.Cursor, first *int, before *ent.Cursor, last *int, orderBy *ent.TodoOrder) (*ent.TodoConnection, error) {
return r.client.Todo.Query().
Paginate(ctx, after, first, before, last,
ent.WithTodoOrder(orderBy),
)
}

GraphQLで使用する

query {
todos(first: 3, orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
text
}
}
}
}

フィールドのコレクション

コレクションテンプレートでは、eager-loadingを使用したentエッジに対して GraphQLフィールドコレクションの自動サポートが追加されています。 つまり、ノードとそのエッジを求めるクエリの場合、entgqlは自動的にWith<E>ステップをルートクエリに追加します。その結果、クライアントはデータベースに対して一定数のクエリを実行することになります。そしてそれは再帰的に機能します。

例えば、次のようなGraphQLクエリがあるとします。

query {
users(first: 100) {
edges {
node {
photos {
link
}
posts {
content
comments {
content
}
}
}
}
}
}

クライアントは、ユーザー取得用クエリを1つ、写真取得用クエリを1つ、さらに投稿とそれらに対するコメントの取得用クエリを2つ実行します (合計で4つ)。 このロジックは、ルートのクエリ/リゾルバとノードAPIの両方で動作します。

スキーマの設定

このオプションを特定のエッジに設定するためには、以下のようにentgql.Annotationを使用します。

func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Todo.Type).
Annotations(entgql.Bind()).
From("parent").
// バインドは,GraphQLスキーマのエッジ名がentスキーマで
// 使われている名前と同等であることを意味します.
Annotations(entgql.Bind()).
Unique(),
edge.From("owner", User.Type).
Ref("tasks").
// GraphQLスキーマで定義されたエッジ名をマッピングします。
Annotations(entgql.MapsTo("taskOwner")),
}
}

使い方と設定

The GraphQL extension generates also edge-resolvers for the nodes under the gql_edge.go file as follows:

func (t *Todo) Children(ctx context.Context) ([]*Todo, error) {
result, err := t.Edges.ChildrenOrErr()
if IsNotLoaded(err) {
result, err = t.QueryChildren().All(ctx)
}
return result, err
}

しかし、これらのリゾルバを手動で明示的に記述する必要がある場合は、GraphQLスキーマにforceResolverオプションを追加することができます。

type Todo implements Node {
id: ID!
children: [Todo]! @goField(forceResolver: true)
}

そして、それをタイプリゾルバに実装することができます。

func (r *todoResolver) Children(ctx context.Context, obj *ent.Todo) ([]*ent.Todo, error) {
// Do something here.
return obj.Edges.ChildrenOrErr()
}

Enumの実装

enumテンプレートは、entが生成したenumのMarshalGQL/UnmarshalGQLメソッドを実装しています。

トランザクション・ミューテーション

entgql.Transactionerハンドラは、トランザクション内で各GraphQLミューテーションを実行します。 リゾルバ用に注入されるクライアントは、トランザクショナル ent.Clientです。 そのため、ent.Clientを使用しているコードは変更する必要がありません。 使用するためには、以下の手順を踏む必要があります。

1. GraphQLサーバーの初期化では、以下のようにentgql.Transactionerハンドラを使用します。

srv := handler.NewDefaultServer(todo.NewSchema(client))
srv.Use(entgql.Transactioner{TxOpener: client})

2. そして、GraphQLミューテーションでは、コンテキストから得たクライアントを次のように使います。

func (mutationResolver) CreateTodo(ctx context.Context, todo TodoInput) (*ent.Todo, error) {
client := ent.FromContext(ctx)
return client.Todo.
Create().
SetStatus(todo.Status).
SetNillablePriority(todo.Priority).
SetText(todo.Text).
SetNillableParentID(todo.Parent).
Save(ctx)
}

使用例

ent/contribには、現時点でいくつかの例が掲載されています。

  1. 数字のIDフィールドを持つシンプルなTodo Appを備えた完全なGraphQLサーバー
  2. 1のTodo Appと同じですが、IDフィールドにUUID型を使用しています
  3. 1や2と同じTodo Appですが、IDフィールドとしてULIDまたはPULIDをプレフィックス付きで設定します。 この例では、Universal IDsのID空間分割を採用するのではなく、IDの前にエンティティ・タイプを付けることで、Relay Node APIをサポートしています。

このドキュメントは開発中であることにご注意ください。 すべてのコード片は ent/contrib/entgqlにあり、 todo-app の例は ent/contrib/entgql/todo にあります。