Saltar al contenido principal

· 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 →

En un artículo anterior, te presentamos elk, una extensión de Ent que permite generar una API HTTP CRUD completamente funcional en Go a partir de tu esquema. En el artículo de hoy me gustaría presentarte una nueva y reluciente característica que ha llegado recientemente a elk: un generador totalmente compatible de la Especificación OpenAPI (OAS).

OAS (anteriormente conocida como Swagger Specification) es una especificación técnica que define una descripción estándar de interfaz, independiente del lenguaje, para APIs REST. Esto permite tanto a humanos como a herramientas automatizadas comprender el servicio descrito sin necesidad del código fuente o documentación adicional. Combinado con las herramientas Swagger, puedes generar código boilerplate tanto para servidor como para cliente en más de 20 lenguajes, simplemente pasando el archivo OAS.

Empezando

El primer paso es añadir el paquete elk a tu proyecto:

go get github.com/masseelch/elk@latest

elk utiliza la API de Extensiones de Ent para integrarse con la generación de código de Ent. Esto requiere que usemos el paquete entc (ent codegen) como se describe aquí para generar el código de nuestro proyecto. Sigue estos dos pasos para activarlo y configurar Ent para trabajar con la extensión elk:

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

// +build ignore

package main

import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/masseelch/elk"
)

func main() {
ex, err := elk.NewExtension(
elk.GenerateSpec("openapi.json"),
)
if err != nil {
log.Fatalf("creating elk 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 un archivo OAS desde tu esquema! Si eres nuevo en Ent y quieres aprender más sobre cómo conectar con diferentes tipos de bases de datos, ejecutar migraciones o trabajar con entidades, dirígete al Tutorial de Configuración.

Generar un archivo OAS

El primer paso hacia nuestro archivo OAS es crear un gráfico de esquema Ent:

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

Para demostrar las capacidades de generación OAS de elk, construiremos juntos una aplicación de ejemplo. Supongamos que tengo varios frigoríficos con múltiples compartimentos, y mi pareja y yo queremos conocer su contenido en todo momento. Para proporcionarnos esta información increíblemente útil, crearemos un servidor en Go con una API RESTful. Para facilitar la creación de aplicaciones cliente que puedan comunicarse con nuestro servidor, crearemos un archivo de Especificación OpenAPI que describa su API. Una vez que lo tengamos, ¡podremos construir un frontend para gestionar frigoríficos y contenidos en el lenguaje que elijamos usando Swagger Codegen! Puedes encontrar un ejemplo que usa docker para generar un cliente aquí.

Creemos nuestro esquema:

ent/fridge.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),
}
}
ent/compartment.go
package schema

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

// 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),
}
}
ent/item.go
package schema

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

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

Ahora, generemos el código de Ent y el archivo OAS.

go generate ./...

Además de los archivos que Ent genera normalmente, se ha creado otro archivo llamado openapi.json. Copia su contenido y pégalo en el Editor Swagger. Deberías ver tres grupos: Compartment, Item y Fridge.

Swagger Editor Example

Swagger Editor Example

Si abres la pestaña de operación POST en el grupo Fridge, verás una descripción de los datos esperados en la solicitud y todas las respuestas posibles. ¡Genial!

POST operation on Fridge

POST operation on Fridge

Configuración básica

La descripción de nuestra API aún no refleja su propósito. ¡Vamos a cambiarlo! elk ofrece constructores de configuración fáciles de usar para personalizar el archivo OAS generado. Abre ent/entc.go y actualiza el título y la descripción de nuestra Fridge API:

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

package main

import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/masseelch/elk"
)

func main() {
ex, err := elk.NewExtension(
elk.GenerateSpec(
"openapi.json",
// It is a Content-Management-System ...
elk.SpecTitle("Fridge CMS"),
// You can use CommonMark syntax (https://commonmark.org/).
elk.SpecDescription("API to manage fridges and their cooled contents. **ICY!**"),
elk.SpecVersion("0.0.1"),
),
)
if err != nil {
log.Fatalf("creating elk extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

Al volver a ejecutar el generador de código se creará un archivo OAS actualizado que puedes copiar y pegar en el Swagger Editor.

Updated API Info

Updated API Info

Configuración de operaciones

No queremos exponer endpoints para eliminar frigoríficos (¿en serio, a quién se le ocurriría?). Afortunadamente, elk nos permite configurar qué endpoints generar y cuáles ignorar. La política predeterminada de elk es exponer todas las rutas. Puedes cambiar este comportamiento para no exponer ninguna ruta excepto las solicitadas explícitamente, o simplemente indicar a elk que excluya la operación DELETE en Fridge usando una elk.SchemaAnnotation:

ent/schema/fridge.go
// Annotations of the Fridge.
func (Fridge) Annotations() []schema.Annotation {
return []schema.Annotation{
elk.DeletePolicy(elk.Exclude),
}
}

¡Y voilà! La operación DELETE ha desaparecido.

DELETE operation is gone

DELETE operation is gone

Para más información sobre cómo funcionan las políticas de elk y lo que puedes hacer con ellas, consulta la godoc.

Extender la especificación

Lo más interesante en este ejemplo sería conocer el contenido actual de un frigorífico. Puedes personalizar el OAS generado hasta donde desees usando Hooks. Sin embargo, esto excedería el alcance de esta publicación. Un ejemplo de cómo añadir un endpoint fridges/{id}/contents al archivo OAS generado puede encontrarse aquí.

Generar un servidor compatible con OAS

Al principio prometí que crearíamos un servidor que se comporte como describe el OAS. elk lo hace fácil: solo debes llamar a elk.GenerateHandlers() al configurar la extensión:

ent/entc.go
[...]
func main() {
ex, err := elk.NewExtension(
elk.GenerateSpec(
[...]
),
+ elk.GenerateHandlers(),
)
[...]
}

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

go generate ./...

Observa que se ha creado un nuevo directorio llamado ent/http.

» tree ent/http
ent/http
├── create.go
├── delete.go
├── easyjson.go
├── handler.go
├── list.go
├── read.go
├── relations.go
├── request.go
├── response.go
└── update.go

0 directories, 10 files

Puedes iniciar el servidor generado con este sencillo main.go:

package main

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

"<your-project>/ent"
elk "<your-project>/ent/http"

_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
)

func main() {
// Create the ent client.
c, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer c.Close()
// Run the auto migration tool.
if err := c.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Start listen to incoming requests.
if err := http.ListenAndServe(":8080", elk.NewHandler(c, zap.NewExample())); err != nil {
log.Fatal(err)
}
}
go run -mod=mod main.go

Nuestro servidor de Fridge API está en funcionamiento. Con el archivo OAS generado y las herramientas Swagger, ahora puedes generar un stub de cliente en cualquier lenguaje soportado y olvidarte de escribir un cliente RESTful... ¡para siempre!

Conclusión

En esta publicación presentamos una nueva característica de elk: la generación automática de Especificaciones OpenAPI. Esta función conecta las capacidades de generación de código de Ent con el 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:]

· 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 →

Hace unos meses, Ariel realizó una contribución silenciosa pero de gran impacto al núcleo de Ent: la API de Extensiones. Aunque Ent ya contaba con capacidades de extensión (como Hooks de generación de código, Plantillas externas y Anotaciones) desde hace tiempo, no existía una forma conveniente de agrupar todos estos componentes en una unidad coherente y autocontenida. La API de Extensiones que analizamos en esta publicación logra precisamente eso.

Muchos ecosistemas de código abierto prosperan específicamente porque ofrecen a los desarrolladores una forma sencilla y estructurada de extender un sistema central pequeño. Se ha criticado mucho al ecosistema Node.js (incluso por su creador original Ryan Dahl), pero es difícil argumentar que la facilidad para publicar y consumir nuevos módulos npm no facilitó la explosión de su popularidad. En mi blog personal he analizado cómo funciona el sistema de plugins de protoc y cómo eso hizo prosperar el ecosistema Protobuf. En resumen, los ecosistemas solo se crean bajo diseños modulares.

En la publicación de hoy, exploraremos la API de Extension de Ent mediante un ejemplo práctico.

Empezando

La API de Extensiones solo funciona en proyectos que utilizan la generación de código de Ent como paquete de Go. Para configurarlo, después de inicializar tu proyecto, crea un archivo llamado ent/entc.go:

ent/entc.go
//+build ignore

package main

import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/ent/schema/field"
)

func main() {
err := entc.Generate("./schema", &gen.Config{})
if err != nil {
log.Fatal("running ent codegen:", err)
}
}

A continuación, modifica ent/generate.go para invocar nuestro archivo entc:

ent/generate.go
package ent

//go:generate go run entc.go

Creando nuestra extensión

Todas las extensiones deben implementar la interfaz Extension:

type Extension interface {
// Hooks holds an optional list of Hooks to apply
// on the graph before/after the code-generation.
Hooks() []gen.Hook
// Annotations injects global annotations to the gen.Config object that
// can be accessed globally in all templates. Unlike schema annotations,
// being serializable to JSON raw value is not mandatory.
//
// {{- with $.Config.Annotations.GQL }}
// {{/* Annotation usage goes here. */}}
// {{- end }}
//
Annotations() []Annotation
// Templates specifies a list of alternative templates
// to execute or to override the default.
Templates() []*gen.Template
// Options specifies a list of entc.Options to evaluate on
// the gen.Config before executing the code generation.
Options() []Option
}

Para simplificar el desarrollo de nuevas extensiones, los desarrolladores pueden incrustar entc.DefaultExtension para crear extensiones sin implementar todos los métodos. En entc.go, añade:

