Saltar al contenido principal

· 9 min de lectura
[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 →

Cuando Ariel lanzó Ent v0.10.0 a finales de enero, introdujo un nuevo motor de migraciones para Ent basado en otro proyecto de código abierto llamado Atlas.

Inicialmente, Atlas admitía un estilo de gestión de esquemas de bases de datos que llamamos "migraciones declarativas". Con las migraciones declarativas, el estado deseado del esquema de la base de datos se proporciona como entrada al motor de migraciones, que planifica y ejecuta un conjunto de acciones para cambiar la base de datos a su estado deseado. Este enfoque se ha popularizado en el campo de la infraestructura nativa en la nube gracias a proyectos como Kubernetes y Terraform. Funciona muy bien en muchos casos, de hecho ha servido muy bien al framework Ent durante los últimos años. Sin embargo, las migraciones de bases de datos son un tema muy delicado, y muchos proyectos requieren un enfoque más controlado.

Por esta razón, la mayoría de las soluciones estándar de la industria, como Flyway, Liquibase, o golang-migrate/migrate (común en el ecosistema de Go), admiten un flujo de trabajo que denominan "migraciones versionadas".

Con las migraciones versionadas (a veces llamadas "migraciones basadas en cambios"), en lugar de describir el estado deseado ("cómo debe verse la base de datos"), describes los cambios en sí mismos ("cómo alcanzar el estado"). La mayoría de las veces esto se hace creando un conjunto de archivos SQL que contienen las sentencias necesarias. A cada archivo se le asigna una versión única y una descripción de los cambios. Herramientas como las mencionadas anteriormente pueden interpretar los archivos de migración y aplicar (algunos de) ellos en el orden correcto para transicionar a la estructura de base de datos deseada.

En esta publicación, quiero mostrar un nuevo tipo de flujo de trabajo de migración que se ha añadido recientemente a Atlas y Ent. Lo llamamos "creación de migraciones versionadas" y es un intento de combinar la simplicidad y expresividad del enfoque declarativo con la seguridad y claridad de las migraciones versionadas. Con la creación de migraciones versionadas, los usuarios siguen declarando su estado deseado y usando el motor de Atlas para planificar una migración segura desde el estado existente al nuevo estado. Sin embargo, en lugar de vincular la planificación y la ejecución, se escribe en un archivo que puede guardarse en control de código fuente, ajustarse manualmente y revisarse en procesos normales de revisión de código.

Como ejemplo, demostraré el flujo de trabajo con golang-migrate/migrate.

Empezando

Lo primero que debes hacer es asegurarte de tener una versión actualizada de Ent:

go get -u entgo.io/ent@master

Hay dos formas de hacer que Ent genere archivos de migración para cambios de esquema. La primera es usar un cliente de Ent instanciado y la segunda es generar los cambios a partir de un gráfico de esquema analizado. Esta publicación tomará el segundo enfoque. Si quieres aprender a usar el primero, puedes consultar la documentación.

Generación de archivos de migración versionados

Como ya hemos activado la función de migraciones versionadas, vamos a crear un esquema pequeño y generar el conjunto inicial de archivos de migración. Considera el siguiente esquema para un proyecto nuevo de Ent:

ent/schema/user.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)

// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("username"),
}
}

// Indexes of the User.
func (User) Indexes() []ent.Index {
return []ent.Index{
index.Fields("username").Unique(),
}
}

Como mencioné antes, queremos usar el gráfico de esquema analizado para calcular la diferencia entre nuestro esquema y la base de datos conectada. Aquí tienes un ejemplo de un contenedor Docker de MySQL (semi)persistente para usar si quieres seguir el ejemplo:

docker run --rm --name ent-versioned-migrations --detach --env MYSQL_ROOT_PASSWORD=pass --env MYSQL_DATABASE=ent -p 3306:3306 mysql

Una vez que hayas terminado, puedes detener el contenedor y eliminar todos los recursos con docker stop ent-versioned-migrations.

Ahora, vamos a crear una pequeña función que cargue el gráfico de esquema y genere los archivos de migración. Crea un nuevo archivo Go llamado main.go y copia el siguiente contenido:

main.go
package main

import (
"context"
"log"
"os"

"ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/schema"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
_ "github.com/go-sql-driver/mysql"
)

func main() {
// We need a name for the new migration file.
if len(os.Args) < 2 {
log.Fatalln("no name given")
}
// Create a local migration directory.
dir, err := migrate.NewLocalDir("migrations")
if err != nil {
log.Fatalln(err)
}
// Load the graph.
graph, err := entc.LoadGraph("./ent/schema", &gen.Config{})
if err != nil {
log.Fatalln(err)
}
tbls, err := graph.Tables()
if err != nil {
log.Fatalln(err)
}
// Open connection to the database.
drv, err := sql.Open("mysql", "root:pass@tcp(localhost:3306)/ent")
if err != nil {
log.Fatalln(err)
}
// Inspect the current database state and compare it with the graph.
m, err := schema.NewMigrate(drv, schema.WithDir(dir))
if err != nil {
log.Fatalln(err)
}
if err := m.NamedDiff(context.Background(), os.Args[1], tbls...); err != nil {
log.Fatalln(err)
}
}

Ahora solo tenemos que crear el directorio de migraciones y ejecutar el archivo Go anterior:

mkdir migrations
go run -mod=mod main.go initial

Ahora verás dos archivos nuevos en el directorio migrations: <timestamp>_initial.down.sql y <timestamp>_initial.up.sql. Los archivos x.up.sql se utilizan para crear la versión x de la base de datos y los x.down.sql para revertir a la versión anterior.

<timestamp>_initial.up.sql
CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(191) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `user_username` (`username`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
<timestamp>_initial.down.sql
DROP TABLE `users`;

Aplicar migraciones

Para aplicar estas migraciones en tu base de datos, instala la herramienta golang-migrate/migrate como se describe en su README. Luego ejecuta el siguiente comando para verificar que todo funcione correctamente.

migrate -help
Usage: migrate OPTIONS COMMAND [arg...]
migrate [ -version | -help ]

Options:
-source Location of the migrations (driver://url)
-path Shorthand for -source=file://path
-database Run migrations against this database (driver://url)
-prefetch N Number of migrations to load in advance before executing (default 10)
-lock-timeout N Allow N seconds to acquire database lock (default 15)
-verbose Print verbose logging
-version Print version
-help Print usage

Commands:
create [-ext E] [-dir D] [-seq] [-digits N] [-format] NAME
Create a set of timestamped up/down migrations titled NAME, in directory D with extension E.
Use -seq option to generate sequential up/down migrations with N digits.
Use -format option to specify a Go time format string.
goto V Migrate to version V
up [N] Apply all or N up migrations
down [N] Apply all or N down migrations
drop Drop everything inside database
force V Set version V but don't run migration (ignores dirty state)
version Print current migration version

Ahora podemos ejecutar nuestra migración inicial y sincronizar la base de datos con nuestro esquema:

migrate -source 'file://migrations' -database 'mysql://root:pass@tcp(localhost:3306)/ent' up
<timestamp>/u initial (349.256951ms)

Flujo de trabajo

Para demostrar el flujo de trabajo habitual con migraciones versionadas, editaremos nuestro esquema para generar los cambios de migración correspondientes y crearemos manualmente archivos de migración para poblar la base de datos con datos iniciales. Primero añadiremos un esquema Group y una relación muchos-a-muchos con el esquema User existente, luego crearemos un grupo de administración con un usuario administrador. Realiza estos cambios:

ent/schema/user.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)

// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("username"),
}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("groups", Group.Type).
Ref("users"),
}
}

// Indexes of the User.
func (User) Indexes() []ent.Index {
return []ent.Index{
index.Fields("username").Unique(),
}
}
ent/schema/group.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)

// Group holds the schema definition for the Group entity.
type Group struct {
ent.Schema
}

// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}

// Indexes of the Group.
func (Group) Indexes() []ent.Index {
return []ent.Index{
index.Fields("name").Unique(),
}
}

Una vez actualizado el esquema, crea un nuevo conjunto de archivos de migración.

go run -mod=mod main.go add_group_schema

Nuevamente aparecerán dos archivos en el directorio migrations: <timestamp>_add_group_schema.down.sql y <timestamp>_add_group_schema.up.sql.

