Saltar al contenido principal

Integración con GraphQL

[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

El framework Ent ofrece soporte para GraphQL utilizando la biblioteca 99designs/gqlgen y proporciona varias integraciones, como:

  1. Generar un esquema GraphQL para nodos y aristas definidos en un esquema Ent.

  2. Resolvedores Query y Mutation auto-generados con integración fluida del framework Relay.

  3. Filtrado, paginación (incluyendo anidada) y soporte compatible con la especificación Relay Cursor Connections.

  4. Recolección eficiente de campos para evitar el problema N+1 sin necesidad de data loaders.

  5. Mutaciones transaccionales para garantizar consistencia ante fallos.

Consulta el tutorial GraphQL en nuestro sitio para más información.

Introducción rápida

Para habilitar la extensión entgql en tu proyecto, necesitas usar el paquete entc (ent codegen) como se describe aquí. Sigue estos 3 pasos para activarla:

1. Crea un nuevo archivo Go llamado ent/entc.go y pega el siguiente contenido:

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. Edita el archivo ent/generate.go para ejecutar el archivo ent/entc.go:

ent/generate.go
package ent

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

Nota que ent/entc.go se ignora mediante una etiqueta de compilación, y se ejecuta mediante el comando go generate a través del archivo generate.go. El ejemplo completo está disponible en el repositorio ent/contrib.

3. Ejecuta la generación de código para tu proyecto Ent:

go generate ./...

Tras ejecutar la generación de código, se añadirán las siguientes funcionalidades a tu proyecto.

API de Nodo

Se crea un nuevo archivo llamado ent/gql_node.go que implementa la interfaz Relay Node.

Para usar la interfaz generada ent.Noder en el resolvedor GraphQL, añade el método Node al resolvedor de consultas y revisa la sección de configuración para entender su uso.

Si usas la opción IDs universales en la migración de esquema, el NodeType se deriva del valor ID y puede usarse así:

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

Sin embargo, si usas un formato personalizado para identificadores globales únicos, puedes controlar el NodeType de esta forma:

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))
}

Configuración GQL

Aquí tienes un ejemplo de configuración para una app de tareas como el que existe en ent/contrib/entgql/todo.

schema:
- todo.graphql

resolver:
# Tell gqlgen to generate resolvers next to the schema file.
layout: follow-schema
dir: .

# gqlgen will search for any type names in the schema in the generated
# ent package. If they match it will use them, otherwise it will new ones.
autobind:
- entgo.io/contrib/entgql/internal/todo/ent

models:
ID:
model:
- github.com/99designs/gqlgen/graphql.IntID
Node:
model:
# ent.Noder is the new interface generated by the Node template.
- entgo.io/contrib/entgql/internal/todo/ent.Noder

Paginación

La plantilla de paginación añade soporte según la Especificación Relay Cursor Connections. Más información sobre Relay puede encontrarse en su sitio web.

Ordenamiento de Conexiones

La opción de ordenamiento permite aplicar un criterio de orden a los bordes devueltos por una conexión.

Notas de Uso

  • Los tipos generados se vincularán automáticamente (autobind) a tipos GraphQL si se mantiene una convención de nombres (ver ejemplo abajo).

  • Los campos de ordenamiento normalmente deben estar indexados para evitar escaneos completos de tablas en la base de datos.

  • Las consultas de paginación pueden ordenarse por un único campo (sin semántica de ordenar por... luego por...).

Ejemplo

Revisemos los pasos necesarios para añadir ordenación a un tipo GraphQL existente. El ejemplo de código se basa en una aplicación de tareas que puede encontrarse en ent/contrib/entql/todo.

Definición de campos de orden en ent/schema

La ordenación puede definirse en cualquier campo comparable de Ent mediante la anotación entgql.Annotation. Nota: el nombre de OrderField debe coincidir con su valor enum en el esquema 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"),
),
}
}