ent/entc.go
// ...

// GreetExtension implements entc.Extension.
type GreetExtension {
entc.DefaultExtension
}

Actualmente, nuestra extensión no hace nada. Ahora, conectémosla a nuestra configuración de generación de código. En entc.go, añade nuestra nueva extensión a la invocación de entc.Generate:

err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(&GreetExtension{})

Añadiendo plantillas

Las plantillas externas pueden agruparse en extensiones para mejorar la funcionalidad central de generación de código de Ent. En nuestro ejemplo práctico, nuestro objetivo es añadir a cada entidad un método generado llamado Greet que devuelva un saludo con el nombre del tipo cuando se invoque. Buscamos algo como:

func (u *User) Greet() string {
return "Greetings, User"
}

Para hacer esto, añadamos un nuevo archivo de plantilla externa y coloquémoslo en ent/templates/greet.tmpl:

ent/templates/greet.tmpl
{{ define "greet" }}

{{/* Add the base header for the generated file */}}
{{ $pkg := base $.Config.Package }}
{{ template "header" $ }}

{{/* Loop over all nodes and add the Greet method */}}
{{ range $n := $.Nodes }}
{{ $receiver := $n.Receiver }}
func ({{ $receiver }} *{{ $n.Name }}) Greet() string {
return "Greetings, {{ $n.Name }}"
}
{{ end }}
{{ end }}

A continuación, implementemos el método Templates:

ent/entc.go
func (*GreetExtension) Templates() []*gen.Template {
return []*gen.Template{
gen.MustParse(gen.NewTemplate("greet").ParseFiles("templates/greet.tmpl")),
}
}

Ahora probemos nuestra extensión. Añade un nuevo esquema para el tipo User en un archivo llamado ent/schema/user.go:

package schema

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

// 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("email_address").
Unique(),
}
}

Luego ejecuta:

go generate ./...

Observa que se creó un nuevo archivo, ent/greet.go, que contiene:

ent/greet.go
// Code generated by ent, DO NOT EDIT.

package ent

func (u *User) Greet() string {
return "Greetings, User"
}

¡Genial! Nuestra extensión fue invocada desde la generación de código de Ent y produjo el código que queríamos para nuestro esquema.

Añadiendo anotaciones

Las anotaciones proporcionan una forma de ofrecer a los usuarios de nuestra extensión una API para modificar el comportamiento de la lógica de generación de código. Para añadir anotaciones a nuestra extensión, implementamos el método Annotations. Supongamos que para nuestra GreetExtension queremos permitir a los usuarios configurar la palabra de saludo en el código generado:

// GreetingWord implements entc.Annotation
type GreetingWord string

func (GreetingWord) Name() string {
return "GreetingWord"
}

Primero, añadimos un campo word a nuestra estructura GreetExtension:

type GreetExtension struct {
entc.DefaultExtension
Word GreetingWord
}

Luego, implementamos el método Annotations:

func (s *GreetExtension) Annotations() []entc.Annotation {
return []entc.Annotation{
s.Word,
}
}

Ahora, desde nuestras plantillas podemos acceder a la anotación GreetingWord. Modificamos ent/templates/greet.tmpl para usar nuestra nueva anotación:

func ({{ $receiver }} *{{ $n.Name }}) Greet() string {
return "{{ $.Annotations.GreetingWord }}, {{ $n.Name }}"
}

A continuación, modificamos la configuración de generación de código para establecer la anotación GreetingWord:

"ent/entc.go
err := entc.Generate("./schema",
&gen.Config{},
entc.Extensions(&GreetExtension{
Word: GreetingWord("Shalom"),
}),
)

Para ver cómo nuestra anotación controla el código generado, volvemos a ejecutar:

go generate ./...

Finalmente, observamos que el archivo generado ent/greet.go se actualizó:

func (u *User) Greet() string {
return "Shalom, User"
}

¡Hurra! ¡Hemos añadido una opción para usar una anotación que controla la palabra de saludo en el método generado Greet!

Más posibilidades

Además de plantillas y anotaciones, la API de Extensiones permite empaquetar gen.Hooks y entc.Options en extensiones para controlar aún más el comportamiento de la generación de código. En este post no discutiremos estas posibilidades, pero si estás interesado en usarlas, visita la documentación.

Conclusión

En este post hemos explorado mediante un ejemplo sencillo cómo usar la API Extension para crear nuevas extensiones de generación de código en Ent. Como mencionamos anteriormente, el diseño modular que permite a cualquiera extender la funcionalidad central del software es crucial para el éxito de cualquier ecosistema. Estamos viendo cómo esta afirmación comienza a materializarse en la comunidad de Ent. Aquí tienes una lista de algunos proyectos interesantes que usan la API de Extensiones:

  • elk - una extensión para generar endpoints REST a partir de esquemas Ent.

  • entgql - genera servidores GraphQL a partir de esquemas Ent.

  • entviz - genera diagramas ER a partir de esquemas Ent.

¿Y tú? ¿Tienes una idea para una extensión útil de Ent? Espero que este post haya demostrado que con la nueva API de Extensiones, no es una tarea difícil.

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

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

· 2 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 enormemente compartir algo que llevamos preparando desde hace tiempo. Ayer, 31 de agosto, se publicó un comunicado de prensa anunciando que Ent se une a la Linux Foundation.

Ent se convirtió en código abierto mientras trabajaba en ello con mis compañeros de Facebook en 2019. Desde entonces, nuestra comunidad ha crecido y hemos visto cómo la adopción de Ent se ha disparado en organizaciones de todos los tamaños y sectores.

Nuestro objetivo al pasar a estar bajo el gobierno de la Linux Foundation es crear un entorno corporativamente neutral donde las organizaciones puedan contribuir código más fácilmente, como hemos visto en otros proyectos OSS exitosos como Kubernetes y GraphQL. Además, este movimiento posiciona a Ent donde siempre hemos querido que esté: como una tecnología de infraestructura fundamental en la que las organizaciones pueden confiar porque está garantizado que permanecerá durante mucho tiempo.

En cuanto a nuestra comunidad, no cambia nada en particular: el repositorio ya se trasladó a github.com/ent/ent hace unos meses, la licencia sigue siendo Apache 2.0 y todos estamos 100% comprometidos con el éxito del proyecto. Estamos seguros de que la sólida marca y capacidades organizativas de la Linux Foundation ayudarán a generar aún más confianza en Ent y fomentarán su adopción en la industria.

Quiero expresar mi profunda gratitud a las increíbles personas de Facebook y la Linux Foundation que han trabajado arduamente para hacer posible este cambio y han depositado su confianza en nuestra comunidad para seguir innovando en frameworks de acceso a datos. Este es un gran logro para nuestra comunidad, así que quiero tomarme un momento para agradeceros a todos por vuestras contribuciones, apoyo y confianza en este proyecto.

A nivel personal, quería compartir que Rotem (colaborador principal de Ent) y yo hemos fundado una nueva empresa, Ariga. Nuestra misión es construir lo que llamamos un "grafo de datos operacional" desarrollado principalmente con Ent, y compartiremos más detalles próximamente. Podéis esperar muchas funciones nuevas y emocionantes aportadas al framework por nuestro equipo. Además, los empleados de Ariga dedicarán tiempo y recursos para apoyar y fomentar esta maravillosa comunidad.

Si tenéis alguna pregunta sobre este cambio o ideas para mejorarlo, no dudéis en contactarme en nuestro servidor de Discord o canal de Slack.

Ariel ❤️

· 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 →

Incorporarse a un proyecto existente con una base de código extensa puede ser una tarea abrumadora.

Comprender el modelo de datos de una aplicación es fundamental para que los desarrolladores puedan empezar a trabajar en un proyecto existente. Una herramienta comúnmente utilizada para superar este desafío y ayudar a los desarrolladores a entender el modelo de datos de una aplicación es un diagrama ER (Entidad-Relación).

Los diagramas ER proporcionan una representación visual de tu modelo de datos y detallan cada campo de las entidades. Muchas herramientas pueden ayudar a crearlos, como por ejemplo Jetbrains DataGrip, que puede generar un diagrama ER conectándose e inspeccionando una base de datos existente:

Datagrip ER diagram

DataGrip ER diagram example

Ent, un framework de entidades simple pero potente para Go, fue desarrollado originalmente dentro de Facebook específicamente para manejar proyectos con modelos de datos grandes y complejos. Por eso Ent utiliza generación de código: proporciona seguridad de tipos y autocompletado de código desde el primer momento, lo que ayuda a explicar el modelo de datos y mejora la velocidad de desarrollo. Además de todo esto, ¿no sería genial generar automáticamente diagramas ER que mantengan una visión de alto nivel del modelo de datos en una representación visualmente atractiva? (Vamos, ¿a quién no le gustan las visualizaciones?)

Presentando entviz

entviz es una extensión de ent que genera automáticamente una página HTML estática que visualiza tu grafo de datos.

Entviz example output

Entviz example output

La mayoría de herramientas para generar diagramas ER necesitan conectarse a tu base de datos e inspeccionarla, lo que dificulta mantener un diagrama actualizado del esquema de la base de datos. Como entviz se integra directamente con tu esquema de Ent, no necesita conectarse a tu base de datos y genera automáticamente una visualización fresca cada vez que modificas tu esquema.

Si quieres saber más sobre cómo se implementó entviz, echa un vistazo a la sección de implementación.

Verlo en acción

