Integración con GraphQL
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:
Generar un esquema GraphQL para nodos y aristas definidos en un esquema Ent.
Resolvedores
QueryyMutationauto-generados con integración fluida del framework Relay.Filtrado, paginación (incluyendo anidada) y soporte compatible con la especificación Relay Cursor Connections.
Recolección eficiente de campos para evitar el problema N+1 sin necesidad de data loaders.
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:
// +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:
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:
Un servidor GraphQL completo con una sencilla aplicación Todo que utiliza un campo ID numérico
La misma aplicación Todo del punto 1, pero con tipo UUID para el campo ID
La misma aplicación Todo de los puntos 1 y 2, pero con ULID con prefijo o
PULIDcomo 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.