Estos son todos los cambios requeridos en el esquema; asegúrate de ejecutar go generate para aplicarlos.

Definición de tipos de orden en el esquema GraphQL

A continuación necesitamos definir los tipos de orden en el esquema GraphQL:

enum OrderDirection {
ASC
DESC
}

enum TodoOrderField {
CREATED_AT
PRIORITY
STATUS
TEXT
}

input TodoOrder {
direction: OrderDirection!
field: TodoOrderField
}

Nota: la nomenclatura debe seguir la forma <T>OrderField / <T>Order para el autobind con los tipos generados por Ent. Alternativamente, puede usarse la directiva @goModel para vinculación manual de tipos.

Añadir argumento orderBy a la consulta de paginación

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

Estos son todos los cambios necesarios en el esquema GraphQL; ejecutemos ahora la generación de código gqlgen.

Actualizar el resolver subyacente

Dirígete al resolver Todo y actualízalo para pasar el argumento orderBy a la llamada .Paginate():

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),
)
}

Uso en GraphQL

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

Colección de Campos

La plantilla de colección añade soporte para la colección automática de campos GraphQL en bordes Ent mediante carga eager. Esto significa que si una consulta solicita nodos y sus bordes, entgql añadirá automáticamente pasos With<E> a la consulta raíz, resultando en que el cliente ejecutará un número constante de consultas a la base de datos - funcionando de forma recursiva.

Por ejemplo, dada esta consulta GraphQL:

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

El cliente ejecutará 1 consulta para obtener usuarios, 1 para fotos, y otras 2 para publicaciones y sus comentarios (4 en total). Esta lógica funciona tanto para consultas/resolvers raíz como para la API node(s).

Configuración del esquema

Para configurar esta opción en bordes específicos, usa entgql.Annotation como sigue:

func (Todo) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Todo.Type).
Annotations(entgql.Bind()).
From("parent").
// Bind implies the edge name in graphql schema is
// equivalent to the name used in ent schema.
Annotations(entgql.Bind()).
Unique(),
edge.From("owner", User.Type).
Ref("tasks").
// Map edge names as defined in graphql schema.
Annotations(entgql.MapsTo("taskOwner")),
}
}

Uso y Configuración

La extensión GraphQL también genera resolvers de bordes para los nodos en el archivo gql_edge.go así:

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
}

No obstante, si necesitas escribir estos resolvers manualmente, puedes añadir la opción forceResolver a tu esquema GraphQL:

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

Luego puedes implementarlo en tu resolver de tipo.

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

Implementación de Enums

La plantilla enum implementa los métodos MarshalGQL/UnmarshalGQL para enums generados por Ent.

Mutaciones Transaccionales

El handler entgql.Transactioner ejecuta cada mutación GraphQL en una transacción. El cliente inyectado en el resolver es un ent.Client transaccional. Por tanto, el código que use ent.Client no requerirá cambios. Para usarlo, sigue estos pasos:

1. En la inicialización del servidor GraphQL, usa el handler entgql.Transactioner así:

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

2. Luego, en las mutaciones GraphQL, usa el cliente del contexto así:

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)
}

Ejemplos

Actualmente, ent/contrib contiene varios ejemplos:

  1. Un servidor GraphQL completo con una sencilla aplicación Todo que utiliza un campo ID numérico

  2. La misma aplicación Todo del punto 1, pero con tipo UUID para el campo ID

  3. La misma aplicación Todo de los puntos 1 y 2, pero con ULID con prefijo o PULID como campo ID. Este ejemplo es compatible con la API Relay Node porque añade un prefijo a los IDs con el tipo de entidad en lugar de emplear la partición del espacio de IDs en IDs universales.


Ten en cuenta que esta documentación está en desarrollo. Todos los fragmentos de código se encuentran en ent/contrib/entgql, y un ejemplo de aplicación todo puede encontrarse en ent/contrib/entgql/todo.