Primero, añadamos la extensión entviz a nuestro archivo entc.go:

go get github.com/hedwigz/entviz
información

Si no estás familiarizado con entc, te invitamos a leer la documentación de entc para aprender más sobre ello.

ent/entc.go
import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/hedwigz/entviz"
)

func main() {
err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(entviz.Extension{}))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

Supongamos que tenemos un esquema simple con una entidad de usuario y algunos campos:

ent/schema/user.go
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.String("email"),
field.Time("created").
Default(time.Now),
}
}

Ahora, entviz generará automáticamente una visualización de nuestro grafo cada vez que ejecutemos:

go generate ./...

Deberías ver ahora un nuevo archivo llamado schema-viz.html en tu directorio de ent:

$ ll ./ent/schema-viz.html
-rw-r--r-- 1 hedwigz hedwigz 7.3K Aug 27 09:00 schema-viz.html

Abre el archivo html con tu navegador favorito para ver la visualización

tutorial image

A continuación, añadamos otra entidad llamada Post y veamos cómo cambia nuestra visualización:

ent new Post
ent/schema/post.go
// Fields of the Post.
func (Post) Fields() []ent.Field {
return []ent.Field{
field.String("content"),
field.Time("created").
Default(time.Now),
}
}

Ahora añadimos una relación (O2M) de User a Post:

ent/schema/post.go
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("posts", Post.Type),
}
}

Finalmente, regeneramos el código:

go generate ./...

¡Actualiza tu navegador para ver el resultado actualizado!

tutorial image 2

Implementación

Entviz se implementó extendiendo ent mediante su API de extensiones. La API de extensiones de Ent permite combinar múltiples plantillas, hooks, opciones y anotaciones. Por ejemplo, entviz utiliza plantillas para añadir otro archivo Go, entviz.go, que expone el método ServeEntviz que puede usarse como manejador HTTP de la siguiente forma:

func main() {
http.ListenAndServe("localhost:3002", ent.ServeEntviz())
}

Definimos una estructura de extensión que incorpora la extensión por defecto, y exportamos nuestra plantilla mediante el método Templates:

//go:embed entviz.go.tmpl
var tmplfile string

type Extension struct {
entc.DefaultExtension
}

func (Extension) Templates() []*gen.Template {
return []*gen.Template{
gen.MustParse(gen.NewTemplate("entviz").Parse(tmplfile)),
}
}

El archivo de plantilla contiene el código que queremos generar:

{{ define "entviz"}}

{{ $pkg := base $.Config.Package }}
{{ template "header" $ }}
import (
_ "embed"
"net/http"
"strings"
"time"
)

//go:embed schema-viz.html
var html string

func ServeEntviz() http.Handler {
generateTime := time.Now()
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
http.ServeContent(w, req, "schema-viz.html", generateTime, strings.NewReader(html))
})
}
{{ end }}

¡Y eso es todo! Ahora tenemos un nuevo método en el paquete ent.

Resumen final

Hemos visto cómo los diagramas ER ayudan a los desarrolladores a mantener el control de su modelo de datos. Luego presentamos entviz, una extensión de Ent que genera automáticamente diagramas ER para esquemas de Ent. Observamos cómo entviz utiliza la API de extensiones de Ent para ampliar la generación de código y añadir funcionalidad adicional. Finalmente, has podido verlo en acción instalando y usando entviz en tu propio proyecto. Si te gusta el código y/o quieres contribuir, no dudes en revisar 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:]

· 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 →

La observabilidad es una cualidad de un sistema que se refiere a qué tan bien se puede medir su estado interno desde el exterior. A medida que un programa informático evoluciona hacia un sistema de producción completo, esta cualidad se vuelve cada vez más importante. Una de las formas de hacer que un sistema de software sea más observable es exportar métricas, es decir, informar de alguna manera externamente visible una descripción cuantitativa del estado del sistema en funcionamiento. Por ejemplo, exponer un endpoint HTTP donde podamos ver cuántos errores han ocurrido desde que el proceso comenzó. En esta publicación, exploraremos cómo construir aplicaciones Ent más observables usando Prometheus.

¿Qué es Ent?

Ent es un marco de entidades simple pero poderoso para Go, que facilita la creación y mantenimiento de aplicaciones con modelos de datos complejos.

¿Qué es Prometheus?

Prometheus es un sistema de monitoreo de código abierto desarrollado por ingeniería en SoundCloud en 2012. Incluye una base de datos de series temporales integrada y muchas integraciones con sistemas de terceros. El cliente de Prometheus expone las métricas del proceso a través de un endpoint HTTP (generalmente /metrics). Este endpoint es descubierto por el scraper de Prometheus que lo consulta periódicamente (normalmente cada 30s) y escribe los datos en una base de datos de series temporales.

Prometheus es solo un ejemplo de una clase de backends de recolección de métricas. Existen muchos otros, como AWS CloudWatch, InfluxDB y otros, ampliamente utilizados en la industria. Hacia el final de esta publicación, discutiremos un posible camino para una integración unificada y basada en estándares con cualquier backend de este tipo.

Trabajando con Prometheus

Para exponer las métricas de una aplicación usando Prometheus, necesitamos crear un Collector de Prometheus. Un collector recopila un conjunto de métricas desde tu servidor.

En nuestro ejemplo, usaremos dos tipos de métricas que pueden almacenarse en un collector: Contadores e Histogramas. Los contadores son métricas acumulativas monótonamente crecientes que representan cuántas veces ha ocurrido algo, comúnmente usados para contar el número de solicitudes que un servidor ha procesado o errores que han ocurrido. Los histogramas agrupan observaciones en cubos de tamaños configurables y se usan comúnmente para representar distribuciones de latencia (ej. cuántas solicitudes respondieron en menos de 5ms, 10ms, 100ms, 1s, etc.). Además, Prometheus permite desglosar métricas mediante etiquetas. Esto es útil, por ejemplo, para contar solicitudes desglosando el contador por nombre del endpoint.

Veamos cómo crear tal collector usando el cliente oficial de Go. Para ello, usaremos un paquete del cliente llamado promauto que simplifica el proceso de creación de collectors. Un ejemplo simple de un collector que cuenta (por ejemplo, solicitudes totales o número de errores en solicitudes):

package example

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
// List of dynamic labels
labelNames = []string{"endpoint", "error_code"}

// Create a counter collector
exampleCollector = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "endpoint_errors",
Help: "Number of errors in endpoints",
},
labelNames,
)
)

// When using you set the values of the dynamic labels and then increment the counter
func incrementError() {
exampleCollector.WithLabelValues("/create-user", "400").Inc()
}

Hooks de Ent

Los Hooks son una característica de Ent que permite agregar lógica personalizada antes y después de operaciones que modifican las entidades de datos.

Una mutación es una operación que cambia algo en la base de datos. Existen 5 tipos de mutaciones:

  1. Create (Crear).

  2. UpdateOne (Actualizar uno).

  3. Update (Actualizar).

  4. DeleteOne (Eliminar uno).

  5. Delete (Eliminar).

Los hooks son funciones que reciben un ent.Mutator y devuelven un mutador. Funcionan de manera similar al popular patrón de middleware HTTP.

package example

import (
"context"

"entgo.io/ent"
)

func exampleHook() ent.Hook {
//use this to init your hook
return func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// Do something before mutation.
v, err := next.Mutate(ctx, m)
if err != nil {
// Do something if error after mutation.
}
// Do something after mutation.
return v, err
})
}
}

En Ent, existen dos tipos de hooks para mutaciones: hooks de esquema y hooks en tiempo de ejecución. Los hooks de esquema se utilizan principalmente para definir lógica personalizada de mutación en un tipo de entidad específico, por ejemplo, sincronizar la creación de una entidad con otro sistema. Los hooks en tiempo de ejecución, por otro lado, se emplean para definir lógica más global que añade funcionalidades como registro de eventos, métricas, trazabilidad, etc.

Para nuestro caso de uso, definitivamente deberíamos utilizar hooks en tiempo de ejecución, porque para que sea valioso necesitamos exportar métricas sobre todas las operaciones en todos los tipos de entidades:

package example

import (
"entprom/ent"
"entprom/ent/hook"
)

func main() {
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")

// Add a hook only on user mutations.
client.User.Use(exampleHook())

// Add a hook only on update operations.
client.Use(hook.On(exampleHook(), ent.OpUpdate|ent.OpUpdateOne))
}

Exportando métricas de Prometheus para una aplicación Ent

Con todas las introducciones completadas, vayamos al grano y mostremos cómo usar Prometheus junto con hooks de Ent para crear una aplicación observable. Nuestro objetivo con este ejemplo es exportar estas métricas mediante un hook:

Metric NameDescription
ent_operation_totalNumber of ent mutation operations
ent_operation_errorNumber of failed ent mutation operations
ent_operation_duration_secondsTime in seconds per operation

Cada una de estas métricas se desglosará mediante etiquetas en dos dimensiones:

  • mutation_type: Tipo de entidad que se está mutando (User, BlogPost, Account, etc.).

  • mutation_op: Operación que se está realizando (Create, Delete, etc.).

Comencemos definiendo nuestros recolectores:

//Ent dynamic dimensions
const (
mutationType = "mutation_type"
mutationOp = "mutation_op"
)

var entLabels = []string{mutationType, mutationOp}

// Create a collector for total operations counter
func initOpsProcessedTotal() *prometheus.CounterVec {
return promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "ent_operation_total",
Help: "Number of ent mutation operations",
},
entLabels,
)
}