<timestamp>_add_group_schema.up.sql
CREATE TABLE `groups` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(191) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `group_name` (`name`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
CREATE TABLE `group_users` (`group_id` bigint NOT NULL, `user_id` bigint NOT NULL, PRIMARY KEY (`group_id`, `user_id`), CONSTRAINT `group_users_group_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `group_users_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE) CHARSET utf8mb4 COLLATE utf8mb4_bin;
<timestamp>_add_group_schema.down.sql
DROP TABLE `group_users`;
DROP TABLE `groups`;

Ahora puedes editar los archivos generados para añadir los datos iniciales o crear nuevos archivos para ello. Elegiré la segunda opción:

migrate create -format unix -ext sql -dir migrations seed_admin
[...]/ent-versioned-migrations/migrations/<timestamp>_seed_admin.up.sql
[...]/ent-versioned-migrations/migrations/<timestamp>_seed_admin.down.sql

Ahora puedes editar esos archivos y añadir sentencias para crear un Group de administración y un User administrador.

migrations/<timestamp>_seed_admin.up.sql
INSERT INTO `groups` (`id`, `name`) VALUES (1, 'Admins');
INSERT INTO `users` (`id`, `username`) VALUES (1, 'admin');
INSERT INTO `group_users` (`group_id`, `user_id`) VALUES (1, 1);
migrations/<timestamp>_seed_admin.down.sql
DELETE FROM `group_users` where `group_id` = 1 and `user_id` = 1;
DELETE FROM `groups` where id = 1;
DELETE FROM `users` where id = 1;

Aplica las migraciones nuevamente y habrás terminado:

migrate -source file://migrations -database 'mysql://root:pass@tcp(localhost:3306)/ent' up
<timestamp>/u add_group_schema (417.434415ms)
<timestamp>/u seed_admin (674.189872ms)

Conclusión

En esta entrada hemos demostrado el flujo de trabajo con Ent Versioned Migrations usando golang-migate/migrate. Creamos un esquema de ejemplo, generamos los archivos de migración correspondientes y aprendimos a aplicarlos. Ahora conocemos el flujo básico y cómo añadir migraciones personalizadas.

¿Tienes preguntas? ¿Necesitas ayuda para empezar? Únete a nuestro servidor de Discord o canal de Slack.

[Para más noticias y actualizaciones de Ent:]

· 11 min de lectura
[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 →

A finales de 2021 anunciamos que Ent había obtenido una nueva extensión oficial para generar un documento OpenAPI Specification completamente compatible: entoas.

Hoy nos complace anunciar una nueva extensión diseñada para funcionar con entoas: ogent. Utiliza la potencia de ogen (sitio web) para proporcionar una implementación de tipo seguro y sin reflexión del documento OpenAPI Specification generado por entoas.

ogen es un generador de código Go con opiniones definidas para documentos OpenAPI Specification v3. ogen genera implementaciones tanto de servidor como de cliente para cualquier documento OpenAPI Specification. Lo único que debe hacer el usuario es implementar una interfaz para acceder a la capa de datos de su aplicación. ogen incluye muchas características interesantes, como la integración con OpenTelemetry. Asegúrate de probarlo y mostrarle algo de aprecio.

La extensión presentada en este artículo sirve de puente entre Ent y el código generado por ogen. Utiliza la configuración de entoas para generar las partes faltantes del código de ogen.

El siguiente diagrama muestra cómo interactúa Ent con las extensiones entoas y ogent, y el papel de ogen en este proceso.

Diagram

Diagram

Si eres nuevo en Ent y quieres aprender más sobre cómo conectarte a diferentes tipos de bases de datos, ejecutar migraciones o trabajar con entidades, visita el Tutorial de Configuración.

El código de este artículo está disponible en los ejemplos de los módulos.

Empezando

[]

Aunque Ent es compatible con versiones de Go 1.16+, ogen requiere que tengas al menos la versión 1.17.

Para utilizar la extensión ogent, emplea el paquete entc (ent codegen) como se describe aquí. Primero instala las extensiones entoas y ogent en tu módulo de Go:

go get ariga.io/ogent@main

Ahora sigue estos dos pasos para habilitarlas y configurar Ent para trabajar con las extensiones:

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

ent/entc.go
//go:build ignore

package main

import (
"log"

"ariga.io/ogent"
"entgo.io/contrib/entoas"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/ogen-go/ogen"
)

func main() {
spec := new(ogen.Spec)
oas, err := entoas.NewExtension(entoas.Spec(spec))
if err != nil {
log.Fatalf("creating entoas extension: %v", err)
}
ogent, err := ogent.NewExtension(spec)
if err != nil {
log.Fatalf("creating ogent extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ogent, oas))
if 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

¡Con estos pasos completados, todo está listo para generar un documento OAS e implementar código de servidor a partir de tu esquema!

Generar un servidor HTTP API CRUD

El primer paso para crear el servidor HTTP API es diseñar un grafo de esquemas Ent. Por brevedad, aquí tienes un esquema de ejemplo:

ent/schema/todo.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

// Todo holds the schema definition for the Todo entity.
type Todo struct {
ent.Schema
}

// Fields of the Todo.
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.String("title"),
field.Bool("done"),
}
}

El código anterior muestra la forma "Ent" de describir un grafo de esquemas. En este caso particular, hemos creado una entidad todo (tarea pendiente).

Ahora ejecuta el generador de código:

go generate ./...

Deberías ver varios archivos generados por el generador de código de Ent. El archivo llamado ent/openapi.json ha sido generado por la extensión entoas. Aquí tienes un vistazo:

ent/openapi.json
{
"info": {
"title": "Ent Schema API",
"description": "This is an auto generated API description made out of an Ent schema definition",
"termsOfService": "",
"contact": {},
"license": {
"name": ""
},
"version": "0.0.0"
},
"paths": {
"/todos": {
"get": {
[...]
Swagger Editor Example

Swagger Editor Example

Sin embargo, este artículo se centra en la implementación del servidor, por lo que nos interesa especialmente el directorio ent/ogent. Todos los archivos que terminan en _gen.go son generados por ogen. El archivo oas_server_gen.go contiene la interfaz que los usuarios de ogen deben implementar para ejecutar el servidor.

ent/ogent/oas_server_gen.go
// Handler handles operations described by OpenAPI v3 specification.
type Handler interface {
// CreateTodo implements createTodo operation.
//
// Creates a new Todo and persists it to storage.
//
// POST /todos
CreateTodo(ctx context.Context, req CreateTodoReq) (CreateTodoRes, error)
// DeleteTodo implements deleteTodo operation.
//
// Deletes the Todo with the requested ID.
//
// DELETE /todos/{id}
DeleteTodo(ctx context.Context, params DeleteTodoParams) (DeleteTodoRes, error)
// ListTodo implements listTodo operation.
//
// List Todos.
//
// GET /todos
ListTodo(ctx context.Context, params ListTodoParams) (ListTodoRes, error)
// ReadTodo implements readTodo operation.
//
// Finds the Todo with the requested ID and returns it.
//
// GET /todos/{id}
ReadTodo(ctx context.Context, params ReadTodoParams) (ReadTodoRes, error)
// UpdateTodo implements updateTodo operation.
//
// Updates a Todo and persists changes to storage.
//
// PATCH /todos/{id}
UpdateTodo(ctx context.Context, req UpdateTodoReq, params UpdateTodoParams) (UpdateTodoRes, error)
}

ogent añade una implementación para ese manejador en el archivo ogent.go. Para ver cómo definir qué rutas generar y qué relaciones precargar, consulta la documentación de entoas.

Este es un ejemplo de una ruta READ generada:

// ReadTodo handles GET /todos/{id} requests.
func (h *OgentHandler) ReadTodo(ctx context.Context, params ReadTodoParams) (ReadTodoRes, error) {
q := h.client.Todo.Query().Where(todo.IDEQ(params.ID))
e, err := q.Only(ctx)
if err != nil {
switch {
case ent.IsNotFound(err):
return &R404{
Code: http.StatusNotFound,
Status: http.StatusText(http.StatusNotFound),
Errors: rawError(err),
}, nil
case ent.IsNotSingular(err):
return &R409{
Code: http.StatusConflict,
Status: http.StatusText(http.StatusConflict),
Errors: rawError(err),
}, nil
default:
// Let the server handle the error.
return nil, err
}
}
return NewTodoRead(e), nil
}

Ejecutar el servidor

El siguiente paso es crear un archivo main.go y conectar todos los componentes para crear un servidor de aplicaciones que sirva la API Todo. La siguiente función main inicializa una base de datos SQLite en memoria, ejecuta las migraciones para crear las tablas necesarias y sirve la API descrita en ent/openapi.json en localhost:8080:

main.go
package main

import (
"context"
"log"
"net/http"

"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
"<your-project>/ent"
"<your-project>/ent/ogent"
_ "github.com/mattn/go-sqlite3"
)

func main() {
// Create ent client.
client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal(err)
}
// Run the migrations.
if err := client.Schema.Create(context.Background(), schema.WithAtlas(true)); err != nil {
log.Fatal(err)
}
// Start listening.
srv, err := ogent.NewServer(ogent.NewOgentHandler(client))
if err != nil {
log.Fatal(err)
}
if err := http.ListenAndServe(":8080", srv); err != nil {
log.Fatal(err)
}
}

Tras ejecutar el servidor con go run -mod=mod main.go, ya puedes interactuar con la API.

Primero, creemos un nuevo Todo. Para demostración, no enviaremos cuerpo en la solicitud:

curl -X POST -H "Content-Type: application/json" localhost:8080/todos
{
"error_message": "body required"
}

Como ves, ogen maneja este caso porque entoas marcó el cuerpo como obligatorio al intentar crear un nuevo recurso. Intentémoslo de nuevo, esta vez proporcionando un cuerpo:

curl -X POST -H "Content-Type: application/json" -d '{"title":"Give ogen and ogent a Star on GitHub"}'  localhost:8080/todos
{
"error_message": "decode CreateTodo:application/json request: invalid: done (field required)"
}

¡Ups! ¿Qué salió mal? ogen te respalda: el campo done es obligatorio. Para solucionarlo, ve a tu definición de esquema y marca el campo done como opcional:

ent/schema/todo.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

// Todo holds the schema definition for the Todo entity.
type Todo struct {
ent.Schema
}

// Fields of the Todo.
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.String("title"),
field.Bool("done").
Optional(),
}
}

Como hemos modificado nuestra configuración, debemos regenerar el código y reiniciar el servidor:

go generate ./...
go run -mod=mod main.go

Ahora, al intentar crear el Todo de nuevo:

curl -X POST -H "Content-Type: application/json" -d '{"title":"Give ogen and ogent a Star on GitHub"}'  localhost:8080/todos
{
"id": 1,
"title": "Give ogen and ogent a Star on GitHub",
"done": false
}

¡Voilà! Hay un nuevo elemento Todo en la base de datos.

Supongamos que has completado tu Todo y has destacado tanto ogen como ogent (¡realmente deberías!). Marca el todo como completado con una solicitud PATCH:

curl -X PATCH -H "Content-Type: application/json" -d '{"done":true}'  localhost:8080/todos/1
{
"id": 1,
"title": "Give ogen and ogent a Star on GitHub",
"done": true
}

Añadir endpoints personalizados

Como ves, el Todo ahora está marcado como completado. Pero sería mejor tener una ruta adicional para marcar un Todo como hecho: PATCH todos/:id/done. Para lograrlo debemos hacer dos cosas: documentar la nueva ruta en nuestro documento OAS e implementar la ruta. Abordemos lo primero usando el constructor de mutaciones de entoas. Edita tu archivo ent/entc.go y añade la descripción de la ruta:

ent/entc.go
//go:build ignore

package main

import (
"log"

"entgo.io/contrib/entoas"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/ariga/ogent"
"github.com/ogen-go/ogen"
)

func main() {
spec := new(ogen.Spec)
oas, err := entoas.NewExtension(
entoas.Spec(spec),
entoas.Mutations(func(_ *gen.Graph, spec *ogen.Spec) error {
spec.AddPathItem("/todos/{id}/done", ogen.NewPathItem().
SetDescription("Mark an item as done").
SetPatch(ogen.NewOperation().
SetOperationID("markDone").
SetSummary("Marks a todo item as done.").
AddTags("Todo").
AddResponse("204", ogen.NewResponse().SetDescription("Item marked as done")),
).
AddParameters(ogen.NewParameter().
InPath().
SetName("id").
SetRequired(true).
SetSchema(ogen.Int()),
),
)
return nil
}),
)
if err != nil {
log.Fatalf("creating entoas extension: %v", err)
}
ogent, err := ogent.NewExtension(spec)
if err != nil {
log.Fatalf("creating ogent extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ogent, oas))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

Tras regenerar el código (go generate ./...), debería aparecer una nueva entrada en ent/openapi.json:

"/todos/{id}/done": {
"description": "Mark an item as done",
"patch": {
"tags": [
"Todo"
],
"summary": "Marks a todo item as done.",
"operationId": "markDone",
"responses": {
"204": {
"description": "Item marked as done"
}
}
},
"parameters": [
{
"name": "id",
"in": "path",
"schema": {
"type": "integer"
},
"required": true
}
]
}
Custom Endpoint

Custom Endpoint

El archivo ent/ogent/oas_server_gen.go generado por ogen también reflejará los cambios:

ent/ogent/oas_server_gen.go
// Handler handles operations described by OpenAPI v3 specification.
type Handler interface {
// CreateTodo implements createTodo operation.
//
// Creates a new Todo and persists it to storage.
//
// POST /todos
CreateTodo(ctx context.Context, req CreateTodoReq) (CreateTodoRes, error)
// DeleteTodo implements deleteTodo operation.
//
// Deletes the Todo with the requested ID.
//
// DELETE /todos/{id}
DeleteTodo(ctx context.Context, params DeleteTodoParams) (DeleteTodoRes, error)
// ListTodo implements listTodo operation.
//
// List Todos.
//
// GET /todos
ListTodo(ctx context.Context, params ListTodoParams) (ListTodoRes, error)
// MarkDone implements markDone operation.
//
// PATCH /todos/{id}/done
MarkDone(ctx context.Context, params MarkDoneParams) (MarkDoneNoContent, error)
// ReadTodo implements readTodo operation.
//
// Finds the Todo with the requested ID and returns it.
//
// GET /todos/{id}
ReadTodo(ctx context.Context, params ReadTodoParams) (ReadTodoRes, error)
// UpdateTodo implements updateTodo operation.
//
// Updates a Todo and persists changes to storage.
//
// PATCH /todos/{id}
UpdateTodo(ctx context.Context, req UpdateTodoReq, params UpdateTodoParams) (UpdateTodoRes, error)
}

Si intentas ejecutar el servidor ahora, el compilador de Go mostrará errores porque el generador de código ogent no sabe implementar la nueva ruta. Debes hacerlo manualmente. Reemplaza el main.go actual con este archivo para implementar el nuevo método:

main.go
package main

import (
"context"
"log"
"net/http"

"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
"github.com/ariga/ogent/example/todo/ent"
"github.com/ariga/ogent/example/todo/ent/ogent"
_ "github.com/mattn/go-sqlite3"
)

type handler struct {
*ogent.OgentHandler
client *ent.Client
}

func (h handler) MarkDone(ctx context.Context, params ogent.MarkDoneParams) (ogent.MarkDoneNoContent, error) {
return ogent.MarkDoneNoContent{}, h.client.Todo.UpdateOneID(params.ID).SetDone(true).Exec(ctx)
}

func main() {
// Create ent client.
client, err := ent.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal(err)
}
// Run the migrations.
if err := client.Schema.Create(context.Background(), schema.WithAtlas(true)); err != nil {
log.Fatal(err)
}
// Create the handler.
h := handler{
OgentHandler: ogent.NewOgentHandler(client),
client: client,
}
// Start listening.
srv := ogent.NewServer(h)
if err := http.ListenAndServe(":8180", srv); err != nil {
log.Fatal(err)
}
}

Al reiniciar el servidor, puedes enviar esta solicitud para marcar un todo como completado:

curl -X PATCH localhost:8180/todos/1/done

Próximas mejoras

Hay mejoras planeadas para ogent, destacando especialmente una forma segura de añadir capacidades de filtrado a las rutas LIST mediante código generado con tipos seguros. Queremos escuchar primero tus comentarios.

Conclusión

En este artículo anunciamos ogent, el generador oficial de implementaciones para documentos de Especificación OpenAPI generados por entoas. Esta extensión aprovecha el poder de ogen, un generador de código Go extremadamente potente y rico en funciones para documentos OpenAPI v3, para proporcionar servidores de API RESTful HTTP listos para usar y extensibles.

Ten en cuenta que tanto ogen como entoas/ogent aún no han alcanzado su primera versión principal y están en desarrollo activo. No obstante, la API puede considerarse estable.

¿Tienes preguntas? ¿Necesitas ayuda para empezar? Únete a nuestro servidor de Discord o canal de Slack.

[Para más noticias y actualizaciones de Ent:]

· 6 min de lectura
[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 →

Querida comunidad,

Me complace anunciar el lanzamiento de la próxima versión de Ent: v0.10. Han pasado casi seis meses desde v0.9.1, así que naturalmente esta versión incluye un montón de novedades. Aún así, quería dedicar tiempo para hablar sobre una mejora importante en la que hemos estado trabajando durante los últimos meses: un nuevo motor de migraciones.

Presentamos: Atlas

El actual motor de migraciones de Ent es genial y hace cosas bastante interesantes que nuestra comunidad ha estado usando en producción durante años, pero con el tiempo empezaron a acumularse problemas que no podíamos resolver con la arquitectura existente. Además, consideramos que los marcos de migración de bases de datos actuales dejan mucho que desear. Hemos aprendido tanto como industria sobre la gestión segura de cambios en sistemas productivos en la última década con principios como Infraestructura-como-Código y gestión declarativa de configuración, que simplemente no existían cuando se concibieron la mayoría de estos proyectos.

Al ver que estos problemas eran bastante genéricos y relevantes para aplicaciones independientemente del framework o lenguaje de programación en el que estuvieran escritas, vimos la oportunidad de solucionarlos como infraestructura común que cualquier proyecto podría usar. Por esta razón, en lugar de simplemente reescribir el motor de migraciones de Ent, decidimos extraer la solución a un nuevo proyecto de código abierto, Atlas (GitHub).

Atlas se distribuye como una herramienta CLI que utiliza un nuevo DDL basado en HCL (similar a Terraform), pero también puede usarse como un paquete de Go. Al igual que Ent, Atlas tiene licencia Apache 2.0.

Finalmente, tras mucho trabajo y pruebas, la integración de Atlas para Ent está lista para usarse. Esta es una gran noticia para muchos de nuestros usuarios que abrieron issues (como #1652, #1631, #1625, #1546 y #1845) que no podían resolverse adecuadamente con el sistema de migraciones existente, pero que ahora se solucionan con el motor Atlas.

Como con cualquier cambio sustancial, usar Atlas como motor de migraciones en tu proyecto es actualmente opcional. En un futuro próximo, cambiaremos a un modo de exclusión voluntaria (opt-out), y finalmente dejaremos obsoleto el motor actual. Naturalmente, esta transición se hará gradualmente, y avanzaremos según recibamos indicaciones positivas de la comunidad.

Cómo empezar con las migraciones Atlas para Ent

Primero, actualiza a la última versión de Ent:

go get entgo.io/ent@v0.10.0

Luego, para ejecutar una migración con el motor Atlas, usa la opción WithAtlas(true).

package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
"entgo.io/ent/dialect/sql/schema"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
err = client.Schema.Create(ctx, schema.WithAtlas(true))
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

¡Y eso es todo!

Una de las grandes mejoras del motor Atlas sobre el código existente de Ent es su estructura en capas, que separa claramente entre inspección (entender el estado actual de una base de datos), diferenciación (calcular la diferencia entre el estado actual y el deseado), planificación (calcular un plan concreto para solucionar la diferencia) y aplicación. Este diagrama muestra cómo Ent utiliza Atlas:

atlas-migration-process

Además de las opciones estándar (ej. WithDropColumn, WithGlobalUniqueID), la integración de Atlas proporciona opciones adicionales para intervenir en los pasos de migración del esquema.

Aquí tenéis dos ejemplos que muestran cómo conectar con los pasos Diff y Apply de Atlas.

package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
"ariga.io/atlas/sql/migrate"
atlas "ariga.io/atlas/sql/schema"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
err := client.Schema.Create(
ctx,
// Hook into Atlas Diff process.
schema.WithDiffHook(func(next schema.Differ) schema.Differ {
return schema.DiffFunc(func(current, desired *atlas.Schema) ([]atlas.Change, error) {
// Before calculating changes.
changes, err := next.Diff(current, desired)
if err != nil {
return nil, err
}
// After diff, you can filter
// changes or return new ones.
return changes, nil
})
}),
// Hook into Atlas Apply process.
schema.WithApplyHook(func(next schema.Applier) schema.Applier {
return schema.ApplyFunc(func(ctx context.Context, conn dialect.ExecQuerier, plan *migrate.Plan) error {
// Example to hook into the apply process, or implement
// a custom applier. For example, write to a file.
//
// for _, c := range plan.Changes {
// fmt.Printf("%s: %s", c.Comment, c.Cmd)
// if err := conn.Exec(ctx, c.Cmd, c.Args, nil); err != nil {
// return err
// }
// }
//
return next.Apply(ctx, conn, plan)
})
}),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

Qué viene después: v0.11

Sé que hemos tardado en sacar esta versión, pero la próxima está a la vuelta de la esquina. Esto es lo que viene en la v0.11:

  • Soporte para esquemas de relaciones/edges - permitiendo adjuntar campos de metadatos a relaciones.

  • Reimplementación de la integración con GraphQL para ser totalmente compatible con la especificación Relay. Soporte para generar recursos GraphQL (esquemas o servidores completos) a partir de esquemas Ent.

  • Implementación del "Autor de Migraciones": las librerías de Atlas incluyen infraestructura para crear directorios de migraciones "versionadas", como se usa comúnmente en muchos frameworks de migraciones (como Flyway, Liquibase, go-migrate, etc.). Muchos usuarios han creado soluciones para integrarse con este tipo de sistemas, y planeamos usar Atlas para ofrecer una infraestructura sólida para estos flujos.

  • Hooks de consulta (interceptores) - actualmente los hooks solo están soportados para Mutaciones. Muchos usuarios han solicitado añadir soporte también para operaciones de lectura.

  • Relaciones polimórficas - El issue sobre añadir soporte para polimorfismo lleva abierto más de un año. Con la llegada de los Tipos Genéricos en Go 1.18, queremos reabrir el debate sobre una posible implementación usando esta característica.

Para finalizar

Además del emocionante anuncio sobre el nuevo motor de migraciones, esta versión es enorme en tamaño y contenido, con 199 commits de 42 colaboradores únicos. Ent es un esfuerzo comunitario que mejora cada día gracias a todos vosotros. Así que un enorme gracias y reconocimiento infinito a todos los que habéis participado en esta release (ordenados alfabéticamente):

attackordie, bbkane, bodokaiser, cjraa, dakimura, dependabot, EndlessIdea, ernado, evanlurvey, freb, genevieve, giautm, grevych, hedwigz, heliumbrain, hilakashai, HurSungYun, idc77, isoppp, JeremyV2014, Laconty, lenuse, masseelch, mattn, mookjp, msal4, naormatania, odeke-em, peanut-cc, posener, RiskyFeryansyahP, rotemtam, s-takehana, sadmansakib, sashamelentyev, seiichi1101, sivchari, storyicon, tarrencev, ThinkontrolSY, timoha, vecpeng, yonidavidson y zeevmoney.

Un saludo, Ariel

[Para más noticias y actualizaciones de Ent:]

· 16 min de lectura
[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 →

GraphQL es un lenguaje de consulta para APIs HTTP que ofrece una interfaz estáticamente tipada para representar eficientemente las complejas jerarquías de datos actuales. Una forma de usar GraphQL es importar una biblioteca que implemente un servidor GraphQL al que se registran resolvers personalizados que gestionan la interfaz con la base de datos. Una alternativa es utilizar un servicio cloud de GraphQL para implementar el servidor GraphQL y registrar funciones serverless en la nube como resolvers. Entre los muchos beneficios de los servicios cloud, una de las mayores ventajas prácticas es la independencia y capacidad de composición de los resolvers. Por ejemplo, podemos escribir un resolver para una base de datos relacional y otro para una base de datos de búsqueda.

Consideraremos una configuración de este tipo usando Amazon Web Services (AWS) a continuación. En particular, utilizaremos AWS AppSync como servicio cloud de GraphQL y AWS Lambda para ejecutar un resolver de base de datos relacional, que implementaremos usando Go con Ent como framework de entidades. Comparado con Nodejs, el runtime más popular para AWS Lambda, Go ofrece tiempos de arranque más rápidos, mayor rendimiento y, desde mi punto de vista, una mejor experiencia para el desarrollador. Como complemento adicional, Ent presenta un enfoque innovador para el acceso tipado a bases de datos relacionales que, en mi opinión, no tiene igual en el ecosistema de Go. En conclusión, ejecutar Ent con AWS Lambda como resolvers de AWS AppSync es una configuración extremadamente potente para afrontar las exigentes necesidades actuales de las APIs.

En las próximas secciones, configuraremos GraphQL en AWS AppSync y la función AWS Lambda que ejecuta Ent. Posteriormente, propondremos una implementación en Go que integra Ent con el manejador de eventos de AWS Lambda, seguida de una prueba rápida de la función con Ent. Finalmente, la registraremos como fuente de datos en nuestra API de AWS AppSync y configuraremos los resolvers, que definen el mapeo de las peticiones GraphQL a eventos de AWS Lambda. Ten en cuenta que este tutorial requiere una cuenta de AWS y la URL de una base de datos Postgres accesible públicamente, lo cual podría generar costes.

Configuración del esquema de AWS AppSync

Para configurar el esquema GraphQL en AWS AppSync, inicia sesión en tu cuenta de AWS y selecciona el servicio AppSync en la barra de navegación. La página de inicio del servicio AppSync debería mostrarte un botón "Create API", que puedes pulsar para llegar a la página "Getting Started":

Screenshot of getting started with AWS AppSync from scratch

Getting started from scratch with AWS AppSync

En el panel superior que dice "Customize your API or import from Amazon DynamoDB", selecciona la opción "Build from scratch" y haz clic en el botón "Start" de ese panel. Ahora deberías ver un formulario donde puedes introducir el nombre de la API. Para este tutorial, escribimos "Todo", como se muestra en la captura de pantalla inferior, y pulsamos el botón "Create".

Screenshot of creating a new AWS AppSync API resource

Creating a new API resource in AWS AppSync

Tras crear la API de AppSync, deberías ver una página de inicio que muestra un panel para definir el esquema, otro para consultar la API y un tercero sobre cómo integrar AppSync en tu aplicación, como se captura en la siguiente captura de pantalla.

Screenshot of the landing page of the AWS AppSync API

Landing page of the AWS AppSync API

Haz clic en el botón "Edit Schema" del primer panel y reemplaza el esquema anterior con el siguiente esquema GraphQL:

input AddTodoInput {
title: String!
}

type AddTodoOutput {
todo: Todo!
}

type Mutation {
addTodo(input: AddTodoInput!): AddTodoOutput!
removeTodo(input: RemoveTodoInput!): RemoveTodoOutput!
}

type Query {
todos: [Todo!]!
todo(id: ID!): Todo
}

input RemoveTodoInput {
todoId: ID!
}

type RemoveTodoOutput {
todo: Todo!
}

type Todo {
id: ID!
title: String!
}

schema {
query: Query
mutation: Mutation
}

Tras reemplazar el esquema, se ejecuta una breve validación y podrás hacer clic en el botón "Save Schema" en la esquina superior derecha, obteniendo la siguiente vista:

Screenshot AWS AppSync: Final GraphQL schema for AWS AppSync API

Final GraphQL schema of AWS AppSync API

Si enviáramos peticiones GraphQL a nuestra API de AppSync, esta devolvería errores porque no se han adjuntado resolvers al esquema. Configuraremos los resolvers después de desplegar la función de Ent mediante AWS Lambda.

Explicar en detalle el esquema GraphQL actual excede el alcance de este tutorial.
En resumen, implementa:

  • Operación de listar todos con Query.todos
  • Lectura individual con Query.todo
  • Creación con Mutation.createTodo
  • Eliminación con Mutation.deleteTodo

Esta API GraphQL equivale a un diseño REST simple para recurso /todos, usando:

  • GET /todos
  • GET /todos/:id
  • POST /todos
  • DELETE /todos/:id

Para detalles del diseño (argumentos, retornos de los objetos Query y Mutation), sigo prácticas de la API GraphQL de GitHub.

Configurar AWS Lambda

Con la API AppSync lista, pasamos a la función AWS Lambda para ejecutar Ent.
Desde la barra de navegación, accedemos al servicio AWS Lambda que muestra nuestras funciones:

Screenshot of AWS Lambda landing page listing functions

AWS Lambda landing page showing functions.

Hacemos clic en "Crear función" (esquina superior derecha) y seleccionamos "Crear desde cero".
Asignamos nombre "ent", runtime "Go 1.x", y pulsamos "Crear función".
Veremos entonces la página principal de nuestra función "ent":

Screenshot of AWS Lambda landing page listing functions

AWS Lambda function overview of the Ent function.

Antes de subir el binario compilado, ajustamos configuraciones:
Primero, cambiamos el manejador predeterminado de hello a main (nombre del binario Go):

Screenshot of AWS Lambda landing page listing functions

AWS Lambda runtime settings of Ent function.

Segundo, añadimos variable de entorno DATABASE_URL con parámetros de conexión:

Screenshot of AWS Lambda landing page listing functions

AWS Lambda environment variables settings of Ent function.

Para abrir conexión a la base de datos, usamos un DSN, ej: postgres://username:password@hostname/dbname.
AWS Lambda cifra automáticamente las variables de entorno, siendo un mecanismo seguro para credenciales.
Alternativas:

  • AWS Secrets Manager para rotación dinámica de credenciales
  • AWS IAM para autorización de base de datos

Si creaste tu base de datos Postgres en AWS RDS, el nombre de usuario y nombre de base de datos predeterminados son postgres. La contraseña se puede restablecer modificando la instancia de AWS RDS.

Configurar Ent y desplegar en AWS Lambda

Ahora revisamos, compilamos y desplegamos el binario Go en la función "ent".
Código completo en bodokaiser/entgo-aws-appsync.

Primero, creamos un directorio vacío al que cambiamos:

mkdir entgo-aws-appsync
cd entgo-aws-appsync

En segundo lugar, iniciamos un nuevo módulo de Go para contener nuestro proyecto:

go mod init entgo-aws-appsync

Tercero, creamos el esquema Todo incorporando las dependencias de Ent:

go run -mod=mod entgo.io/ent/cmd/ent new Todo

y añade el campo title:

ent/schema/todo.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

// Todo holds the schema definition for the Todo entity.
type Todo struct {
ent.Schema
}

// Fields of the Todo.
func (Todo) Fields() []ent.Field {
return []ent.Field{
field.String("title"),
}
}

// Edges of the Todo.
func (Todo) Edges() []ent.Edge {
return nil
}

Finalmente, realizamos la generación de código de Ent:

go generate ./ent

Con Ent, escribimos un conjunto de funciones de resolución que implementan las operaciones de creación, lectura y eliminación en los todos:

internal/handler/resolver.go
package resolver

import (
"context"
"fmt"
"strconv"

"entgo-aws-appsync/ent"
"entgo-aws-appsync/ent/todo"
)

// TodosInput is the input to the Todos query.
type TodosInput struct{}

// Todos queries all todos.
func Todos(ctx context.Context, client *ent.Client, input TodosInput) ([]*ent.Todo, error) {
return client.Todo.
Query().
All(ctx)
}

// TodoByIDInput is the input to the TodoByID query.
type TodoByIDInput struct {
ID string `json:"id"`
}

// TodoByID queries a single todo by its id.
func TodoByID(ctx context.Context, client *ent.Client, input TodoByIDInput) (*ent.Todo, error) {
tid, err := strconv.Atoi(input.ID)
if err != nil {
return nil, fmt.Errorf("failed parsing todo id: %w", err)
}
return client.Todo.
Query().
Where(todo.ID(tid)).
Only(ctx)
}

// AddTodoInput is the input to the AddTodo mutation.
type AddTodoInput struct {
Title string `json:"title"`
}

// AddTodoOutput is the output to the AddTodo mutation.
type AddTodoOutput struct {
Todo *ent.Todo `json:"todo"`
}

// AddTodo adds a todo and returns it.
func AddTodo(ctx context.Context, client *ent.Client, input AddTodoInput) (*AddTodoOutput, error) {
t, err := client.Todo.
Create().
SetTitle(input.Title).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating todo: %w", err)
}
return &AddTodoOutput{Todo: t}, nil
}

// RemoveTodoInput is the input to the RemoveTodo mutation.
type RemoveTodoInput struct {
TodoID string `json:"todoId"`
}

// RemoveTodoOutput is the output to the RemoveTodo mutation.
type RemoveTodoOutput struct {
Todo *ent.Todo `json:"todo"`
}

// RemoveTodo removes a todo and returns it.
func RemoveTodo(ctx context.Context, client *ent.Client, input RemoveTodoInput) (*RemoveTodoOutput, error) {
t, err := TodoByID(ctx, client, TodoByIDInput{ID: input.TodoID})
if err != nil {
return nil, fmt.Errorf("failed querying todo with id %q: %w", input.TodoID, err)
}
err = client.Todo.
DeleteOne(t).
Exec(ctx)
if err != nil {
return nil, fmt.Errorf("failed deleting todo with id %q: %w", input.TodoID, err)
}
return &RemoveTodoOutput{Todo: t}, nil
}

El uso de structs de entrada para las funciones resolutoras permite mapear los argumentos de las solicitudes GraphQL. El uso de structs de salida permite devolver múltiples objetos para operaciones más complejas.

Para mapear el evento Lambda a una función de resolución, implementamos un Handler que realiza el mapeo según un campo action en el evento:

internal/handler/handler.go
package handler

import (
"context"
"encoding/json"
"fmt"
"log"

"entgo-aws-appsync/ent"
"entgo-aws-appsync/internal/resolver"
)

// Action specifies the event type.
type Action string

// List of supported event actions.
const (
ActionMigrate Action = "migrate"

ActionTodos = "todos"
ActionTodoByID = "todoById"
ActionAddTodo = "addTodo"
ActionRemoveTodo = "removeTodo"
)

// Event is the argument of the event handler.
type Event struct {
Action Action `json:"action"`
Input json.RawMessage `json:"input"`
}

// Handler handles supported events.
type Handler struct {
client *ent.Client
}

// Returns a new event handler.
func New(c *ent.Client) *Handler {
return &Handler{
client: c,
}
}

// Handle implements the event handling by action.
func (h *Handler) Handle(ctx context.Context, e Event) (interface{}, error) {
log.Printf("action %s with payload %s\n", e.Action, e.Input)

switch e.Action {
case ActionMigrate:
return nil, h.client.Schema.Create(ctx)
case ActionTodos:
var input resolver.TodosInput
return resolver.Todos(ctx, h.client, input)
case ActionTodoByID:
var input resolver.TodoByIDInput
if err := json.Unmarshal(e.Input, &input); err != nil {
return nil, fmt.Errorf("failed parsing %s params: %w", ActionTodoByID, err)
}
return resolver.TodoByID(ctx, h.client, input)
case ActionAddTodo:
var input resolver.AddTodoInput
if err := json.Unmarshal(e.Input, &input); err != nil {
return nil, fmt.Errorf("failed parsing %s params: %w", ActionAddTodo, err)
}
return resolver.AddTodo(ctx, h.client, input)
case ActionRemoveTodo:
var input resolver.RemoveTodoInput
if err := json.Unmarshal(e.Input, &input); err != nil {
return nil, fmt.Errorf("failed parsing %s params: %w", ActionRemoveTodo, err)
}
return resolver.RemoveTodo(ctx, h.client, input)
}

return nil, fmt.Errorf("invalid action %q", e.Action)
}

Además de las acciones del resolutor, también hemos añadido una acción de migración, que ofrece una forma conveniente de exponer las migraciones de la base de datos.

Finalmente, debemos registrar una instancia del tipo Handler en la librería de AWS Lambda.

lambda/main.go
package main

import (
"database/sql"
"log"
"os"

"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"

"github.com/aws/aws-lambda-go/lambda"
_ "github.com/jackc/pgx/v4/stdlib"

"entgo-aws-appsync/ent"
"entgo-aws-appsync/internal/handler"
)

func main() {
// open the database connection using the pgx driver
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatalf("failed opening database connection: %v", err)
}

// initiate the ent database client for the Postgres database
client := ent.NewClient(ent.Driver(entsql.OpenDB(dialect.Postgres, db)))
defer client.Close()

// register our event handler to listen on Lambda events
lambda.Start(handler.New(client).Handle)
}

El cuerpo de la función main se ejecuta cuando un AWS Lambda realiza un cold start. Después del cold start, una función Lambda se considera "warm" y solo se ejecuta el código del manejador de eventos, lo que optimiza la eficiencia de las ejecuciones Lambda.

Compilamos y desplegamos el código Go con:

GOOS=linux go build -o main ./lambda
zip function.zip main
aws lambda update-function-code --function-name ent --zip-file fileb://function.zip

El primer comando crea un binario compilado llamado main. El segundo comando comprime el binario en un archivo ZIP, requisito de AWS Lambda. El tercer comando reemplaza el código de la función Lambda llamada ent con el nuevo archivo ZIP. Si trabajas con múltiples cuentas de AWS, usa el parámetro --profile <your aws profile>.

Tras desplegar correctamente la función Lambda, abre la pestaña "Test" de la función "ent" en la consola web e invócala con una acción "migrate":

Screenshot of invoking the Ent Lambda with a migrate action

Invoking Lambda with a "migrate" action

Si tiene éxito, verás una caja de confirmación verde y podrás probar el resultado de una acción "todos":

Screenshot of invoking the Ent Lambda with a todos action

Invoking Lambda with a "todos" action

Si las pruebas fallan, lo más probable es que tengas un problema con la conexión a tu base de datos.

Configuración de resolvers en AWS AppSync

Con la función "ent" desplegada correctamente, nos queda registrarla como fuente de datos en nuestra API de AppSync y configurar los resolvers del esquema para mapear las peticiones de AppSync a eventos Lambda. Primero, abre nuestra API de AWS AppSync en la consola web y dirígete a "Data Sources" en el panel de navegación izquierdo.

Screenshot of the list of data sources registered to the AWS AppSync API

List of data sources registered to the AWS AppSync API

Haz clic en el botón "Create data source" en la esquina superior derecha para comenzar a registrar la función "ent" como fuente de datos:

Screenshot registering the ent Lambda as data source to the AWS AppSync API

Registering the ent Lambda as data source to the AWS AppSync API

Ahora, abre el esquema GraphQL de la API de AppSync y busca el tipo Query en la barra lateral derecha. Haz clic en el botón "Attach" junto al tipo Query.Todos:

Screenshot attaching a resolver to Query type in the AWS AppSync API

Attaching a resolver for the todos Query in the AWS AppSync API

En la vista del resolver para Query.todos, selecciona la función Lambda como fuente de datos, activa la opción de plantilla de mapeo de solicitud,

Screenshot configuring the resolver mapping for the todos Query in the AWS AppSync API

Configuring the resolver mapping for the todos Query in the AWS AppSync API

y copia la siguiente plantilla:

Query.todos
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"action": "todos"
}
}

Repite el mismo procedimiento para los tipos Query y Mutation restantes:

Query.todo
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"action": "todo",
"input": $util.toJson($context.args.input)
}
}
Mutation.addTodo
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"action": "addTodo",
"input": $util.toJson($context.args.input)
}
}
Mutation.removeTodo
{
"version" : "2017-02-28",
"operation": "Invoke",
"payload": {
"action": "removeTodo",
"input": $util.toJson($context.args.input)
}
}

Las plantillas de mapeo de solicitud nos permiten construir los objetos de evento con los que invocamos las funciones Lambda. A través del objeto $context, tenemos acceso a la solicitud GraphQL y la sesión de autenticación. Además, es posible organizar múltiples resolvers secuencialmente y referenciar sus salidas mediante el objeto $context. En principio, también es posible definir plantillas de mapeo de respuesta. Sin embargo, en la mayoría de los casos es suficiente devolver el objeto de respuesta "tal cual".

Probando AppSync con el explorador de consultas

La forma más sencilla de probar la API es usando el Query Explorer en AWS AppSync. Alternativamente, se puede registrar una API key en la configuración de AppSync y usar cualquier cliente GraphQL estándar.

Primero creemos un todo con el título foo:

mutation MyMutation {
addTodo(input: {title: "foo"}) {
todo {
id
title
}
}
}
Screenshot of an executed addTodo Mutation using the AppSync Query Explorer

"addTodo" Mutation using the AppSync Query Explorer

Al solicitar una lista de todos debería devolver un solo todo con título foo:

query MyQuery {
todos {
title
id
}
}
Screenshot of an executed addTodo Mutation using the AppSync Query Explorer

"addTodo" Mutation using the AppSync Query Explorer

Solicitar el todo foo por su ID también debería funcionar:

query MyQuery {
todo(id: "1") {
title
id
}
}
Screenshot of an executed addTodo Mutation using the AppSync Query Explorer

"addTodo" Mutation using the AppSync Query Explorer

Conclusión

Hemos desplegado con éxito una API GraphQL serverless para gestionar tareas simples usando AWS AppSync, AWS Lambda y Ent. En particular, proporcionamos instrucciones paso a paso para configurar AppSync y Lambda a través de la consola web. Además, discutimos una propuesta sobre cómo estructurar nuestro código Go.

No cubrimos pruebas ni configuración de infraestructura de bases de datos en AWS. Estos aspectos son más desafiantes en el paradigma serverless que en el tradicional. Por ejemplo, cuando muchas funciones Lambda se inician en frío simultáneamente, agotamos rápidamente el pool de conexiones de la base de datos y necesitamos un proxy de base de datos. Además, debemos repensar las pruebas ya que solo tenemos acceso a pruebas locales y de extremo a extremo, pues no podemos ejecutar servicios cloud fácilmente de forma aislada.

Sin embargo, el servidor GraphQL propuesto escala bien para las demandas complejas de aplicaciones reales, beneficiándose de la infraestructura serverless y la agradable experiencia de desarrollo con Ent.

¿Tienes preguntas? ¿Necesitas ayuda para comenzar? Únete a nuestro servidor de Discord o canal de Slack.

[Para más noticias y actualizaciones de Ent:]

· 10 min de lectura
[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 →

Llevo años desarrollando software, pero hasta hace poco no sabía qué era un ORM. Aprendí muchas cosas durante mi grado en Ingeniería Informática, pero el Mapeo Objeto-Relacional no fue una de ellas; estaba demasiado centrado en construir cosas con bits y bytes como para preocuparme por algo de tan alto nivel. No debería sorprender entonces que, cuando me encargaron ayudar a construir una aplicación web distribuida, terminara completamente fuera de mi zona de confort.

Una de las dificultades de desarrollar software para otros es que no puedes ver dentro de su cabeza. Los requisitos no siempre están claros y hacer preguntas solo te permite entender una parte de lo que realmente buscan. A veces, simplemente tienes que construir un prototipo y mostrarlo para obtener comentarios útiles.

El problema de este enfoque, claro está, es que desarrollar prototipos lleva tiempo y necesitas pivotar con frecuencia. Si eres como yo y no sabías qué era un ORM, malgastarías mucho tiempo en tareas sencillas pero tediosas:

  1. Redefinir el modelo de datos con los nuevos comentarios del cliente.

  2. Recrear la base de datos de pruebas.

  3. Reescribir las sentencias SQL para interactuar con la base de datos.

  4. Redefinir la interfaz gRPC entre los servicios backend y frontend.

  5. Rediseñar el frontend y la interfaz web.

  6. Mostrárselo al cliente y obtener comentarios

  7. Repetir

Cientos de horas de trabajo solo para descubrir que todo necesita reescribirse. ¡Qué frustración! Imagina mi alivio (y también mi vergüenza) cuando un desarrollador senior me preguntó por qué no usaba un ORM como Ent.

Descubriendo Ent

Solo me llevó un día reimplementar nuestro modelo de datos actual con Ent. ¡No podía creer que hubiera estado haciendo todo este trabajo manualmente cuando existía un framework así! La integración gRPC mediante entproto fue la guinda del pastel: podía realizar operaciones CRUD básicas sobre gRPC simplemente añadiendo unas anotaciones a mi esquema. Esto me permitía saltarme todos los pasos entre la definición del modelo de datos y el rediseño de la interfaz web. Pero había un problema para mi caso de uso: ¿cómo obtener los detalles de las entidades a través de gRPC sin conocer sus IDs de antemano? Veía que Ent podía consultar todo, pero ¿dónde estaba el método GetAll de entproto?

Convirtiéndome en colaborador de código abierto

¡Me sorprendió descubrir que no existía! Podría haber implementado la función en un servicio aparte para mi proyecto, pero parecía un método lo suficientemente genérico como para ser útil en general. Durante años había querido encontrar un proyecto de código abierto al que pudiera contribuir de forma significativa: ¡esta parecía la oportunidad perfecta!

Así que, tras hurgar en el código fuente de entproto hasta altas horas de la madrugada, ¡logré implementar la función! Sintiéndome satisfecho, abrí un pull request y me fui a dormir, sin ser consciente de la experiencia de aprendizaje que acababa de buscar.

Por la mañana, me desperté con la decepción de que Rotem había cerrado mi pull request, pero con una invitación a seguir colaborando para refinar la idea. La razón del cierre era obvia: mi implementación de GetAll era peligrosa. Devolver todos los datos de una tabla solo es viable si la tabla es pequeña. ¡Exponer esta interfaz en tablas grandes podría tener resultados desastrosos!

Generación opcional de métodos de servicio

Mi solución fue hacer que el método GetAll fuera opcional pasando un argumento a entproto.Service(). Esto proporciona control sobre si se expone esta funcionalidad. Decidimos que era una característica deseable, pero que debería ser más genérica. ¿Por qué GetAll debería recibir un trato especial solo por haber sido añadido al final? Sería mejor si todos los métodos pudieran generarse opcionalmente. Algo como:

entproto.Service(entproto.Methods(entproto.Create | entproto.Get))

Sin embargo, para mantener la compatibilidad con versiones anteriores, una anotación entproto.Service() vacía también debería generar todos los métodos. No soy experto en Go, y la única forma que conocía para hacer esto era con una función variádica:

func Service(methods ...Method)

El problema con este enfoque es que solo puedes tener un tipo de argumento de longitud variable. ¿Qué pasa si quisiéramos añadir más opciones a la anotación del servicio más adelante? Aquí fue donde conocí el poderoso patrón de diseño de opciones funcionales:

// ServiceOption configures the entproto.Service annotation.
type ServiceOption func(svc *service)

// Service annotates an ent.Schema to specify that protobuf service generation is required for it.
func Service(opts ...ServiceOption) schema.Annotation {
s := service{
Generate: true,
}
for _, apply := range opts {
apply(&s)
}
// Default to generating all methods
if s.Methods == 0 {
s.Methods = MethodAll
}
return s
}

Este enfoque recibe un número variable de funciones que se llaman para establecer opciones en una estructura, en este caso, nuestra anotación de servicio. Con este método, podemos implementar cualquier cantidad de funciones de opciones además de Methods. ¡Muy interesante!

List: El GetAll Superior

Con la generación opcional de métodos resuelta, pudimos centrarnos nuevamente en añadir GetAll. ¿Cómo podríamos implementar este método de forma segura? Rotem sugirió basar el método en la Propuesta de Mejora de API (AIP) de Google para List, AIP-132. Este enfoque permite a un cliente recuperar todas las entidades, pero divide la recuperación en páginas. Como ventaja adicional, ¡también suena mejor que "GetAll"!

Solicitud List

Con este diseño, un mensaje de solicitud se vería así:

message ListUserRequest {
int32 page_size = 1;

string page_token = 2;

View view = 3;

enum View {
VIEW_UNSPECIFIED = 0;

BASIC = 1;

WITH_EDGE_IDS = 2;
}
}

Tamaño de página

El campo page_size permite al cliente especificar el número máximo de entradas que desea recibir en el mensaje de respuesta, con un tamaño máximo de página de 1000. Esto elimina el problema de devolver más resultados de los que el cliente puede manejar en la implementación inicial de GetAll. Además, el tamaño máximo de página se implementó para evitar que un cliente sobrecargue el servidor.

Token de página

El campo page_token es una cadena codificada en base64 que utiliza el servidor para determinar dónde comienza la siguiente página. Un token vacío significa que queremos la primera página.

Vista

El campo view se utiliza para especificar si la respuesta debe devolver los IDs de los bordes asociados con las entidades.

Respuesta List

El mensaje de respuesta se vería así:

message ListUserResponse {
repeated User user_list = 1;

string next_page_token = 2;
}

Lista

El campo user_list contiene las entidades de la página.

Token de página siguiente

El campo next_page_token es una cadena codificada en base64 que puede utilizarse en otra solicitud List para recuperar la siguiente página de entidades. Un token vacío significa que esta respuesta contiene la última página de entidades.

Paginación

Con la interfaz gRPC definida, comenzó el desafío de implementarla. Una de las decisiones de diseño más críticas fue cómo implementar la paginación. El enfoque ingenuo sería usar paginación LIMIT/OFFSET para saltar las entradas que ya hemos visto. Sin embargo, este enfoque tiene importantes desventajas; la más problemática es que la base de datos tiene que recuperar todas las filas que está omitiendo para obtener las filas que queremos.

Paginación por Conjunto de Claves

Rotem propuso un enfoque mucho mejor: la paginación por conjunto de claves. Este método es ligeramente más complejo porque requiere usar una columna única (o combinación de columnas) para ordenar las filas. Pero a cambio obtenemos una mejora significativa de rendimiento. Esto se debe a que podemos aprovechar las filas ordenadas para seleccionar solo las entradas con valores en las columnas únicas que sean mayores (orden ascendente) o menores (orden descendente) que o iguales a los valores del token de página proporcionado por el cliente. Así, la base de datos no necesita recuperar las filas que queremos omitir, ¡acelerando considerablemente las consultas en tablas grandes!

Con la paginación por conjunto de claves seleccionada, el siguiente paso fue determinar cómo ordenar las entidades. El enfoque más directo para Ent era usar el campo id; todos los esquemas lo tienen y está garantizado que es único. Esta fue la estrategia elegida para la implementación inicial. Además, necesitábamos decidir si usar orden ascendente o descendente. Para la primera versión se eligió el orden descendente.

Uso

Veamos cómo utilizar realmente la nueva función List:

package main

import (
"context"
"log"

"ent-grpc-example/ent/proto/entpb"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)

func main() {
// Open a connection to the server.
conn, err := grpc.Dial(":5000", grpc.WithInsecure())
if err != nil {
log.Fatalf("failed connecting to server: %s", err)
}
defer conn.Close()
// Create a User service Client on the connection.
client := entpb.NewUserServiceClient(conn)
ctx := context.Background()
// Initialize token for first page.
pageToken := ""
// Retrieve all pages of users.
for {
// Ask the server for the next page of users, limiting entries to 100.
users, err := client.List(ctx, &entpb.ListUserRequest{
PageSize: 100,
PageToken: pageToken,
})
if err != nil {
se, _ := status.FromError(err)
log.Fatalf("failed retrieving user list: status=%s message=%s", se.Code(), se.Message())
}
// Check if we've reached the last page of users.
if users.NextPageToken == "" {
break
}
// Update token for next request.
pageToken = users.NextPageToken
log.Printf("users retrieved: %v", users)
}
}

Perspectivas futuras

La implementación actual de List tiene algunas limitaciones que podrían abordarse en futuras revisiones. Primero, la ordenación está limitada a la columna id. Esto hace que List sea compatible con cualquier esquema, pero no es muy flexible. Idealmente, el cliente debería poder especificar qué columnas usar para ordenar. Alternativamente, las columnas de ordenación podrían definirse en el esquema. Además, List está restringida al orden descendente. En el futuro, esto podría ser una opción especificada en la solicitud. Finalmente, List actualmente solo funciona con esquemas que usan campos id de tipo int32, uuid o string. Esto se debe a que debe definirse un método de conversión separado al token de página para cada tipo que Ent admite en la plantilla de generación de código (¡solo soy una persona!).

Conclusión

Estaba bastante nervioso cuando comencé mi misión de contribuir con esta funcionalidad a entproto; como nuevo colaborador de código abierto, no sabía qué esperar. ¡Me complace compartir que trabajar en el proyecto Ent fue muy divertido! Pude colaborar con personas increíbles y conocedoras mientras ayudaba a la comunidad de código abierto. Desde opciones funcionales y paginación por conjunto de claves hasta pequeñas ideas obtenidas mediante revisiones de PR, aprendí mucho sobre Go (y desarrollo de software en general) en el proceso. ¡Animo encarecidamente a cualquiera que piense en contribuir a que dé el salto! Te sorprenderá lo mucho que ganas con la experiencia.

¿Tienes preguntas? ¿Necesitas ayuda para empezar? Únete a nuestro servidor de Discord o canal de Slack.

[Para más noticias y actualizaciones de Ent:]

· 6 min de lectura
[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 →

La Especificación OpenAPI (OAS, anteriormente conocida como Swagger Specification) es un estándar técnico que define una descripción de interfaz independiente del lenguaje para APIs REST. Esto permite tanto a humanos como a herramientas automatizadas entender el servicio descrito sin necesidad de acceder al código fuente o documentación adicional. Combinada con las herramientas Swagger, puedes generar código boilerplate para servidores y clientes en más de 20 lenguajes simplemente proporcionando el documento OAS.

En un blog anterior, os presentamos una nueva característica de la extensión elk de Ent: un generador de documentos OpenAPI Specification completamente compatible.

Hoy nos complace anunciar que el generador de especificaciones se ha convertido en una extensión oficial del proyecto Ent y ha sido trasladado al repositorio ent/contrib. Además, hemos escuchado los comentarios de la comunidad e implementado mejoras que esperamos os gusten.

Empezando

Para usar la extensión entoas, utiliza el paquete entc (ent codegen) como se describe aquí. Primero instala la extensión en tu módulo Go:

go get entgo.io/contrib/entoas

Ahora sigue estos dos pasos para activarla y configurar Ent para trabajar con la extensión entoas:

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

// +build ignore

package main

import (
"log"

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

func main() {
ex, err := entoas.NewExtension()
if err != nil {
log.Fatalf("creating entoas extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if 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

¡Con estos pasos completados, todo está listo para generar documentos OAS desde tus esquemas! Si eres nuevo en Ent y quieres aprender más sobre cómo conectar diferentes tipos de bases de datos, ejecutar migraciones o trabajar con entidades, visita el Tutorial de Configuración.

Generar un documento OAS

El primer paso para crear nuestro documento OAS es definir un grafo de esquemas Ent. Para brevedad, aquí tienes un esquema de ejemplo:

ent/schema/schema.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)

// Fridge holds the schema definition for the Fridge entity.
type Fridge struct {
ent.Schema
}

// Fields of the Fridge.
func (Fridge) Fields() []ent.Field {
return []ent.Field{
field.String("title"),
}
}

// Edges of the Fridge.
func (Fridge) Edges() []ent.Edge {
return []ent.Edge{
edge.To("compartments", Compartment.Type),
}
}

// Compartment holds the schema definition for the Compartment entity.
type Compartment struct {
ent.Schema
}

// Fields of the Compartment.
func (Compartment) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

// Edges of the Compartment.
func (Compartment) Edges() []ent.Edge {
return []ent.Edge{
edge.From("fridge", Fridge.Type).
Ref("compartments").
Unique(),
edge.To("contents", Item.Type),
}
}

// Item holds the schema definition for the Item entity.
type Item struct {
ent.Schema
}

// Fields of the Item.
func (Item) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}

// Edges of the Item.
func (Item) Edges() []ent.Edge {
return []ent.Edge{
edge.From("compartment", Compartment.Type).
Ref("contents").
Unique(),
}
}

El código anterior muestra la forma característica de Ent para describir un grafo de esquemas. En este caso creamos tres entidades: Fridge, Compartment e Item. Además, añadimos relaciones: una nevera puede tener varios compartimentos, y un compartimento puede contener varios items.

Ahora ejecuta el generador de código:

go generate ./...

Además de los archivos que Ent genera normalmente, se creará un nuevo archivo llamado ent/openapi.json. Aquí tienes un avance de su contenido:

ent/openapi.json
{
"info": {
"title": "Ent Schema API",
"description": "This is an auto generated API description made out of an Ent schema definition",
"termsOfService": "",
"contact": {},
"license": {
"name": ""
},
"version": "0.0.0"
},
"paths": {
"/compartments": {
"get": {
[...]

Si lo deseas, copia su contenido y pégualo en el Swagger Editor. Debería verse así:

Swagger Editor

Swagger Editor

Configuración básica

La descripción de nuestra API aún no refleja su funcionalidad, ¡pero entoas permite personalizarlo! Abre ent/entc.go y actualiza el título y descripción de nuestra API de neveras:

ent/entc.go
//go:build ignore
// +build ignore

package main

import (
"log"

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

func main() {
ex, err := entoas.NewExtension(
entoas.SpecTitle("Fridge CMS"),
entoas.SpecDescription("API to manage fridges and their cooled contents. **ICY!**"),
entoas.SpecVersion("0.0.1"),
)
if err != nil {
log.Fatalf("creating entoas extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

Al regenerar el código se creará un documento OAS actualizado.

ent/openapi.json
{
"info": {
"title": "Fridge CMS",
"description": "API to manage fridges and their cooled contents. **ICY!**",
"termsOfService": "",
"contact": {},
"license": {
"name": ""
},
"version": "0.0.1"
},
"paths": {
"/compartments": {
"get": {
[...]

Configuración de operaciones

A veces no querrás generar endpoints para todas las operaciones de cada nodo. Afortunadamente, entoas permite configurar qué endpoints generar y cuáles ignorar. La política predeterminada de entoas es exponer todas las rutas. Puedes cambiar este comportamiento para no exponer ninguna ruta excepto las solicitadas explícitamente, o simplemente indicar a entoas que excluya operaciones específicas usando entoas.Annotation. Las políticas también controlan operaciones de sub-recursos:

ent/schema/fridge.go
// Edges of the Fridge.
func (Fridge) Edges() []ent.Edge {
return []ent.Edge{
edge.To("compartments", Compartment.Type).
// Do not generate an endpoint for POST /fridges/{id}/compartments
Annotations(
entoas.CreateOperation(
entoas.OperationPolicy(entoas.PolicyExclude),
),
),
}
}

// Annotations of the Fridge.
func (Fridge) Annotations() []schema.Annotation {
return []schema.Annotation{
// Do not generate an endpoint for DELETE /fridges/{id}
entoas.DeleteOperation(entoas.OperationPolicy(entoas.PolicyExclude)),
}
}

¡Y voilà! Las operaciones han desaparecido.

Para más detalles sobre el funcionamiento de políticas en entoas y sus posibilidades, consulta la godoc.

Modelos simplificados

Por defecto entoas genera un esquema de respuesta por endpoint. La estrategia de nombres se detalla en la godoc.

One Schema per Endpoint

One Schema per Endpoint

Muchos usuarios solicitaron cambiar este comportamiento para mapear directamente los esquemas Ent al documento OAS. Ahora puedes configurar entoas para lograrlo:

ex, err := entoas.NewExtension(
entoas.SpecTitle("Fridge CMS"),
entoas.SpecDescription("API to manage fridges and their cooled contents. **ICY!**"),
entoas.SpecVersion("0.0.1"),
entoas.SimpleModels(),
)
Simple Schemas

Simple Schemas

Conclusión

En este anuncio presentamos entoas, la integración oficial en Ent del generador de especificaciones OpenAPI que antes formaba parte de elk. Esta característica conecta las capacidades de generación de código de Ent con el rico ecosistema de herramientas OpenAPI/Swagger.

¿Tienes preguntas? ¿Necesitas ayuda para empezar? Únete a nuestro servidor de Discord o canal de Slack.

[Para más noticias y actualizaciones de Ent:]

· 8 min de lectura
[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 →

Una de las preguntas más frecuentes que recibimos de la comunidad de Ent es cómo sincronizar objetos o referencias entre la base de datos que respalda una aplicación de Ent (como MySQL o PostgreSQL) con servicios externos. Por ejemplo, los usuarios quieren crear o eliminar registros en su CRM cuando se crea o elimina un usuario en Ent, publicar mensajes en un sistema Pub/Sub cuando se actualiza una entidad, o verificar referencias a blobs en almacenamiento de objetos como AWS S3 o Google Cloud Storage.

Garantizar la consistencia entre dos sistemas de datos separados no es tarea sencilla. Cuando queremos propagar, por ejemplo, la eliminación de un registro de un sistema a otro, no hay forma obvia de garantizar que ambos sistemas terminen en un estado sincronizado, ya que uno de ellos podría fallar o el enlace de red podría ser lento o estar caído. Dicho esto, y especialmente con el auge de las arquitecturas de microservicios, estos problemas se han vuelto más comunes, y los investigadores de sistemas distribuidos han desarrollado patrones para resolverlos, como el Patrón Saga.

La aplicación de estos patrones suele ser compleja y difícil, por lo que en muchos casos los arquitectos no persiguen un diseño "perfecto", sino que optan por soluciones más simples que implican aceptar cierta inconsistencia entre los sistemas o implementar procedimientos de reconciliación en segundo plano.

En esta publicación, no discutiremos cómo resolver transacciones distribuidas ni implementar el patrón Saga con Ent. En su lugar, limitaremos nuestro alcance a estudiar cómo interceptar mutaciones de Ent antes y después de que ocurran, y ejecutar allí nuestra lógica personalizada.

Propagar mutaciones a sistemas externos

En nuestro ejemplo, crearemos un esquema simple de User con 2 campos de texto inmutables: "name" y "avatar_url". Ejecutemos el comando ent init para crear un esqueleto de esquema para nuestro User:

go run entgo.io/ent/cmd/ent new User

Luego, añadimos los campos name y avatar_url y ejecutamos go generate para generar los recursos.

ent/schema/user.go
type User struct {
ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Immutable(),
field.String("avatar_url").
Immutable(),
}
}
go generate ./ent

El problema

El campo avatar_url define una URL a una imagen en un bucket de nuestro almacenamiento de objetos (por ejemplo, AWS S3). Para los fines de esta discusión, queremos asegurarnos de que:

  • Cuando se crea un usuario, exista una imagen con la URL almacenada en "avatar_url" en nuestro bucket.

  • Se eliminen las imágenes huérfanas del bucket. Esto significa que cuando un usuario se elimina de nuestro sistema, su imagen de avatar también se borra.

Para interactuar con blobs, usaremos el paquete gocloud.dev/blob. Este paquete proporciona abstracciones para leer, escribir, eliminar y listar blobs en un bucket. Similar al paquete database/sql, permite interactuar con diversos almacenamientos de objetos con la misma API configurando su URL de controlador. Por ejemplo:

// Open an in-memory bucket. 
if bucket, err := blob.OpenBucket(ctx, "mem://photos/"); err != nil {
log.Fatal("failed opening in-memory bucket:", err)
}

// Open an S3 bucket named photos.
if bucket, err := blob.OpenBucket(ctx, "s3://photos"); err != nil {
log.Fatal("failed opening s3 bucket:", err)
}

// Open a bucket named photos in Google Cloud Storage.
if bucket, err := blob.OpenBucket(ctx, "gs://my-bucket"); err != nil {
log.Fatal("failed opening gs bucket:", err)
}

Hooks de esquema

Los hooks son una característica potente de Ent que permite añadir lógica personalizada antes y después de operaciones que mutan el grafo.

Los hooks pueden definirse dinámicamente usando client.Use (llamados "Runtime Hooks"), o explícitamente en el esquema (llamados "Schema Hooks") de la siguiente manera:

// Hooks of the User.
func (User) Hooks() []ent.Hook {
return []ent.Hook{
EnsureImageExists(),
DeleteOrphans(),
}
}

Como puedes imaginar, el hook EnsureImageExists se encargará de garantizar que cuando se cree un usuario, su URL de avatar exista en el bucket, y DeleteOrphans asegurará que se eliminen las imágenes huérfanas. Comencemos a escribirlos.

ent/schema/hooks.go
func EnsureImageExists() ent.Hook {
hk := func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
avatarURL, exists := m.AvatarURL()
if !exists {
return nil, errors.New("avatar field is missing")
}
// TODO:
// 1. Verify that "avatarURL" points to a real object in the bucket.
// 2. Otherwise, fail.
return next.Mutate(ctx, m)
})
}
// Limit the hook only to "Create" operations.
return hook.On(hk, ent.OpCreate)
}

func DeleteOrphans() ent.Hook {
hk := func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
id, exists := m.ID()
if !exists {
return nil, errors.New("id field is missing")
}
// TODO:
// 1. Get the AvatarURL field of the deleted user.
// 2. Cascade the deletion to object storage.
return next.Mutate(ctx, m)
})
}
// Limit the hook only to "DeleteOne" operations.
return hook.On(hk, ent.OpDeleteOne)
}

Ahora, quizás te estés preguntando: ¿cómo accedemos al cliente de blobs desde los hooks de mutación? Lo descubrirás en la siguiente sección.

Inyección de Dependencias

La opción entc.Dependency permite extender los builders generados con dependencias externas como campos de estructura, y proporciona opciones para inyectarlas durante la inicialización del cliente.

Para inyectar un blob.Bucket que esté disponible dentro de nuestros hooks, podemos seguir el tutorial sobre dependencias externas en el sitio web, y definir gocloud.dev/blob.Bucket como dependencia.

ent/entc.go
func main() {
opts := []entc.Option{
entc.Dependency(
entc.DependencyName("Bucket"),
entc.DependencyType(&blob.Bucket{}),
),
}
if err := entc.Generate("./schema", &gen.Config{}, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

A continuación, vuelve a ejecutar la generación de código:

go generate ./ent

Ahora podemos acceder a la API de Bucket desde todos los builders generados. Terminemos las implementaciones de los hooks mencionados.

ent/schema/hooks.go
// EnsureImageExists ensures the avatar_url points
// to a real object in the bucket.
func EnsureImageExists() ent.Hook {
hk := func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
avatarURL, exists := m.AvatarURL()
if !exists {
return nil, errors.New("avatar field is missing")
}
switch exists, err := m.Bucket.Exists(ctx, avatarURL); {
case err != nil:
return nil, fmt.Errorf("check key existence: %w", err)
case !exists:
return nil, fmt.Errorf("key %q does not exist in the bucket", avatarURL)
default:
return next.Mutate(ctx, m)
}
})
}
return hook.On(hk, ent.OpCreate)
}

// DeleteOrphans cascades the user deletion to the bucket.
// Hence, when a user is deleted, its avatar image is deleted
// as well.
func DeleteOrphans() ent.Hook {
hk := func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
id, exists := m.ID()
if !exists {
return nil, errors.New("id field is missing")
}
u, err := m.Client().User.Get(ctx, id)
if err != nil {
return nil, fmt.Errorf("getting deleted user: %w", err)
}
if err := m.Bucket.Delete(ctx, u.AvatarURL); err != nil {
return nil, fmt.Errorf("deleting user avatar from bucket: %w", err)
}
return next.Mutate(ctx, m)
})
}
return hook.On(hk, ent.OpDeleteOne)
}

¡Llegó el momento de probar nuestros hooks! Escribamos un ejemplo verificable que confirme que nuestros 2 hooks funcionan como se espera.

package main

import (
"context"
"fmt"
"log"

"github.com/a8m/ent-sync-example/ent"
_ "github.com/a8m/ent-sync-example/ent/runtime"

"entgo.io/ent/dialect"
_ "github.com/mattn/go-sqlite3"
"gocloud.dev/blob"
_ "gocloud.dev/blob/memblob"
)

func Example_SyncCreate() {
ctx := context.Background()
// Open an in-memory bucket.
bucket, err := blob.OpenBucket(ctx, "mem://photos/")
if err != nil {
log.Fatal("failed opening bucket:", err)
}
client, err := ent.Open(
dialect.SQLite,
"file:ent?mode=memory&cache=shared&_fk=1",
// Inject the blob.Bucket on client initialization.
ent.Bucket(bucket),
)
if err != nil {
log.Fatal("failed opening connection to sqlite:", err)
}
defer client.Close()
if err := client.Schema.Create(ctx); err != nil {
log.Fatal("failed creating schema resources:", err)
}
if err := client.User.Create().SetName("a8m").SetAvatarURL("a8m.png").Exec(ctx); err == nil {
log.Fatal("expect user creation to fail because the image does not exist in the bucket")
}
if err := bucket.WriteAll(ctx, "a8m.png", []byte{255, 255, 255}, nil); err != nil {
log.Fatalf("failed uploading image to the bucket: %v", err)
}
fmt.Printf("%q\n", keys(ctx, bucket))

// User creation should pass as image was uploaded to the bucket.
u := client.User.Create().SetName("a8m").SetAvatarURL("a8m.png").SaveX(ctx)

// Deleting a user, should delete also its image from the bucket.
client.User.DeleteOne(u).ExecX(ctx)
fmt.Printf("%q\n", keys(ctx, bucket))

// Output:
// ["a8m.png"]
// []
}

Conclusión

¡Genial! Hemos configurado Ent para extender nuestro código generado e inyectar el blob.Bucket como dependencia externa. Luego definimos dos hooks de mutación y usamos la API de blob.Bucket para garantizar que se cumplan nuestras restricciones de producto.

El código de este ejemplo está disponible en github.com/a8m/ent-sync-example.

[Para más noticias y actualizaciones de Ent:]

· 5 min de lectura
[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 →

Ent es un potente framework de entidades que ayuda a los desarrolladores a escribir código limpio que se traduce en consultas de base de datos (potencialmente complejas). A medida que crece el uso de tu aplicación, no pasa mucho tiempo antes de que encuentres problemas de rendimiento con tu base de datos. Solucionar problemas de rendimiento en bases de datos es notoriamente difícil, especialmente cuando no cuentas con las herramientas adecuadas.

El siguiente ejemplo muestra cómo el código de consulta de Ent se traduce en una consulta SQL.

ent example 1

Example 1 - ent code is translated to SQL query

Tradicionalmente, ha sido muy difícil correlacionar las consultas de base de datos de bajo rendimiento con el código de aplicación que las genera. Las herramientas de análisis de rendimiento de bases de datos pueden ayudar a identificar consultas lentas mediante el análisis de los registros del servidor, pero ¿cómo se pueden rastrear hasta la aplicación?

Sqlcommenter

A principios de este año, Google presentó Sqlcommenter. Sqlcommenter es

an open source library that addresses the gap between the ORM libraries and understanding database performance. Sqlcommenter gives application developers visibility into which application code is generating slow queries and maps application traces to database query plans

En otras palabras, Sqlcommenter añade metadatos de contexto de aplicación a las consultas SQL. Esta información luego puede usarse para proporcionar información significativa. Lo logra agregando comentarios SQL a la consulta que transportan metadatos pero son ignorados por la base de datos durante la ejecución. Por ejemplo, la siguiente consulta contiene un comentario con metadatos sobre la aplicación que la emitió (users-mgr), qué controlador y ruta la activaron (users y user_rename, respectivamente), y el controlador de base de datos utilizado (ent:v0.9.1):

update users set username = ‘hedwigz’ where id = 88
/*application='users-mgr',controller='users',route='user_rename',db_driver='ent:v0.9.1'*/

Para probar cómo el análisis de metadatos recopilados por Sqlcommenter puede ayudarnos a entender mejor los problemas de rendimiento, considere este ejemplo: Google Cloud lanzó recientemente Cloud SQL Insights, un producto de análisis de rendimiento SQL basado en la nube. En la imagen inferior, vemos una captura del panel de Cloud SQL Insights que muestra que la ruta HTTP 'api/users' está causando muchos bloqueos en la base de datos. También podemos ver que esta consulta se llamó 16.067 veces en las últimas 6 horas.

Cloud SQL insights

Screenshot from Cloud SQL Insights Dashboard

Este es el poder de las etiquetas SQL: proporcionan correlación entre la información a nivel de aplicación y los monitores de tu base de datos.

sqlcomment

sqlcomment es un driver de Ent que añade metadatos a las consultas SQL usando comentarios siguiendo la especificación sqlcommenter. Al envolver un driver existente de Ent con sqlcomment, los usuarios pueden aprovechar cualquier herramienta compatible con el estándar para diagnosticar problemas de rendimiento de consultas. Sin más preámbulos, veamos sqlcomment en acción.

Primero, para instalar sqlcomment ejecuta:

go get ariga.io/sqlcomment

sqlcomment envuelve un driver SQL subyacente, por lo tanto, debemos abrir nuestra conexión SQL usando el módulo sql de ent, en lugar del popular helper ent.Open.

información

Asegúrate de importar entgo.io/ent/dialect/sql en el siguiente fragmento

// Create db driver.
db, err := sql.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}

// Create sqlcomment driver which wraps sqlite driver.
drv := sqlcomment.NewDriver(db,
sqlcomment.WithDriverVerTag(),
sqlcomment.WithTags(sqlcomment.Tags{
sqlcomment.KeyApplication: "my-app",
sqlcomment.KeyFramework: "net/http",
}),
)

// Create and configure ent client.
client := ent.NewClient(ent.Driver(drv))

Ahora, cada vez que ejecutemos una consulta, sqlcomment añadirá un sufijo a nuestra consulta SQL con las etiquetas configuradas. Si ejecutáramos la siguiente consulta:

client.User.
Update().
Where(
user.Or(
user.AgeGT(30),
user.Name("bar"),
),
user.HasFollowers(),
).
SetName("foo").
Save()

Ent generaría la siguiente consulta SQL comentada:

UPDATE `users`
SET `name` = ?
WHERE (
`users`.`age` > ?
OR `users`.`name` = ?
)
AND `users`.`id` IN (
SELECT `user_following`.`follower_id`
FROM `user_following`
)
/*application='my-app',db_driver='ent:v0.9.1',framework='net%2Fhttp'*/

Como puedes ver, Ent generó una consulta SQL con un comentario al final que contiene toda la información relevante asociada a esa consulta.

sqlcomment admite más etiquetas y tiene integraciones con OpenTelemetry y OpenCensus. Para ver más ejemplos y escenarios, visita el repositorio de GitHub.

Resumen final

En este artículo he mostrado cómo añadir metadatos a las consultas mediante comentarios SQL ayuda a correlacionar código fuente con consultas de base de datos. Después presenté sqlcomment, un driver para Ent que añade etiquetas SQL a todas tus consultas. Finalmente, vimos sqlcomment en acción instalándolo y configurándolo con Ent. Si te gusta el código y/o quieres contribuir, no dudes en visitar el proyecto en GitHub.

¿Tienes preguntas? ¿Necesitas ayuda para empezar? Únete a nuestro servidor de Discord o canal de Slack.

[Para más noticias y actualizaciones de Ent:]

· 7 min de lectura
[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 →

Mientras trabajábamos en el motor de consultas de grafos de datos operativos de Ariga, identificamos una oportunidad para mejorar drásticamente el rendimiento en numerosos casos de uso mediante una biblioteca de caché robusta. Como usuarios intensivos de Ent, era natural implementar esta capa como extensión de Ent. En esta publicación, explicaré brevemente qué son las cachés, cómo encajan en arquitecturas de software y presentaré entcache, un controlador de caché para Ent.

El almacenamiento en caché es una estrategia popular para mejorar el rendimiento de aplicaciones. Se basa en la observación de que la velocidad para recuperar datos usando distintos tipos de medios puede variar en órdenes de magnitud. Jeff Dean presentó famosamente estas cifras en una conferencia sobre "Consejos de Ingeniería de Software para construir sistemas distribuidos a gran escala":

cifras de caché

Estas cifras reflejan lo que ingenieros de software experimentados conocen intuitivamente: leer de memoria es más rápido que de disco, y obtener datos del mismo centro de datos es más rápido que desde internet. Añadamos que algunos cálculos son costosos y lentos, por lo que recuperar resultados precomputados puede ser mucho más rápido (y económico) que recalcularlos cada vez.

La inteligencia colectiva de Wikipedia define una caché como "un componente hardware o software que almacena datos para servir futuras solicitudes más rápido". En otras palabras: si almacenamos resultados de consultas en RAM, podemos atender solicitudes dependientes mucho más rápido que si debemos consultar la base de datos a través de red, esperar que lea datos de disco, ejecute cómputos y finalmente nos envíe la respuesta (por red).

Sin embargo, como ingenieros debemos recordar que las cachés son notoriamente complejas. Como dijo Phil Karlton, ingeniero de Netscape: "Solo hay dos problemas difíciles en Informática: la invalidación de caché y nombrar cosas". Por ejemplo, en sistemas que requieren consistencia fuerte, una entrada de caché obsoleta puede causar comportamientos incorrectos. Por ello, diseñar cachés en arquitecturas requiere extremo cuidado y atención al detalle.

Presentando entcache

El paquete entcache proporciona un nuevo controlador para Ent que envuelve drivers SQL existentes. A alto nivel, decora el método Query del driver subyacente realizando en cada llamada:

  1. Genera una clave de caché (ej. hash) con sus argumentos (sentencia y parámetros).

  2. Consulta la caché para ver si los resultados están disponibles. Si existen (lo que llamamos cache-hit), omite la base de datos y devuelve los resultados desde memoria.

  3. Si la caché no contiene la entrada, pasa la consulta a la base de datos.

  4. Tras ejecutar la consulta, registra los valores brutos de las filas devueltas (sql.Rows) y las almacena en caché con la clave generada.

El paquete ofrece varias opciones para configurar el TTL de entradas, controlar la función hash, implementar almacenes personalizados o multinivel, invalidar y omitir entradas. Consulta la documentación completa en https://pkg.go.dev/ariga.io/entcache.

Como mencionamos anteriormente, configurar correctamente el almacenamiento en caché para una aplicación es una tarea delicada. Por ello, entcache ofrece a los desarrolladores diferentes niveles de caché que pueden utilizarse:

  1. Una caché basada en context.Context. Suele vincularse a una solicitud y no funciona con otros niveles de caché. Se utiliza para eliminar consultas duplicadas que ejecuta la misma solicitud.

  2. Una caché a nivel de driver utilizada por ent.Client. Una aplicación normalmente crea un driver por base de datos, por lo que lo tratamos como una caché a nivel de proceso.

  3. Una caché remota. Por ejemplo, una base de datos Redis que proporciona una capa de persistencia para almacenar y compartir entradas de caché entre múltiples procesos. Esta capa es resistente a cambios o fallos en los despliegues de aplicaciones, y permite reducir el número de consultas idénticas ejecutadas en la base de datos por diferentes procesos.

  4. Una jerarquía de caché, o caché multinivel, permite estructurar la caché de forma jerárquica. La jerarquía de almacenes de caché se basa principalmente en velocidades de acceso y tamaños. Por ejemplo, una caché de 2 niveles compuesta por una caché LRU en la memoria de la aplicación y una caché remota respaldada por Redis.

Ilustremos esto explicando la caché basada en context.Context.

Caché a Nivel de Contexto

La opción ContextLevel configura el driver para trabajar con una caché a nivel de context.Context. El contexto suele vincularse a una solicitud (ej. *http.Request) y no está disponible en modo multinivel. Cuando se usa esta opción como almacén de caché, el context.Context adjunto lleva una caché LRU (configurable de otra manera), y el driver almacena y busca entradas en esta caché al ejecutar consultas.

Esta opción es ideal para aplicaciones que requieren consistencia fuerte pero quieren evitar ejecutar consultas duplicadas en la misma solicitud. Por ejemplo, dada la siguiente consulta GraphQL:

query($ids: [ID!]!) {
nodes(ids: $ids) {
... on User {
id
name
todos {
id
owner {
id
name
}
}
}
}
}

Una solución ingenua para resolver esta consulta ejecutaría: 1 consulta para obtener N usuarios, otras N consultas para obtener los todos de cada usuario, y una consulta por cada todo para obtener su propietario (más sobre el Problema N+1).

Sin embargo, Ent ofrece un enfoque único para resolver estas consultas (detalles en el sitio de Ent), por lo que solo se ejecutan 3 consultas: 1 para obtener N usuarios, 1 para obtener los todos de todos los usuarios, y 1 para obtener los propietarios de todos los todos.

Con entcache, el número de consultas puede reducirse a 2, ya que la primera y última consulta son idénticas (ver ejemplo de código).

context-level-cache

Los diferentes niveles se explican en profundidad en el README del repositorio.

Empezando

Si no estás familiarizado con la creación de proyectos en Ent, completa primero el tutorial de configuración.

Primero, go get el paquete usando el siguiente comando:

go get ariga.io/entcache

Tras instalar entcache, puedes añadirlo fácilmente a tu proyecto con este fragmento:

// Open the database connection.
db, err := sql.Open(dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatal("opening database", err)
}
// Decorates the sql.Driver with entcache.Driver.
drv := entcache.NewDriver(db)
// Create an ent.Client.
client := ent.NewClient(ent.Driver(drv))

// Tell the entcache.Driver to skip the caching layer
// when running the schema migration.
if client.Schema.Create(entcache.Skip(ctx)); err != nil {
log.Fatal("running schema migration", err)
}

// Run queries.
if u, err := client.User.Get(ctx, id); err != nil {
log.Fatal("querying user", err)
}
// The query below is cached.
if u, err := client.User.Get(ctx, id); err != nil {
log.Fatal("querying user", err)
}

Para ver ejemplos más avanzados, visita el directorio de ejemplos del repositorio.

Conclusión

En esta entrada, he presentado "entcache", un nuevo controlador de caché para Ent que desarrollé mientras trabajaba en el motor de consultas del Grafo de Datos Operacionales de Ariga. Comenzamos la discusión mencionando brevemente la motivación para incluir cachés en sistemas de software. A continuación, describimos las características y capacidades de entcache y concluimos con un breve ejemplo de cómo puedes configurarlo en tu aplicación.

Hay varias características en las que estamos trabajando y que nos gustaría implementar, pero necesitamos ayuda de la comunidad para diseñarlas correctamente (¿alguien para resolver la invalidación de caché? ;)). Si estás interesado en contribuir, contáctame en el canal de Slack de Ent.

[Para más noticias y actualizaciones de Ent:]

· 9 min de lectura
[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 →

Hace unos meses, el proyecto Ent anunció la Iniciativa de Importación de Esquemas, cuyo objetivo es ayudar a cubrir múltiples casos de uso para generar esquemas Ent a partir de recursos externos. Hoy me complace compartir un proyecto en el que he estado trabajando: entimport, una herramienta de línea de comandos importante (juego de palabras intencionado) diseñada para crear esquemas Ent a partir de bases de datos SQL existentes. Esta es una funcionalidad solicitada por la comunidad desde hace tiempo, así que espero que mucha gente la encuentre útil. Puede facilitar la transición de configuraciones existentes desde otros lenguajes u ORM a Ent. También resulta útil en casos donde se desea acceder a los mismos datos desde diferentes plataformas (como para sincronizarlas automáticamente).
La primera versión soporta bases de datos MySQL y PostgreSQL, con algunas limitaciones que se describen más abajo. El soporte para otras bases de datos relacionales como SQLite está en desarrollo.

Primeros pasos

Para que te hagas una idea de cómo funciona entimport, quiero compartir un ejemplo rápido de uso completo con una base de datos MySQL. A grandes rasgos, esto es lo que haremos:

  1. Crear una base de datos y esquema: queremos mostrar cómo entimport puede generar un esquema Ent para una base de datos existente. Primero crearemos una base de datos, luego definiremos algunas tablas que podremos importar a Ent.

  2. Inicializar un proyecto Ent: usaremos la CLI de Ent para crear la estructura de directorios necesaria y un script de generación de esquemas Ent.

  3. Instalar entimport

  4. Ejecutar entimport contra nuestra base de datos demo: a continuación, importaremos el esquema de base de datos que hemos creado a nuestro proyecto Ent.

  5. Explicar cómo usar Ent con nuestros esquemas generados.

Empecemos.

Crear una base de datos

Comenzaremos creando una base de datos. La forma que prefiero hacerlo es usando un contenedor de Docker. Utilizaremos un docker-compose que pasará automáticamente todos los parámetros necesarios al contenedor MySQL.

Inicia el proyecto en un nuevo directorio llamado entimport-example. Crea un archivo llamado docker-compose.yaml y pega el siguiente contenido dentro:

version: "3.7"

services:

mysql8:
platform: linux/amd64
image: mysql
environment:
MYSQL_DATABASE: entimport
MYSQL_ROOT_PASSWORD: pass
healthcheck:
test: mysqladmin ping -ppass
ports:
- "3306:3306"

Este archivo contiene la configuración del servicio para un contenedor Docker de MySQL. Ejecútalo con el siguiente comando:

docker-compose up -d

A continuación, crearemos un esquema simple. Para este ejemplo usaremos una relación entre dos entidades:

  • Usuario

  • Coche

Conéctate a la base de datos usando la shell de MySQL. Puedes hacerlo con el siguiente comando:

Asegúrate de ejecutarlo desde el directorio raíz del proyecto

docker-compose exec mysql8 mysql --database=entimport -ppass
create table users
(
id bigint auto_increment primary key,
age bigint not null,
name varchar(255) not null,
last_name varchar(255) null comment 'surname'
);

create table cars
(
id bigint auto_increment primary key,
model varchar(255) not null,
color varchar(255) not null,
engine_size mediumint not null,
user_id bigint null,
constraint cars_owners foreign key (user_id) references users (id) on delete set null
);

Validemos que hemos creado las tablas mencionadas anteriormente. En tu shell de MySQL, ejecuta:

show tables;
+---------------------+
| Tables_in_entimport |
+---------------------+
| cars |
| users |
+---------------------+

Deberíamos ver dos tablas: users y cars

Inicializar proyecto Ent

Ahora que hemos creado nuestra base de datos y un esquema base para demostrar nuestro ejemplo, necesitamos crear un proyecto Go con Ent. En esta fase explicaré cómo hacerlo. Como eventualmente querremos usar nuestro esquema importado, necesitamos crear la estructura de directorios de Ent.

Inicializa un nuevo proyecto Go dentro de un directorio llamado entimport-example

go mod init entimport-example

Ejecuta Ent Init:

go run -mod=mod entgo.io/ent/cmd/ent new 

El proyecto debería verse así:

├── docker-compose.yaml
├── ent
│ ├── generate.go
│ └── schema
└── go.mod

Instalar entimport

¡Vale, ahora empieza lo divertido! Por fin estamos listos para instalar entimport y verlo en acción.
Empecemos ejecutando entimport:

go run -mod=mod ariga.io/entimport/cmd/entimport -h

Se descargará entimport y el comando mostrará:

Usage of entimport:
-dialect string
database dialect (default "mysql")
-dsn string
data source name (connection information)
-schema-path string
output path for ent schema (default "./ent/schema")
-tables value
comma-separated list of tables to inspect (all if empty)

Ejecutar entimport

¡Ya estamos listos para importar nuestro esquema MySQL a Ent!

Lo haremos con el siguiente comando:

Este comando importará todas las tablas de nuestro esquema. También puedes limitarlo a tablas específicas usando el flag -tables.

go run ariga.io/entimport/cmd/entimport -dialect mysql -dsn "root:pass@tcp(localhost:3306)/entimport"

Como muchas herramientas Unix, entimport no muestra nada cuando se ejecuta correctamente. Para verificar que funcionó, revisaremos el sistema de archivos, concretamente el directorio ent/schema.

├── docker-compose.yaml
├── ent
│ ├── generate.go
│ └── schema
│ ├── car.go
│ └── user.go
├── go.mod
└── go.sum

Veamos qué obtenemos: recordemos que teníamos dos esquemas, users y cars con una relación uno a muchos. Veamos cómo lo gestionó entimport.

entimport-example/ent/schema/user.go
type User struct {
ent.Schema
}

func (User) Fields() []ent.Field {
return []ent.Field{field.Int("id"), field.Int("age"), field.String("name"), field.String("last_name").Optional().Comment("surname")}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{edge.To("cars", Car.Type)}
}
func (User) Annotations() []schema.Annotation {
return nil
}
entimport-example/ent/schema/car.go
type Car struct {
ent.Schema
}

func (Car) Fields() []ent.Field {
return []ent.Field{field.Int("id"), field.String("model"), field.String("color"), field.Int32("engine_size"), field.Int("user_id").Optional()}
}
func (Car) Edges() []ent.Edge {
return []ent.Edge{edge.From("user", User.Type).Ref("cars").Unique().Field("user_id")}
}
func (Car) Annotations() []schema.Annotation {
return nil
}

¡entimport creó con éxito las entidades y su relación!

Hasta aquí todo bien. Ahora probémoslo realmente. Primero debemos generar el esquema de Ent, porque Ent es un ORM primero-esquema que genera código Go para interactuar con diferentes bases de datos.

Para ejecutar la generación de código de Ent:

go generate ./ent

Veamos nuestro directorio ent:

...
├── ent
│ ├── car
│ │ ├── car.go
│ │ └── where.go
...
│ ├── schema
│ │ ├── car.go
│ │ └── user.go
...
│ ├── user
│ │ ├── user.go
│ │ └── where.go
...

Ejemplo con Ent

Ejecutemos un ejemplo rápido para verificar que nuestro esquema funciona:

Crea un archivo llamado example.go en la raíz del proyecto con este contenido:

Esta parte del ejemplo está disponible aquí

entimport-example/example.go
package main

import (
"context"
"fmt"
"log"

"entimport-example/ent"

"entgo.io/ent/dialect"
_ "github.com/go-sql-driver/mysql"
)

func main() {
client, err := ent.Open(dialect.MySQL, "root:pass@tcp(localhost:3306)/entimport?parseTime=True")
if err != nil {
log.Fatalf("failed opening connection to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
example(ctx, client)
}

Intentemos añadir un usuario. Escribe este código al final del archivo:

entimport-example/example.go
func example(ctx context.Context, client *ent.Client) {
// Create a User.
zeev := client.User.
Create().
SetAge(33).
SetName("Zeev").
SetLastName("Manilovich").
SaveX(ctx)
fmt.Println("User created:", zeev)
}

Luego ejecuta:

go run example.go

Debería mostrar:

# User created: User(id=1, age=33, name=Zeev, last_name=Manilovich)

Comprobemos en la base de datos si realmente se añadió el usuario:

SELECT *
FROM users
WHERE name = 'Zeev';

+--+---+----+----------+
|id|age|name|last_name |
+--+---+----+----------+
|1 |33 |Zeev|Manilovich|
+--+---+----+----------+

¡Genial! Ahora juguemos un poco más con Ent añadiendo relaciones. Añade este código al final de la función example():

Asegúrate de añadir "entimport-example/ent/user" en la declaración de import()

entimport-example/example.go
// Create Car.
vw := client.Car.
Create().
SetModel("volkswagen").
SetColor("blue").
SetEngineSize(1400).
SaveX(ctx)
fmt.Println("First car created:", vw)

// Update the user - add the car relation.
client.User.Update().Where(user.ID(zeev.ID)).AddCars(vw).SaveX(ctx)

// Query all cars that belong to the user.
cars := zeev.QueryCars().AllX(ctx)
fmt.Println("User cars:", cars)

// Create a second Car.
delorean := client.Car.
Create().
SetModel("delorean").
SetColor("silver").
SetEngineSize(9999).
SaveX(ctx)
fmt.Println("Second car created:", delorean)

// Update the user - add another car relation.
client.User.Update().Where(user.ID(zeev.ID)).AddCars(delorean).SaveX(ctx)

// Traverse the sub-graph.
cars = delorean.
QueryUser().
QueryCars().
AllX(ctx)
fmt.Println("User cars:", cars)

Esta parte del ejemplo está disponible aquí

Ahora haz: go run example.go.
Tras ejecutar el código anterior, la base de datos debería contener un usuario con 2 coches en una relación uno a muchos.

SELECT *
FROM users;

+--+---+----+----------+
|id|age|name|last_name |
+--+---+----+----------+
|1 |33 |Zeev|Manilovich|
+--+---+----+----------+

SELECT *
FROM cars;

+--+----------+------+-----------+-------+
|id|model |color |engine_size|user_id|
+--+----------+------+-----------+-------+
|1 |volkswagen|blue |1400 |1 |
|2 |delorean |silver|9999 |1 |
+--+----------+------+-----------+-------+

Sincronizar cambios en la BD

Como queremos mantener sincronizada la base de datos, necesitamos que entimport pueda modificar el esquema tras cambios en la BD. Veamos cómo funciona.

Ejecuta este código SQL para añadir una columna phone con índice unique a la tabla users:

alter table users
add phone varchar(255) null;

create unique index users_phone_uindex
on users (phone);

La tabla debería verse así:

describe users;
+-----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+----------------+
| id | bigint | NO | PRI | NULL | auto_increment |
| age | bigint | NO | | NULL | |
| name | varchar(255) | NO | | NULL | |
| last_name | varchar(255) | YES | | NULL | |
| phone | varchar(255) | YES | UNI | NULL | |
+-----------+--------------+------+-----+---------+----------------+

Ahora ejecutemos entimport nuevamente para obtener el esquema actualizado:

go run -mod=mod ariga.io/entimport/cmd/entimport -dialect mysql -dsn "root:pass@tcp(localhost:3306)/entimport"

Podemos ver que el archivo user.go cambió:

entimport-example/ent/schema/user.go
func (User) Fields() []ent.Field {
return []ent.Field{field.Int("id"), ..., field.String("phone").Optional().Unique()}
}

Ahora podemos ejecutar go generate ./ent nuevamente y usar el nuevo esquema para añadir un phone a la entidad User.

Planes futuros

Como mencioné antes, esta versión inicial soporta bases de datos MySQL y PostgreSQL.
También admite todos los tipos de relaciones SQL. Tengo planes para mejorar la herramienta añadiendo características como campos faltantes de PostgreSQL, valores por defecto y más.

Conclusión

En esta publicación, he presentado entimport, una herramienta muy esperada y solicitada en múltiples ocasiones por la comunidad de Ent. He mostrado un ejemplo práctico de cómo usarla junto con Ent. Esta herramienta es otra incorporación a las utilidades de importación de esquemas de Ent, diseñadas para facilitar aún más la integración de Ent. Para debates y soporte, abre un issue. El ejemplo completo puede encontrarse aquí. ¡Espero que esta publicación te haya resultado útil!

[Para más noticias y actualizaciones de Ent:]