Recolección de Campos en GraphQL
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
En esta sección, continuamos nuestro ejemplo de GraphQL explicando cómo Ent implementa la Recolección de Campos en GraphQL para nuestro esquema GraphQL y resuelve el "Problema N+1" en nuestros resolvers.
Clonar el código (opcional)
El código de este tutorial está disponible en github.com/a8m/ent-graphql-example, con etiquetas (usando Git) en cada paso. Si quieres saltarte la configuración básica y comenzar con la versión inicial del servidor GraphQL, puedes clonar el repositorio así:
git clone git@github.com:a8m/ent-graphql-example.git
cd ent-graphql-example
go run ./cmd/todo/
Problema
El "problema N+1" en GraphQL ocurre cuando un servidor ejecuta consultas de base de datos innecesarias para obtener asociaciones de nodos (ej. aristas) cuando podría evitarlas. La cantidad potencial de consultas ejecutadas (N+1) depende del número de nodos devueltos por la consulta raíz, sus asociaciones, y así recursivamente. Esto puede convertirse en un número muy grande (mucho mayor que N+1).
Intentemos explicarlo con la siguiente consulta:
query {
users(first: 50) {
edges {
node {
photos {
link
}
posts {
content
comments {
content
}
}
}
}
}
}
En la consulta anterior, queremos obtener los primeros 50 usuarios con sus fotos y publicaciones, incluyendo sus comentarios.
En la solución ingenua (el caso problemático), el servidor obtendrá los primeros 50 usuarios en una consulta, luego para cada usuario
ejecutará una consulta para sus fotos (50 consultas) y otra para sus publicaciones (50). Supongamos que cada usuario tiene exactamente
10 publicaciones. Por tanto, para cada publicación (de cada usuario), el servidor ejecutará otra consulta para obtener sus comentarios (500).
Esto significa que tendremos 1+50+50+500=601 consultas en total.

Solución de Ent
La extensión de Ent para recolección de campos añade soporte automático para la recolección de campos en GraphQL
de asociaciones (ej. aristas) usando carga anticipada. Si una consulta solicita nodos y sus aristas,
entgql automáticamente añadirá pasos With<E> a la consulta raíz, haciendo que el cliente ejecute
un número constante de consultas a la base de datos - y funciona recursivamente.
En la consulta GraphQL anterior, 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 de nodo(s).
Ejemplo
Para este ejemplo, deshabilitaremos la recolección automática de campos, cambiaremos el ent.Client a modo depuración
en el resolver Todos, y reiniciaremos nuestro servidor GraphQL:
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().
+ return r.client.Debug().Todo.Query().
Paginate(ctx, after, first, before, last,
ent.WithTodoOrder(orderBy),
)
}
Ejecutamos la consulta GraphQL del tutorial de paginación añadiendo la arista parent al resultado:
query {
todos(last: 10, orderBy: {direction: DESC, field: TEXT}) {
edges {
node {
id
text
parent {
id
}
}
cursor
}
}
}
Revisa la salida del proceso y verás que el servidor ejecutó 11 consultas a la base de datos: 1 para obtener los últimos 10 elementos todo, y otras 10 para obtener el padre de cada elemento:
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` ORDER BY `id` ASC LIMIT 11
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` JOIN (SELECT `todo_parent` FROM `todos` WHERE `id` = ?) AS `t1` ON `todos`.`id` = `t1`.`todo_parent` LIMIT 2
Veamos cómo Ent resuelve automáticamente nuestro problema: al definir una arista en Ent, entgql la vincula automáticamente
con su uso en GraphQL y genera resolvers de aristas para los nodos en el archivo gql_edge.go:
func (t *Todo) Children(ctx context.Context) ([]*Todo, error) {
if fc := graphql.GetFieldContext(ctx); fc != nil && fc.Field.Alias != "" {
result, err = t.NamedChildren(graphql.GetFieldContext(ctx).Field.Alias)
} else {
result, err = t.Edges.ChildrenOrErr()
}
if IsNotLoaded(err) {
result, err = t.QueryChildren().All(ctx)
}
return result, err
}
Si revisamos nuevamente la salida del proceso sin deshabilitar la recolección de campos, veremos que esta vez el servidor ejecutó solo dos consultas a la base de datos: una para obtener los últimos 10 elementos todo, y otra para obtener el elemento padre de cada elemento todo devuelto en la primera consulta.
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority`, `todos`.`todo_parent` FROM `todos` ORDER BY `id` DESC LIMIT 11
SELECT DISTINCT `todos`.`id`, `todos`.`text`, `todos`.`created_at`, `todos`.`status`, `todos`.`priority` FROM `todos` WHERE `todos`.`id` IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Si tienes problemas para ejecutar este ejemplo, ve a la primera sección, clona el código y ejecuta el ejemplo.
Mapeo de Campos
La función entgql.MapsTo permite añadir un mapeo personalizado de campos/bordes entre el esquema de Ent y el esquema de GraphQL. Esto es útil cuando quieres exponer un campo o borde con un nombre diferente en el esquema de GraphQL. Por ejemplo:
// One to one mapping.
field.Int("priority").
Annotations(
entgql.OrderField("PRIORITY_ORDER"),
entgql.MapsTo("priorityOrder"),
)
// Multiple GraphQL fields can map to the same Ent field.
field.Int("category_id").
Annotations(
entgql.MapsTo("categoryID", "category_id", "categoryX"),
)
Campos de Resolver Recolectados
La anotación entgql.CollectedFor te permite especificar que un campo debe recolectarse automáticamente cuando se consultan ciertos campos de resolver de GraphQL (campos extendidos). Esto resulta útil cuando tienes campos de resolver que dependen de valores de campos subyacentes de Ent.
field.String("name").
Optional().
Annotations(
entgql.CollectedFor("uppercaseName"),
)
Si Ent desconoce el mapeo entre un campo de resolver y su campo subyacente de Ent, y encuentra un campo desconocido en la consulta, consultará todos los campos de la base de datos para asegurarse de que el resolver tenga los datos necesarios.
¡Bien hecho! Al usar la recopilación automática de campos en nuestra definición de esquema de Ent, hemos logrado mejorar enormemente la eficiencia de las consultas GraphQL en nuestra aplicación. En la siguiente sección, aprenderemos a hacer que las mutaciones de GraphQL sean transaccionales.