// Create a collector for error counter
func initOpsProcessedError() *prometheus.CounterVec {
return promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "ent_operation_error",
Help: "Number of failed ent mutation operations",
},
entLabels,
)
}

// Create a collector for duration histogram collector
func initOpsDuration() *prometheus.HistogramVec {
return promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "ent_operation_duration_seconds",
Help: "Time in seconds per operation",
},
entLabels,
)
}

A continuación, definamos nuestro nuevo hook:

// Hook init collectors, count total at beginning error on mutation error and duration also after.
func Hook() ent.Hook {
opsProcessedTotal := initOpsProcessedTotal()
opsProcessedError := initOpsProcessedError()
opsDuration := initOpsDuration()
return func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// Before mutation, start measuring time.
start := time.Now()
// Extract dynamic labels from mutation.
labels := prometheus.Labels{mutationType: m.Type(), mutationOp: m.Op().String()}
// Increment total ops counter.
opsProcessedTotal.With(labels).Inc()
// Execute mutation.
v, err := next.Mutate(ctx, m)
if err != nil {
// In case of error increment error counter.
opsProcessedError.With(labels).Inc()
}
// Stop time measure.
duration := time.Since(start)
// Record duration in seconds.
opsDuration.With(labels).Observe(duration.Seconds())
return v, err
})
}
}

Conectando el recolector de Prometheus a nuestro servicio

Después de definir nuestro hook, veamos cómo conectarlo a nuestra aplicación y cómo usar Prometheus para servir un endpoint que exponga las métricas de nuestros recolectores:

package main

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

"entprom"
"entprom/ent"

_ "github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

func createClient() *ent.Client {
c, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
ctx := context.Background()
// Run the auto migration tool.
if err := c.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
return c
}

func handler(client *ent.Client) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// Run operations.
_, err := client.User.Create().SetName("a8m").Save(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
}

func main() {
// Create Ent client and migrate
client := createClient()
// Use the hook
client.Use(entprom.Hook())
// Simple handler to run actions on our DB.
http.HandleFunc("/", handler(client))
// This endpoint sends metrics to the prometheus to collect
http.Handle("/metrics", promhttp.Handler())
log.Println("server starting on port 8080")
// Run the server
log.Fatal(http.ListenAndServe(":8080", nil))
}

Tras acceder varias veces a / en nuestro servidor (usando curl o un navegador), visita /metrics. Allí verás la salida del cliente de Prometheus:

# HELP ent_operation_duration_seconds Time in seconds per operation
# TYPE ent_operation_duration_seconds histogram
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.005"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.01"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.025"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.05"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.1"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.25"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.5"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="1"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="2.5"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="5"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="10"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="+Inf"} 2
ent_operation_duration_seconds_sum{mutation_op="OpCreate",mutation_type="User"} 0.000265669
ent_operation_duration_seconds_count{mutation_op="OpCreate",mutation_type="User"} 2
# HELP ent_operation_error Number of failed ent mutation operations
# TYPE ent_operation_error counter
ent_operation_error{mutation_op="OpCreate",mutation_type="User"} 1
# HELP ent_operation_total Number of ent mutation operations
# TYPE ent_operation_total counter
ent_operation_total{mutation_op="OpCreate",mutation_type="User"} 2

En la parte superior, podemos ver el histograma calculado, que cuenta el número de operaciones en cada "cubo". Después, vemos el número total de operaciones y la cantidad de errores. Cada métrica incluye su descripción, visible al consultar con el panel de Prometheus.

El cliente de Prometheus es solo un componente de su arquitectura. Para ejecutar un sistema completo que incluya un recolector que sondee tu endpoint, un Prometheus que almacene tus métricas y pueda responder consultas, y una interfaz simple para interactuar, recomiendo leer la documentación oficial o usar el docker-compose.yaml en este repositorio de ejemplo.

Trabajo futuro en observabilidad para Ent

Como mencionamos antes, existe una abundancia de backends para recolección de métricas disponibles hoy, siendo Prometheus solo uno de muchos proyectos exitosos. Aunque estas soluciones difieren en múltiples dimensiones (autohospedadas vs SaaS, diferentes motores de almacenamiento con lenguajes de consulta distintos, etc.), desde la perspectiva del cliente que reporta métricas son virtualmente idénticas.

En casos como este, los principios de buena ingeniería de software sugieren que el backend concreto debería abstraerse del cliente mediante una interfaz. Esta interfaz podría ser implementada por diferentes backends, permitiendo a las aplicaciones cliente cambiar fácilmente entre implementaciones. Cambios similares están ocurriendo en nuestra industria en años recientes. Consideremos, por ejemplo, la Open Container Initiative o la Service Mesh Interface: ambas son iniciativas que buscan definir una interfaz estándar para un espacio problemático. En el ámbito de la observabilidad, está ocurriendo exactamente la misma convergencia con OpenCensus y OpenTracing fusionándose actualmente en OpenTelemetry.

Por muy atractivo que sea publicar una extensión de Ent + Prometheus similar a la presentada en este post, somos firmes creyentes de que la observabilidad debe resolverse con un enfoque basado en estándares. Invitamos a todos a unirse a la discusión sobre cuál es la forma correcta de implementar esto para Ent.

Conclusión

Comenzamos esta publicación presentando Prometheus, una popular solución de monitoreo de código abierto. Luego revisamos los "Hooks", una característica de Ent que permite agregar lógica personalizada antes y después de operaciones que modifican entidades de datos. Mostramos cómo integrar ambos para crear aplicaciones observables con Ent. Finalmente, discutimos el futuro de la observabilidad en Ent e invitamos a todos a unirse a la discusión para darle forma.

¿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 →

Han pasado casi 4 meses desde nuestro último lanzamiento, y por buenos motivos. La versión 0.9.0, lanzada hoy, incluye características muy esperadas. Quizás la más destacada de la lista sea una función que lleva en discusión más de año y medio y que fue una de las más solicitadas en la Encuesta de Usuarios de Ent: ¡la API de Upsert!

La versión 0.9.0 añade soporte para sentencias tipo "Upsert" usando una nueva bandera de características: sql/upsert. Ent tiene una colección de banderas de características que pueden activarse para añadir funcionalidades adicionales al código generado por Ent. Esto sirve tanto como mecanismo para activar opcionalmente características no necesariamente deseadas en todos los proyectos, como para experimentar con funcionalidades que podrían integrarse en el núcleo de Ent en el futuro.

En esta publicación, presentaremos la nueva función, sus casos de uso y demostraremos cómo utilizarla.

Upsert

"Upsert" es un término común en sistemas de datos que combina "update" (actualizar) e "insert" (insertar). Normalmente se refiere a una sentencia que intenta insertar un registro en una tabla y, si se viola una restricción de unicidad (por ejemplo, ya existe un registro con ese ID), actualiza dicho registro en su lugar. Aunque ninguna base de datos relacional popular tiene una sentencia UPSERT específica, la mayoría admiten formas de lograr este comportamiento.

Por ejemplo, supongamos que tenemos una tabla con esta definición en una base de datos SQLite:

CREATE TABLE users (
id integer PRIMARY KEY AUTOINCREMENT,
email varchar(255) UNIQUE,
name varchar(255)
)

Si intentamos ejecutar la misma inserción dos veces:

INSERT INTO users (email, name) VALUES ('rotem@entgo.io', 'Rotem Tamir');
INSERT INTO users (email, name) VALUES ('rotem@entgo.io', 'Rotem Tamir');

Obtenemos este error:

[2021-08-05 06:49:22] UNIQUE constraint failed: users.email

En muchos casos, es útil que las operaciones de escritura sean idempotentes, lo que significa que podemos ejecutarlas múltiples veces consecutivas dejando el sistema en el mismo estado.

En otros escenarios, no es deseable consultar si existe un registro antes de intentar crearlo. Para estas situaciones, SQLite admite la cláusula ON CONFLICT en sentencias INSERT. Para indicar a SQLite que sobrescriba un valor existente con el nuevo, podemos ejecutar:

INSERT INTO users (email, name) values ('rotem@entgo.io', 'Tamir, Rotem')
ON CONFLICT (email) DO UPDATE SET email=excluded.email, name=excluded.name;

Si preferimos mantener los valores existentes, podemos usar la acción de conflicto DO NOTHING:

INSERT INTO users (email, name) values ('rotem@entgo.io', 'Tamir, Rotem') 
ON CONFLICT DO NOTHING;

A veces queremos fusionar ambas versiones. Podemos usar la acción DO UPDATE de forma ligeramente diferente para lograr algo como:

INSERT INTO users (email, full_name) values ('rotem@entgo.io', 'Tamir, Rotem') 
ON CONFLICT (email) DO UPDATE SET name=excluded.name || ' (formerly: ' || users.name || ')'

En este caso, tras nuestro segundo INSERT, el valor de la columna name sería: Tamir, Rotem (formerly: Rotem Tamir). No es muy útil, pero esperamos que veas que así puedes hacer cosas interesantes.

Upsert con Ent

Supongamos que tenemos un proyecto Ent existente con una entidad similar a la tabla users descrita anteriormente:

// 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("email").
Unique(),
field.String("name"),
}
}

Como la API de Upsert es una función recién lanzada, asegúrate de actualizar tu versión de ent con:

go get -u entgo.io/ent@v0.9.0

Luego, añade la bandera sql/upsert a tus flags de generación de código, en ent/generate.go:

package ent

//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/upsert ./schema

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

go generate ./...

Observa que se ha añadido un nuevo método llamado OnConflict al archivo ent/user_create.go:

// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
// of the `INSERT` statement. For example:
//
// client.User.Create().
// SetEmailAddress(v).
// OnConflict(
// // Update the row with the new values
// // the was proposed for insertion.
// sql.ResolveWithNewValues(),
// ).
// // Override some of the fields with custom
// // update values.
// Update(func(u *ent.UserUpsert) {
// SetEmailAddress(v+v)
// }).
// Exec(ctx)
//
func (uc *UserCreate) OnConflict(opts ...sql.ConflictOption) *UserUpsertOne {
uc.conflict = opts
return &UserUpsertOne{
create: uc,
}
}

Este método (junto con otro nuevo código generado) nos permitirá lograr el comportamiento de upsert para nuestra entidad User. Para explorarlo, empecemos escribiendo una prueba que reproduzca el error de restricción de unicidad:

func TestUniqueConstraintFails(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.TODO()

// Create the user for the first time.
client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Rotem Tamir").
SaveX(ctx)

// Try to create a user with the same email the second time.
_, err := client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Rotem Tamir").
Save(ctx)

if !ent.IsConstraintError(err) {
log.Fatalf("expected second created to fail with constraint error")
}
log.Printf("second query failed with: %v", err)
}

La prueba pasa:

=== RUN   TestUniqueConstraintFails
2021/08/05 07:12:11 second query failed with: ent: constraint failed: insert node to table "users": UNIQUE constraint failed: users.email
--- PASS: TestUniqueConstraintFails (0.00s)

Ahora veamos cómo indicar a Ent que sobrescriba los valores existentes con los nuevos en caso de conflicto:

func TestUpsertReplace(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.TODO()

// Create the user for the first time.
orig := client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Rotem Tamir").
SaveX(ctx)

// Try to create a user with the same email the second time.
// This time we set ON CONFLICT behavior, and use the `UpdateNewValues`
// modifier.
newID := client.User.Create().
SetEmail("rotem@entgo.io").
SetName("Tamir, Rotem").
OnConflict().
UpdateNewValues().
// we use the IDX method to receive the ID
// of the created/updated entity
IDX(ctx)

// We expect the ID of the originally created user to be the same as
// the one that was just updated.
if orig.ID != newID {
log.Fatalf("expected upsert to update an existing record")
}

current := client.User.GetX(ctx, orig.ID)
if current.Name != "Tamir, Rotem" {
log.Fatalf("expected upsert to replace with the new values")
}
}

Ejecutando nuestra prueba:

=== RUN   TestUpsertReplace
--- PASS: TestUpsertReplace (0.00s)

Alternativamente, podemos usar el modificador Ignore para indicar a Ent que mantenga la versión anterior al resolver el conflicto. Escribamos una prueba que demuestre esto:

func TestUpsertIgnore(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.TODO()

// Create the user for the first time.
orig := client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Rotem Tamir").
SaveX(ctx)

// Try to create a user with the same email the second time.
// This time we set ON CONFLICT behavior, and use the `Ignore`
// modifier.
client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Tamir, Rotem").
OnConflict().
Ignore().
ExecX(ctx)

current := client.User.GetX(ctx, orig.ID)
if current.FullName != orig.FullName {
log.Fatalf("expected upsert to keep the original version")
}
}

Puedes obtener más información sobre esta función en la documentación de Feature Flags o Upsert API.

Conclusión

En esta publicación, presentamos la API Upsert, una funcionalidad muy esperada que está disponible mediante una bandera de características en Ent v0.9.0. Discutimos dónde se usan comúnmente los upserts en aplicaciones y cómo se implementan en bases de datos relacionales comunes. Finalmente, mostramos un ejemplo simple de cómo empezar a usar la API Upsert con Ent.

¿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 →

Cuando decimos que uno de los principios fundamentales de Ent es "Esquema como Código", queremos decir algo más que "el DSL de Ent para definir entidades y sus relaciones se hace con código Go normal". El enfoque único de Ent, comparado con otros ORMs, es expresar toda la lógica relacionada con una entidad, como código, directamente en la definición del esquema.

Con Ent, los desarrolladores pueden escribir toda la lógica de autorización (llamada "Privacidad" en Ent) y todos los efectos secundarios de mutaciones (llamados "Hooks" en Ent) directamente en el esquema. Tener todo en el mismo lugar es muy conveniente, pero su verdadero poder se revela cuando se combina con la generación de código.

Si los esquemas se definen así, se hace posible generar automáticamente código para servidores de producción completamente funcionales. Si trasladamos la responsabilidad de las decisiones de autorización y efectos secundarios personalizados desde la capa RPC a la capa de datos, la implementación de los endpoints CRUD básicos (Crear, Leer, Actualizar y Eliminar) se vuelve tan genérica que puede ser generada por máquinas. Esta es exactamente la idea detrás de las populares extensiones GraphQL y gRPC de Ent.

Hoy nos complace presentar una nueva extensión de Ent llamada elk que puede generar automáticamente endpoints API RESTful completamente funcionales a partir de tus esquemas Ent. elk se esfuerza por automatizar todo el trabajo tedioso de configurar los endpoints CRUD básicos para cada entidad que añadas a tu grafo, incluyendo registro de actividad, validación del cuerpo de la solicitud, carga temprana de relaciones y serialización, todo ello evitando la reflexión y manteniendo la seguridad de tipos.

¡Comencemos!

Empezando

La versión final del código a continuación puede encontrarse en GitHub.

Comienza creando un nuevo proyecto de Go:

mkdir elk-example
cd elk-example
go mod init elk-example

Invoca el generador de código de ent y crea dos esquemas: User, Pet:

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

Tu proyecto debería tener ahora este aspecto:

.
├── ent
│ ├── generate.go
│ └── schema
│ ├── pet.go
│ └── user.go
├── go.mod
└── go.sum

A continuación, añade el paquete elk a nuestro proyecto:

go get -u github.com/masseelch/elk

elk utiliza la API de extensiones de Ent para integrarse con la generación de código de Ent. Esto requiere que usemos el paquete entc (ent codegen) como se describe aquí. Sigue estos tres pasos para habilitarlo y configurar Ent para trabajar con la extensión elk:

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

// +build ignore

package main

import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/masseelch/elk"
)

func main() {
ex, err := elk.NewExtension(
elk.GenerateSpec("openapi.json"),
elk.GenerateHandlers(),
)
if err != nil {
log.Fatalf("creating elk 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

3. elk utiliza algunos paquetes externos en su código generado. Actualmente, debes obtener estos paquetes manualmente una vez al configurar elk:

go get github.com/mailru/easyjson github.com/masseelch/render github.com/go-chi/chi/v5 go.uber.org/zap

¡Con estos pasos completados, todo está listo para usar nuestro Ent potenciado por elk! Para aprender más sobre Ent, cómo conectarse a diferentes tipos de bases de datos, ejecutar migraciones o trabajar con entidades, visita el Tutorial de Configuración.

Generación de manejadores HTTP CRUD con elk

Para generar los manejadores HTTP completamente funcionales, primero necesitamos crear una definición de esquema Ent. Abre y edita ent/schema/pet.go:

package schema

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

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

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

Hemos añadido dos campos a nuestra entidad Pet: name y age. El ent.Schema simplemente define los campos de nuestra entidad. Para generar código ejecutable a partir de nuestro esquema, ejecuta:

go generate ./...

Observa que, además de los archivos que Ent generaría normalmente, se ha creado un directorio llamado ent/http. Estos archivos fueron generados por la extensión elk y contienen el código para los manejadores HTTP generados. Por ejemplo, aquí tienes parte del código generado para una operación de lectura en la entidad Pet:

const (
PetCreate Routes = 1 << iota
PetRead
PetUpdate
PetDelete
PetList
PetRoutes = 1<<iota - 1
)

// PetHandler handles http crud operations on ent.Pet.
type PetHandler struct {
handler

client *ent.Client
log *zap.Logger
}

func NewPetHandler(c *ent.Client, l *zap.Logger) *PetHandler {
return &PetHandler{
client: c,
log: l.With(zap.String("handler", "PetHandler")),
}
}

// Read fetches the ent.Pet identified by a given url-parameter from the
// database and renders it to the client.
func (h *PetHandler) Read(w http.ResponseWriter, r *http.Request) {
l := h.log.With(zap.String("method", "Read"))
// ID is URL parameter.
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
l.Error("error getting id from url parameter", zap.String("id", chi.URLParam(r, "id")), zap.Error(err))
render.BadRequest(w, r, "id must be an integer greater zero")
return
}
// Create the query to fetch the Pet
q := h.client.Pet.Query().Where(pet.ID(id))
e, err := q.Only(r.Context())
if err != nil {
switch {
case ent.IsNotFound(err):
msg := stripEntError(err)
l.Info(msg, zap.Error(err), zap.Int("id", id))
render.NotFound(w, r, msg)
case ent.IsNotSingular(err):
msg := stripEntError(err)
l.Error(msg, zap.Error(err), zap.Int("id", id))
render.BadRequest(w, r, msg)
default:
l.Error("could not read pet", zap.Error(err), zap.Int("id", id))
render.InternalServerError(w, r, nil)
}
return
}
l.Info("pet rendered", zap.Int("id", id))
easyjson.MarshalToHTTPResponseWriter(NewPet2657988899View(e), w)
}

A continuación, veamos cómo crear un servidor HTTP RESTful real que pueda gestionar tus entidades Pet. Crea un archivo llamado main.go y añade el siguiente contenido:

package main

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

"elk-example/ent"
elk "elk-example/ent/http"

"github.com/go-chi/chi/v5"
_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
)

func main() {
// Create the ent client.
c, err := ent.Open("sqlite3", "./ent.db?_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer c.Close()
// Run the auto migration tool.
if err := c.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Router and Logger.
r, l := chi.NewRouter(), zap.NewExample()
// Create the pet handler.
r.Route("/pets", func(r chi.Router) {
elk.NewPetHandler(c, l).Mount(r, elk.PetRoutes)
})
// Start listen to incoming requests.
fmt.Println("Server running")
defer fmt.Println("Server stopped")
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal(err)
}
}

Luego, inicia el servidor:

go run -mod=mod main.go

¡Enhorabuena! Ahora tenemos un servidor en funcionamiento que sirve la API de Pets. Podríamos pedir al servidor una lista de todas las mascotas en la base de datos, pero aún no hay ninguna. Primero creemos una:

curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Kuro","age":3}' 'localhost:8080/pets'

Deberías obtener esta respuesta:

{
"age": 3,
"id": 1,
"name": "Kuro"
}

Si miras en la terminal donde se está ejecutando el servidor, también puedes ver el registro integrado de elk:

{
"level": "info",
"msg": "pet rendered",
"handler": "PetHandler",
"method": "Create",
"id": 1
}

elk utiliza zap para el registro. Para saber más sobre él, consulta su documentación.

Relaciones

Para ilustrar más características de elk, ampliemos nuestro gráfico. Edita ent/schema/user.go y ent/schema/pet.go:

ent/schema/pet.go
// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique(),
}
}

ent/schema/user.go
package schema

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

// 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("name"),
field.Int("age"),
}
}

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

Hemos creado una relación Uno-a-Muchos entre los esquemas Pet y User: una mascota pertenece a un usuario, y un usuario puede tener varias mascotas.

Vuelve a ejecutar el generador de código:

go generate ./...

No olvides registrar el UserHandler en nuestro enrutador. Añade las siguientes líneas a main.go:

[...]
r.Route("/pets", func(r chi.Router) {
elk.NewPetHandler(c, l, v).Mount(r, elk.PetRoutes)
})
+ // Create the user handler.
+ r.Route("/users", func(r chi.Router) {
+ elk.NewUserHandler(c, l, v).Mount(r, elk.UserRoutes)
+ })
// Start listen to incoming requests.
fmt.Println("Server running")
[...]

Después de reiniciar el servidor, podemos crear un User que sea dueño de la mascota Kuro que creamos anteriormente:

curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Elk","age":30,"owner":1}' 'localhost:8080/users'

El servidor devuelve la siguiente respuesta:

{
"age": 30,
"edges": {},
"id": 1,
"name": "Elk"
}

En la salida podemos ver que el usuario se ha creado, pero las aristas están vacías. Por defecto, elk no incluye las aristas en su salida. Puedes configurar elk para representar las aristas usando una característica llamada "grupos de serialización". Anota tus esquemas con las estructuras elk.SchemaAnnotation y elk.Annotation. Edita ent/schema/user.go y añádelas:

// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type).
Annotations(elk.Groups("user")),
}
}

// Annotations of the User.
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{elk.ReadGroups("user")}
}

Las elk.Annotation añadidas a los campos y aristas indican a elk que los cargue de forma ansiosa y los añada a la carga útil si se solicita el grupo "user". La elk.SchemaAnnotation se utiliza para que la operación de lectura del UserHandler solicite "user". Ten en cuenta que cualquier campo que no tenga un grupo de serialización asociado se incluye por defecto. Las aristas, sin embargo, se excluyen a menos que se configure lo contrario.

A continuación, regenera el código una vez más y reinicia el servidor. Ahora deberías ver las mascotas de un usuario representadas si lees un recurso:

curl 'localhost:8080/users/1'
{
"age": 30,
"edges": {
"pets": [
{
"id": 1,
"name": "Kuro",
"age": 3,
"edges": {}
}
]
},
"id": 1,
"name": "Elk"
}

Validación de solicitudes

Nuestros esquemas actuales permiten establecer una edad negativa para mascotas o usuarios y podemos crear mascotas sin dueño (como hicimos con Kuro). Ent tiene soporte integrado para validaciones básicas. En algunos casos, es posible que desees validar las solicitudes realizadas a tu API antes de pasar sus datos a Ent. elk utiliza este paquete para definir reglas de validación y validar datos. Podemos crear reglas de validación separadas para operaciones de Creación y Actualización usando elk.Annotation. En nuestro ejemplo, supongamos que queremos que nuestro esquema Pet solo permita edades mayores que cero y prohiba crear una mascota sin dueño. Edita ent/schema/pet.go:

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age").
Positive().
Annotations(
elk.CreateValidation("required,gt=0"),
elk.UpdateValidation("gt=0"),
),
}
}

// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique().
Required().
Annotations(elk.Validation("required")),
}
}

A continuación, regenera el código y reinicia el servidor. Para probar nuestras nuevas reglas de validación, intentemos crear una mascota con edad inválida y sin dueño:

curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Bob","age":-2}' 'localhost:8080/pets'

elk devuelve una respuesta detallada que incluye información sobre qué validaciones fallaron:

{
"code": 400,
"status": "Bad Request",
"errors": {
"Age": "This value failed validation on 'gt:0'.",
"Owner": "This value is required."
}
}

Observa los nombres de campos en mayúsculas. El paquete de validación usa el nombre del campo en las estructuras para generar sus errores de validación, pero puedes sobrescribirlo fácilmente, como se muestra en el ejemplo.

Si no defines reglas de validación, elk no incluirá el código de validación en su salida generada. La validación de solicitudes de elk es especialmente útil si necesitas realizar validación entre campos.

Próximas características

Esperamos que estés de acuerdo en que elk ya tiene características útiles, pero aún quedan muchas cosas emocionantes por venir. La próxima versión de elk incluirá:

  • Frontend de Flutter completamente funcional para administrar tus nodos

  • Integración de la validación de Ent en el validador actual de solicitudes

  • Más formatos de transporte (actualmente solo JSON)

Conclusión

Esta publicación muestra solo una pequeña parte de lo que elk puede hacer. Para ver más ejemplos de sus capacidades, visita el README del proyecto en GitHub. Espero que con Ent potenciado por elk, tú y tus compañeros desarrolladores puedan automatizar tareas repetitivas en la construcción de APIs RESTful y centrarse en trabajos más significativos.

elk está en una etapa temprana de desarrollo, agradecemos cualquier sugerencia o feedback. Si estás dispuesto a ayudar, nos encantaría. Los GitHub Issues son un lugar excelente para solicitar ayuda, dar feedback, sugerir ideas o contribuir.

Sobre el autor

MasseElch es ingeniero de software del ventoso y llano norte de Alemania. Cuando no está haciendo senderismo con su perro Kuro (quien tiene su propio canal de Instagram 😱) o jugando al escondite con su hijo, toma café y disfruta programando.

· 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 →

Los bloqueos son uno de los pilares fundamentales de cualquier programa informático concurrente. Cuando ocurren múltiples operaciones simultáneamente, los programadores recurren a bloqueos para garantizar la exclusión mutua del acceso concurrente a un recurso. Estos mecanismos (y otras primitivas de exclusión mutua) existen en múltiples niveles de la pila tecnológica, desde instrucciones de CPU de bajo nivel hasta APIs de aplicación como sync.Mutex en Go.

Al trabajar con bases de datos relacionales, una necesidad común para los desarrolladores es la capacidad de adquirir bloqueos sobre registros. Imagina una tabla inventory que lista artículos disponibles en un sitio de comercio electrónico. Esta tabla podría tener una columna state con valores available o purchased. Para evitar que dos usuarios crean haber comprado el mismo artículo, la aplicación debe impedir que dos operaciones modifiquen simultáneamente el estado del artículo de disponible a comprado.

¿Cómo garantiza esto la aplicación? No basta con que el servidor verifique si el artículo está available antes de marcarlo como purchased. Imagina que dos usuarios intentan comprar simultáneamente el mismo artículo. Dos solicitudes llegarían casi al mismo tiempo al servidor. Ambas consultarían el estado del artículo en la base de datos y verían que está available. Ante esto, ambos manejadores de solicitudes ejecutarían una consulta UPDATE estableciendo el estado a purchased y el buyer_id al ID del usuario solicitante. Ambas actualizaciones tendrían éxito, pero el estado final del registro sería que el usuario que ejecutó la última consulta UPDATE sería considerado el comprador.

Con los años han evolucionado diversas técnicas que permiten a los desarrolladores crear aplicaciones que ofrecen estas garantías. Algunas involucran mecanismos explícitos de bloqueo proporcionados por las bases de datos, mientras que otras se basan en propiedades ACID más generales para lograr exclusión mutua. En esta publicación exploraremos la implementación de dos de estas técnicas usando Ent.

Bloqueo optimista

El bloqueo optimista (también llamado Control de Concurrencia Optimista) es una técnica que permite lograr comportamiento de bloqueo sin adquirir explícitamente un bloqueo sobre ningún registro.

En términos generales, así funciona el bloqueo optimista:

  • Cada registro tiene un número de versión numérico que debe ser monótonamente creciente. Normalmente se usan timestamps Unix de la última actualización.

  • Una transacción lee el registro, anotando su número de versión desde la base de datos.

  • Se ejecuta una sentencia UPDATE para modificar el registro:

    • La sentencia debe incluir un predicado que requiera que el número de versión no haya cambiado. Ejemplo: WHERE id=<id> AND version=<previous version>.
    • La sentencia debe incrementar la versión. Algunas aplicaciones aumentan el valor actual en 1, otras lo establecen al timestamp actual.
  • La base de datos devuelve la cantidad de filas modificadas por la sentencia UPDATE. Si el número es 0, significa que alguien modificó el registro entre nuestra lectura y la actualización. La transacción se considera fallida, se revierte y puede reintentarse.

El bloqueo optimista se usa comúnmente en entornos de "baja contención" (donde la probabilidad de interferencia entre transacciones es reducida) y donde la lógica de bloqueo puede confiarse a la capa de aplicación. Si existen escritores en la base de datos que no garantizan cumplir esta lógica, la técnica pierde utilidad.

Veamos cómo implementar esta técnica usando Ent.

Comenzamos definiendo nuestro ent.Schema para un User. El usuario tiene un campo booleano online para especificar si está actualmente conectado y un campo int64 para el número de versión actual.

// 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.Bool("online"),
field.Int64("version").
DefaultFunc(func() int64 {
return time.Now().UnixNano()
}).
Comment("Unix time of when the latest update occurred")
}
}

A continuación, implementemos una actualización con bloqueo optimista simple para nuestro campo online:

func optimisticUpdate(tx *ent.Tx, prev *ent.User, online bool) error {
// The next version number for the record must monotonically increase
// using the current timestamp is a common technique to achieve this.
nextVer := time.Now().UnixNano()

// We begin the update operation:
n := tx.User.Update().

// We limit our update to only work on the correct record and version:
Where(user.ID(prev.ID), user.Version(prev.Version)).

// We set the next version:
SetVersion(nextVer).

// We set the value we were passed by the user:
SetOnline(online).
SaveX(context.Background())

// SaveX returns the number of affected records. If this value is
// different from 1 the record must have been changed by another
// process.
if n != 1 {
return fmt.Errorf("update failed: user id=%d updated by another process", prev.ID)
}
return nil
}

Ahora escribamos una prueba para verificar que si dos procesos intentan editar el mismo registro, solo uno tendrá éxito:

func TestOCC(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.Background()

// Create the user for the first time.
orig := client.User.Create().SetOnline(true).SaveX(ctx)

// Read another copy of the same user.
userCopy := client.User.GetX(ctx, orig.ID)

// Open a new transaction:
tx, err := client.Tx(ctx)
if err != nil {
log.Fatalf("failed creating transaction: %v", err)
}

// Try to update the record once. This should succeed.
if err := optimisticUpdate(tx, userCopy, false); err != nil {
tx.Rollback()
log.Fatal("unexpected failure:", err)
}

// Try to update the record a second time. This should fail.
err = optimisticUpdate(tx, orig, false)
if err == nil {
log.Fatal("expected second update to fail")
}
fmt.Println(err)
}

Ejecutando nuestra prueba:

=== RUN   TestOCC
update failed: user id=1 updated by another process
--- PASS: Test (0.00s)

¡Genial! Usando bloqueo optimista podemos evitar que dos procesos interfieran entre sí.

Bloqueo Pesimista

Como mencionamos anteriormente, el bloqueo optimista no siempre es apropiado. Para casos de uso donde preferimos delegar la responsabilidad de mantener la integridad del bloqueo a las bases de datos, algunos motores (como MySQL, Postgres y MariaDB, pero no SQLite) ofrecen capacidades de bloqueo pesimista. Estas bases de datos soportan un modificador en las sentencias SELECT llamado SELECT ... FOR UPDATE. La documentación de MySQL explica:

Un SELECT ... FOR UPDATE lee los últimos datos disponibles, estableciendo bloqueos exclusivos en cada fila que lee. Por lo tanto, establece los mismos bloqueos que un UPDATE de SQL establecería en las filas.

Alternativamente, los usuarios pueden usar sentencias SELECT ... FOR SHARE, como explican los documentos, SELECT ... FOR SHARE:

Establece un bloqueo en modo compartido en cualquier fila que se lea. Otras sesiones pueden leer las filas, pero no modificarlas hasta que tu transacción se confirme. Si alguna de estas filas fue cambiada por otra transacción que aún no se ha confirmado, tu consulta esperará hasta que esa transacción termine y luego usará los valores más recientes.

Ent recientemente añadió soporte para sentencias FOR SHARE/FOR UPDATE mediante una bandera de característica llamada sql/lock. Para usarla, modifica tu archivo generate.go para incluir --feature sql/lock:

//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/lock ./schema 

A continuación, implementemos una función que use bloqueo pesimista para asegurar que solo un proceso pueda actualizar el campo online de nuestro objeto User:

func pessimisticUpdate(tx *ent.Tx, id int, online bool) (*ent.User, error) {
ctx := context.Background()

// On our active transaction, we begin a query against the user table
u, err := tx.User.Query().

// We add a predicate limiting the lock to the user we want to update.
Where(user.ID(id)).

// We use the ForUpdate method to tell ent to ask our DB to lock
// the returned records for update.
ForUpdate(
// We specify that the query should not wait for the lock to be
// released and instead fail immediately if the record is locked.
sql.WithLockAction(sql.NoWait),
).
Only(ctx)

// If we failed to acquire the lock we do not proceed to update the record.
if err != nil {
return nil, err
}

// Finally, we set the online field to the desired value.
return u.Update().SetOnline(online).Save(ctx)
}

Ahora escribamos una prueba que verifique que si dos procesos intentan editar el mismo registro, solo uno tendrá éxito:

func TestPessimistic(t *testing.T) {
ctx := context.Background()
client := enttest.Open(t, dialect.MySQL, "root:pass@tcp(localhost:3306)/test?parseTime=True")

// Create the user for the first time.
orig := client.User.Create().SetOnline(true).SaveX(ctx)

// Open a new transaction. This transaction will acquire the lock on our user record.
tx, err := client.Tx(ctx)
if err != nil {
log.Fatalf("failed creating transaction: %v", err)
}
defer tx.Commit()

// Open a second transaction. This transaction is expected to fail at
// acquiring the lock on our user record.
tx2, err := client.Tx(ctx)
if err != nil {
log.Fatalf("failed creating transaction: %v", err)
}
defer tx.Commit()

// The first update is expected to succeed.
if _, err := pessimisticUpdate(tx, orig.ID, true); err != nil {
log.Fatalf("unexpected error: %s", err)
}

// Because we did not run tx.Commit yet, the row is still locked when
// we try to update it a second time. This operation is expected to
// fail.
_, err = pessimisticUpdate(tx2, orig.ID, true)
if err == nil {
log.Fatal("expected second update to fail")
}
fmt.Println(err)
}

Algunos detalles importantes en este ejemplo:

  • Nota que usamos una instancia real de MySQL para ejecutar esta prueba, ya que SQLite no soporta SELECT .. FOR UPDATE.

  • Por simplicidad del ejemplo, usamos la opción sql.NoWait para indicar a la base de datos que devuelva un error si no se puede adquirir el bloqueo. Esto significa que la aplicación llamante necesita reintentar la escritura después de recibir el error. Si no especificamos esta opción, podemos crear flujos donde nuestra aplicación se bloquea hasta que se libera el bloqueo y luego continúa sin reintentos. Esto no siempre es deseable pero abre opciones de diseño interesantes.

  • Siempre debemos confirmar (commit) nuestra transacción. Olvidar hacerlo puede causar problemas graves. Recuerda que mientras se mantiene el bloqueo, nadie puede leer o actualizar este registro.

Ejecutando nuestra prueba:

=== RUN   TestPessimistic
Error 3572: Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.
--- PASS: TestPessimistic (0.08s)

¡Genial! Hemos usado las capacidades de "lecturas con bloqueo" de MySQL y el nuevo soporte de Ent para implementar un mecanismo de bloqueo que proporciona garantías reales de exclusión mutua.

Conclusión

Comenzamos este post presentando el tipo de requisitos empresariales que llevan a los desarrolladores a usar técnicas de bloqueo con bases de datos. Continuamos presentando dos enfoques diferentes para lograr exclusión mutua al actualizar registros y demostramos cómo emplear estas técnicas usando Ent.

¿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 →

En resumen

Hemos añadido una nueva integración a la extensión GraphQL de Ent que genera filtros GraphQL con seguridad de tipos (predicados Where) a partir de un ent/schema, permitiendo mapear consultas GraphQL a consultas Ent de forma transparente.

Por ejemplo, para obtener todos los elementos de tareas COMPLETED, podemos ejecutar lo siguiente:

query QueryAllCompletedTodos {
todos(
where: {
status: COMPLETED,
},
) {
edges {
node {
id
}
}
}
}

Los filtros GraphQL generados siguen la sintaxis de Ent. Esto significa que la siguiente consulta también es válida:

query FilterTodos {
todos(
where: {
or: [
{
hasParent: false,
status: COMPLETED,
},
{
status: IN_PROGRESS,
hasParentWith: {
priorityLT: 1,
statusNEQ: COMPLETED,
},
}
]
},
) {
edges {
node {
id
}
}
}
}

Antecedentes

Muchas bibliotecas que manejan datos en Go utilizan interfaces vacías (interface{}) y reflexión en tiempo de ejecución para mapear datos a campos de estructuras. Además del impacto en rendimiento, la principal desventaja para los equipos es la pérdida de seguridad de tipos.

Cuando las APIs son explícitas y conocidas en tiempo de compilación (o incluso mientras escribimos), la retroalimentación sobre errores es casi inmediata. Muchos defectos se detectan antes y ¡el desarrollo es mucho más divertido!

Ent fue diseñado para ofrecer una excelente experiencia a equipos que trabajan con modelos de datos complejos. Uno de nuestros principios fundamentales es: "API estática y explícita mediante generación de código". Esto significa que por cada entidad definida en ent/schema, se genera código con seguridad de tipos para interactuar eficientemente con los datos. Por ejemplo, en el Ejemplo de sistema de archivos del repositorio ent, encontrarás un esquema llamado File:

// File holds the schema definition for the File entity.
type File struct {
ent.Schema
}
// Fields of the File.
func (File) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Bool("deleted").
Default(false),
field.Int("parent_id").
Optional(),
}
}

Al ejecutar el generador de código de Ent, se crean funciones de predicado. Por ejemplo, esta función para filtrar Files por su campo name:

package file
// .. truncated ..

// Name applies the EQ predicate on the "name" field.
func Name(v string) predicate.File {
return predicate.File(func(s *sql.Selector) {
s.Where(sql.EQ(s.C(FieldName), v))
})
}

GraphQL es un lenguaje de consulta para APIs creado en Facebook. Al igual que Ent, modela datos como grafos y facilita consultas con seguridad de tipos. Hace un año, lanzamos una integración entre Ent y GraphQL. Similar a la Integración gRPC, su objetivo es permitir crear servidores API que mapeen operaciones Ent para consultar y modificar datos.

Generación Automática de Filtros GraphQL

En una encuesta reciente, la integración Ent+GraphQL fue destacada como una de las funcionalidades más valoradas del proyecto. Hasta ahora permitía consultas básicas, pero hoy anunciamos una función que abrirá nuevos casos de uso: "Generación Automática de Filtros GraphQL".

Como vimos, el generador de Ent crea funciones de predicado para filtrar datos explícitamente. Este poder no estaba disponible (automáticamente) en la integración GraphQL hasta ahora. Con esta nueva función, añadiendo una línea de configuración, los desarrolladores pueden agregar "Tipos de Entrada de Filtro" completos a su esquema GraphQL. Además, el sistema traduce estos predicados a consultas Ent en tiempo de ejecución. Veámoslo en acción:

Generación de Tipos de Entrada de Filtro

Para generar filtros de entrada (ej. TodoWhereInput) para cada tipo en tu paquete ent/schema, edita el archivo ent/entc.go así:

// +build ignore

package main

import (
"log"

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

func main() {
ex, err := entgql.NewExtension(
entgql.WithWhereFilters(true),
entgql.WithConfigPath("../gqlgen.yml"),
entgql.WithSchemaPath("<PATH-TO-GRAPHQL-SCHEMA>"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

Si eres nuevo en Ent y GraphQL, sigue el Tutorial de Inicio.

A continuación, ejecuta go generate ./ent/.... Observa que Ent ha generado <T>WhereInput para cada tipo en tu esquema. Ent actualizará automáticamente el esquema GraphQL, así que no necesitas autobindearlos manualmente con gqlgen. Por ejemplo:

ent/where_input.go
// TodoWhereInput represents a where input for filtering Todo queries.
type TodoWhereInput struct {
Not *TodoWhereInput `json:"not,omitempty"`
Or []*TodoWhereInput `json:"or,omitempty"`
And []*TodoWhereInput `json:"and,omitempty"`

// "created_at" field predicates.
CreatedAt *time.Time `json:"createdAt,omitempty"`
CreatedAtNEQ *time.Time `json:"createdAtNEQ,omitempty"`
CreatedAtIn []time.Time `json:"createdAtIn,omitempty"`
CreatedAtNotIn []time.Time `json:"createdAtNotIn,omitempty"`
CreatedAtGT *time.Time `json:"createdAtGT,omitempty"`
CreatedAtGTE *time.Time `json:"createdAtGTE,omitempty"`
CreatedAtLT *time.Time `json:"createdAtLT,omitempty"`
CreatedAtLTE *time.Time `json:"createdAtLTE,omitempty"`

// "status" field predicates.
Status *todo.Status `json:"status,omitempty"`
StatusNEQ *todo.Status `json:"statusNEQ,omitempty"`
StatusIn []todo.Status `json:"statusIn,omitempty"`
StatusNotIn []todo.Status `json:"statusNotIn,omitempty"`

// .. truncated ..
}
todo.graphql
"""
TodoWhereInput is used for filtering Todo objects.
Input was generated by ent.
"""
input TodoWhereInput {
not: TodoWhereInput
and: [TodoWhereInput!]
or: [TodoWhereInput!]

"""created_at field predicates"""
createdAt: Time
createdAtNEQ: Time
createdAtIn: [Time!]
createdAtNotIn: [Time!]
createdAtGT: Time
createdAtGTE: Time
createdAtLT: Time
createdAtLTE: Time

"""status field predicates"""
status: Status
statusNEQ: Status
statusIn: [Status!]
statusNotIn: [Status!]

# .. truncated ..
}

Para completar la integración, necesitamos hacer dos cambios más:

1. Edita el esquema GraphQL para aceptar los nuevos tipos de filtros:

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

2. Utiliza los nuevos tipos de filtros en los resolvers de GraphQL:

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

Especificación de Filtros

Como mencionamos antes, con los nuevos tipos de filtros GraphQL, puedes expresar los mismos filtros de Ent que usas en tu código Go.

Conjunción, disyunción y negación

Los operadores Not, And y Or pueden añadirse usando los campos not, and y or. Por ejemplo:

{
or: [
{
status: COMPLETED,
},
{
not: {
hasParent: true,
status: IN_PROGRESS,
}
}
]
}

Cuando se proporcionan múltiples campos de filtro, Ent añade implícitamente el operador And.

{
status: COMPLETED,
textHasPrefix: "GraphQL",
}

La consulta anterior producirá el siguiente query de Ent:

client.Todo.
Query().
Where(
todo.And(
todo.StatusEQ(todo.StatusCompleted),
todo.TextHasPrefix("GraphQL"),
)
).
All(ctx)

Filtros de relaciones (edges)

Los predicados de relaciones (edges) pueden expresarse con la misma sintaxis de Ent:

{
hasParent: true,
hasChildrenWith: {
status: IN_PROGRESS,
}
}

La consulta anterior producirá el siguiente query de Ent:

client.Todo.
Query().
Where(
todo.HasParent(),
todo.HasChildrenWith(
todo.StatusEQ(todo.StatusInProgress),
),
).
All(ctx)

Ejemplo de Implementación

Existe un ejemplo funcional en github.com/a8m/ent-graphql-example.

Conclusión

Como mencionamos antes, Ent tiene como principio fundamental crear "APIs estáticamente tipadas y explícitas mediante generación de código". Con la generación automática de filtros GraphQL, reforzamos esta idea para brindar a los desarrolladores la misma experiencia de desarrollo explícita y tipada en la capa RPC.

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

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

· 3 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, anunciamos el soporte experimental para generar servicios gRPC a partir de definiciones de esquemas Ent. La implementación aún no estaba completa, pero queríamos ponerla a disposición de la comunidad para experimentar y obtener retroalimentación.

Hoy, tras recibir numerosos comentarios de la comunidad, nos complace anunciar que la integración entre Ent y gRPC está "Lista para uso". Esto significa que todas las funcionalidades básicas están completas y anticipamos que la mayoría de aplicaciones Ent pueden aprovechar esta integración.

¿Qué hemos añadido desde nuestro anuncio inicial?

  • Soporte para "Campos opcionales" - Un problema común con Protobufs es cómo se representan los valores nulos: un campo primitivo con valor cero no se codifica en la representación binaria. Esto significa que las aplicaciones no pueden distinguir entre cero y no establecido para campos primitivos. Para solucionarlo, el proyecto Protobuf incluye unos "Tipos bien conocidos" llamados "tipos envoltorio" que encapsulan el valor primitivo en una estructura. Esto no estaba soportado antes, pero ahora cuando entproto genera una definición de mensaje Protobuf, utiliza estos tipos envoltorio para representar campos "opcionales" de Ent:

    // Code generated by entproto. DO NOT EDIT.
    syntax = "proto3";

    package entpb;

    import "google/protobuf/wrappers.proto";

    message User {
    int32 id = 1;

    string name = 2;

    string email_address = 3;

    google.protobuf.StringValue alias = 4;
    }
  • Soporte para múltiples relaciones - Cuando lanzamos la versión inicial de
    protoc-gen-entgrpc, solo soportábamos generar implementaciones de servicios gRPC para relaciones "Únicas" (que referencian como máximo una entidad). Desde una versión reciente, el plugin soporta la generación de métodos gRPC para leer y escribir entidades con relaciones O2M (Uno a Muchos) y M2M (Muchos a Muchos).

  • Respuestas parciales - Por defecto, la información de relaciones no se devuelve en el método Get del servicio. Esto es deliberado porque la cantidad de entidades relacionadas con una entidad no tiene límite.

    Para permitir que el llamante especifique si desea o no obtener la información de relaciones, el servicio generado sigue Google AIP-157 (Respuestas Parciales). En resumen, el mensaje Get<T>Request incluye una enumeración llamada View, que permite al llamante controlar si esta información debe recuperarse de la base de datos.

    message GetUserRequest {
    int32 id = 1;

    View view = 2;

    enum View {
    VIEW_UNSPECIFIED = 0;

    BASIC = 1;

    WITH_EDGE_IDS = 2;
    }
    }

Empezando

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