Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto.
¿Encontraste un error? Reportar problema →
Migrar a un nuevo ORM no es un proceso sencillo, y el coste de transición puede ser prohibitivo para muchas organizaciones. Por mucho que a los desarrolladores nos cautiven las "novedades relucientes", la realidad es que rara vez tenemos la oportunidad de trabajar en un proyecto realmente "desde cero". Durante la mayor parte de nuestras carreras, operamos en contextos donde múltiples limitaciones técnicas y de negocio (también conocidas como sistemas heredados) dictan y restringen nuestras opciones para avanzar. Las tecnologías emergentes que quieran triunfar deben ofrecer capacidades de interoperabilidad y vías de integración que ayuden a las organizaciones a transitar sin problemas hacia nuevas formas de resolver problemas existentes.
Para ayudar a reducir el coste de migrar a Ent (o simplemente experimentar con él), hemos lanzado la "Schema Import Initiative" para respaldar numerosos casos de uso de generación de esquemas de Ent desde recursos externos. La pieza central de este esfuerzo es el paquete schemast (código fuente, documentación), que permite a los desarrolladores crear fácilmente programas que generen y manipulen esquemas de Ent. Con este paquete, los desarrolladores pueden programar usando una API de alto nivel, liberándoles de preocuparse por el análisis de código y las manipulaciones de AST.
El primer proyecto en usar esta nueva API es protoc-gen-ent, un plugin de protoc para generar esquemas de Ent desde archivos .proto (documentación). Las organizaciones que ya tienen esquemas definidos en Protobuf pueden usar esta herramienta para generar automáticamente código de Ent. Por ejemplo, tomando una definición de mensaje sencilla:
¿Tienes esquemas definidos en otros lugares que te gustaría importar automáticamente a Ent? Con el paquete schemast, es más fácil que nunca crear la herramienta que necesitas para hacerlo. ¿No sabes por dónde empezar? ¿Quieres colaborar con la comunidad para planificar y desarrollar tu idea? ¡Contáctanos a través de nuestro servidor de Discord, canal de Slack o inicia un debate en GitHub!
Definir esquemas de entidades en un formato centralizado y neutral al lenguaje
ofrece muchas ventajas a medida que crece la escala de las organizaciones de desarrollo de software. Para lograrlo, muchas organizaciones utilizan Protocol Buffers como su lenguaje de definición de interfaces (IDL). Además, gRPC,
un framework RPC basado en Protobuf inspirado en el sistema interno Stubby de Google, está ganando popularidad debido a su eficiencia y capacidades de generación de código.
Al ser un IDL, gRPC no establece pautas específicas para implementar la capa de acceso a datos, por lo que las implementaciones varían enormemente. Ent es un candidato natural para construir la capa de acceso a datos en cualquier aplicación Go, por lo que existe un gran potencial al integrar ambas tecnologías.
Hoy anunciamos una versión experimental de entproto, un paquete Go y una herramienta de línea de comandos para añadir soporte de Protobuf y gRPC a usuarios de Ent. Con entproto, los desarrolladores pueden configurar un servidor gRPC CRUD completamente funcional en pocos minutos. En este artículo, mostraremos exactamente cómo hacerlo.
Ahora, añadamos el paquete entproto a nuestro proyecto:
go get -u entgo.io/contrib/entproto
A continuación, definiremos el esquema para la entidad User. Abre ent/schema/user.go y edita:
package schema import( "entgo.io/ent" "entgo.io/ent/schema" ) // 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"). Unique(), field.String("email_address"). Unique(), } }
En este paso, añadimos dos campos únicos a nuestra entidad User: name y email_address. El ent.Schema es solo la definición del esquema; para generar código utilizable a partir de él necesitamos ejecutar la herramienta de generación de código de Ent. Ejecuta:
go generate ./...
Observa que ahora se han creado varios archivos nuevos a partir de nuestra definición de esquema:
├── ent │ ├── client.go │ ├── config.go // .... many more │ ├── user │ ├── user.go │ ├── user_create.go │ ├── user_delete.go │ ├── user_query.go │ └── user_update.go ├── go.mod └── go.sum
En este punto, podemos abrir una conexión a una base de datos, ejecutar una migración para crear la tabla users, y comenzar a leer y escribir datos en ella. Esto se cubre en el Tutorial de Configuración, así que vayamos al grano y aprendamos sobre la generación de definiciones Protobuf y servidores gRPC a partir de nuestro esquema.
Dado que los esquemas de Ent y Protobuf no son idénticos, debemos añadir algunas anotaciones en nuestro esquema para ayudar a entproto a determinar exactamente cómo generar las definiciones Protobuf (llamadas "Mensajes" en la jerga de protobuf).
Lo primero que necesitamos hacer es añadir una anotación entproto.Message(). Este es nuestro opt-in para la generación del esquema Protobuf; no necesariamente queremos generar mensajes proto o definiciones de servicios gRPC para todas nuestras entidades de esquema, y esta anotación nos da ese control. Para añadirla, agrega en ent/schema/user.go:
A continuación, debemos anotar cada campo y asignarle un número de campo. Recuerda que al definir un tipo de mensaje protobuf, cada campo debe tener un número único. Para ello, añadimos una anotación entproto.Field en cada campo. Actualiza los Fields en ent/schema/user.go:
// Fields of the User. func(User)Fields()[]ent.Field { return[]ent.Field{ field.String("name"). Unique(). Annotations( entproto.Field(2), ), field.String("email_address"). Unique(). Annotations( entproto.Field(3), ), } }
Observa que no comenzamos nuestros números de campo desde 1, esto se debe a que ent crea implícitamente el campo ID para la entidad, y a ese campo se le asigna automáticamente el número 1. Ahora podemos generar nuestras definiciones de tipos de mensajes protobuf. Para hacerlo, añadiremos en ent/generate.go una directiva go:generate que invoque la herramienta de línea de comandos entproto. Ahora debería verse así:
package ent //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema //go:generate go run -mod=mod entgo.io/contrib/entproto/cmd/entproto -path ./schema
Vamos a regenerar nuestro código:
go generate ./...
Observa que se ha creado un nuevo directorio que contendrá todo el código generado relacionado con protobuf: ent/proto. Ahora contiene:
Se ha creado un nuevo archivo generate.go con una invocación a protoc, el generador de código protobuf, que le indica cómo generar código Go desde nuestro archivo .proto. Para que este comando funcione, primero debemos instalar protoc y 3 plugins de protobuf: protoc-gen-go (que genera estructuras Protobuf de Go), protoc-gen-go-grpc (que genera interfaces de servicio gRPC de Go y clientes), y protoc-gen-entgrpc (que genera una implementación de la interfaz de servicio). Si no los tienes instalados, sigue estas instrucciones:
Ejecuta go get -u entgo.io/contrib/entproto/cmd/protoc-gen-entgrpc para instalar protoc-gen-entgrpc
Tras instalar estas dependencias, podemos volver a ejecutar la generación de código:
go generate ./...
Observa que se ha creado un nuevo archivo llamado ent/proto/entpb/entpb.pb.go que contiene las estructuras Go generadas para nuestras entidades.
Observa que se ha creado un nuevo archivo llamado pb_test.go que contiene las estructuras Go generadas para nuestras entidades.
package main import( "testing" "ent-grpc-example/ent/proto/entpb" ) funcTestUserProto(t *testing.T){ user := entpb.User{ Name:"rotemtam", EmailAddress:"rotemtam@example.com", } if user.GetName()!="rotemtam"{ t.Fatal("expected user name to be rotemtam") } if user.GetEmailAddress()!="rotemtam@example.com"{ t.Fatal("expected email address to be rotemtam@example.com") } }
Para ejecutarlo:
go get -u./... # install deps of the generated package go test ./...
¡Bravo! La prueba ha pasado. Hemos generado con éxito estructuras Protobuf de Go funcionales a partir de nuestro esquema Ent. A continuación, veamos cómo generar automáticamente un servidor gRPC CRUD funcional a partir de nuestro esquema.
¡Genial! La prueba pasa. Hemos generado con éxito estructuras Go Protobuf funcionales a partir de nuestro esquema Ent. A continuación, veamos cómo generar automáticamente un servidor gRPC CRUD funcional a partir de nuestro esquema.
Tener estructuras Protobuf generadas desde nuestro ent.Schema puede ser útil, pero lo que realmente nos interesa es obtener un servidor real que pueda crear, leer, actualizar y eliminar entidades de una base de datos real. ¡Para hacerlo, solo necesitamos actualizar una línea de código! Cuando anotamos un esquema con entproto.Service, le indicamos al code-gen de entproto que queremos generar una definición de servicio gRPC, que protoc-gen-entgrpc leerá para generar una implementación del servicio. Edita ent/schema/user.go y modifica las Annotations del esquema:
Además, se crearon dos archivos nuevos. El primero, ent_grpc.pb.go, contiene el stub del cliente gRPC y la definición de la interfaz. Si abres el archivo, encontrarás en él (entre muchas otras cosas):
// UserServiceClient is the client API for UserService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type UserServiceClient interface{ Create(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption)(*User,error) Get(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption)(*User,error) Update(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption)(*User,error) Delete(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption)(*emptypb.Empty,error) }
El segundo archivo, entpub_user_service.go, contiene una implementación generada para esta interfaz. Por ejemplo, una implementación para el método Get:
Crea un nuevo archivo cmd/server/main.go y escribe:
package main import( "context" "log" "net" _"github.com/mattn/go-sqlite3" "ent-grpc-example/ent" "ent-grpc-example/ent/proto/entpb" "google.golang.org/grpc" ) funcmain(){ // Initialize an ent client. client, 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 client.Close() // Run the migration tool (creating tables, etc). if err := client.Schema.Create(context.Background()); err !=nil{ log.Fatalf("failed creating schema resources: %v", err) } // Initialize the generated User service. svc := entpb.NewUserService(client) // Create a new gRPC server (you can wire multiple services to a single server). server := grpc.NewServer() // Register the User service with the server. entpb.RegisterUserServiceServer(server, svc) // Open port 5000 for listening to traffic. lis, err := net.Listen("tcp",":5000") if err !=nil{ log.Fatalf("failed listening: %s", err) } // Listen for traffic indefinitely. if err := server.Serve(lis); err !=nil{ log.Fatalf("server ended: %s", err) } }
Fíjate que hemos añadido una importación de github.com/mattn/go-sqlite3, por lo que debemos añadirla a nuestro módulo:
go get -u github.com/mattn/go-sqlite3
A continuación, vamos a ejecutar el servidor mientras escribimos un cliente que se comunicará con él:
Vamos a crear un cliente simple que realizará algunas llamadas a nuestro servidor. Crea un nuevo archivo llamado cmd/client/main.go y escribe:
package main import( "context" "fmt" "log" "math/rand" "time" "ent-grpc-example/ent/proto/entpb" "google.golang.org/grpc" "google.golang.org/grpc/status" ) funcmain(){ rand.Seed(time.Now().UnixNano()) // 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) // Ask the server to create a random User. ctx := context.Background() user :=randomUser() created, err := client.Create(ctx,&entpb.CreateUserRequest{ User: user, }) if err !=nil{ se,_:= status.FromError(err) log.Fatalf("failed creating user: status=%s message=%s", se.Code(), se.Message()) } log.Printf("user created with id: %d", created.Id) // On a separate RPC invocation, retrieve the user we saved previously. get, err := client.Get(ctx,&entpb.GetUserRequest{ Id: created.Id, }) if err !=nil{ se,_:= status.FromError(err) log.Fatalf("failed retrieving user: status=%s message=%s", se.Code(), se.Message()) } log.Printf("retrieved user with id=%d: %v", get.Id, get) } funcrandomUser()*entpb.User { return&entpb.User{ Name: fmt.Sprintf("user_%d", rand.Int()), EmailAddress: fmt.Sprintf("user_%d@example.com", rand.Int()), } }
Nuestro cliente crea una conexión al puerto 5000, donde nuestro servidor está escuchando, luego emite una solicitud Create para crear un nuevo usuario, y luego emite una segunda solicitud Get para recuperarlo de la base de datos. Ejecutemos nuestro código de cliente:
go run ./cmd/client
Ejecutemos nuestro código de cliente:
2021/03/18 10:42:58 user created with id: 1 2021/03/18 10:42:58 retrieved user with id=1: id:1 name:"user_730811260095307266" email_address:"user_7338662242574055998@example.com"
¡Increíble! Con unas pocas anotaciones en nuestro esquema, usamos los superpoderes de la generación de código para crear un servidor gRPC funcional en muy poco tiempo.
¡Increíble! Con solo unas anotaciones en nuestro esquema, hemos aprovechado los superpoderes de la generación de código para crear un servidor gRPC funcional en un instante.
entproto aún se encuentra en etapa experimental y carece de cierta funcionalidad básica. Por ejemplo, muchas aplicaciones probablemente querrán un método List o Find en su servicio, pero estos aún no están soportados. Además, hay otros problemas que planeamos abordar en un futuro próximo:
Actualmente solo se admiten aristas "únicas" (O2O, O2M).
Actualmente solo se admiten relaciones "únicas" (O2O, O2M).
Los métodos de "mutación" generados (Create/Update) actualmente establecen todos los campos, sin considerar valores cero/nulos ni la nulabilidad de los campos.
Creemos que ent + gRPC puede ser una excelente manera de construir aplicaciones de servidor en Go. Por ejemplo, para establecer controles de acceso granulares a las entidades gestionadas por nuestra aplicación, los desarrolladores ya pueden usar Políticas de Privacidad que funcionan sin configuración adicional con la integración gRPC. Para ejecutar código Go arbitrario en los diferentes eventos del ciclo de vida de las entidades, los desarrolladores pueden utilizar Hooks personalizados.
¿Quieres construir servidores gRPC con ent? Si necesitas ayuda para configurarlo o quieres que la integración admita tu caso de uso, contáctanos a través de nuestra Página de Discusiones en GitHub, en el canal #ent de Gophers Slack o en nuestro servidor de Discord.
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto.
¿Encontraste un error? Reportar problema →
Durante los últimos meses, ha habido mucha discusión en los issues del proyecto Ent sobre añadir soporte para recuperar el campo de clave externa al obtener entidades con relaciones Uno-a-Uno o Uno-a-Muchos. Nos complace anunciar que a partir de v0.7.0 Ent soporta esta funcionalidad.
Antes de fusionar esta rama, un usuario que quería recuperar el campo de clave externa para una entidad necesitaba usar eager-loading. Supongamos que nuestro esquema era así:
// ent/schema/user.go: // 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"). Unique(). NotEmpty(), } } // Edges of the User. func(User)Edges()[]ent.Edge { return[]ent.Edge{ edge.From("pets", Pet.Type). Ref("owner"), } } // ent/schema/pet.go // 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"). NotEmpty(), } } // Edges of the Pet. func(Pet)Edges()[]ent.Edge { return[]ent.Edge{ edge.To("owner", User.Type). Unique(). Required(), } }
El esquema describe dos entidades relacionadas: User y Pet, con una relación Uno-a-Muchos entre ellas: un usuario puede tener varias mascotas y una mascota puede tener un único dueño.
Al recuperar mascotas del almacenamiento de datos, es común que los desarrolladores quieran acceder al campo de clave externa en la mascota. Sin embargo, dado que este campo se creaba implícitamente desde el borde owner, no era accesible automáticamente al recuperar una entidad. Para obtenerlo, un desarrollador necesitaba hacer algo como:
funcTest(t *testing.T){ ctx := context.Background() c := enttest.Open(t, dialect.SQLite,"file:ent?mode=memory&cache=shared&_fk=1") defer c.Close() // Create the User u := c.User.Create(). SetUserName("rotem"). SaveX(ctx) // Create the Pet p := c.Pet. Create(). SetOwner(u).// Associate with the user SetName("donut"). SaveX(ctx) petWithOwnerId := c.Pet.Query(). Where(pet.ID(p.ID)). WithOwner(func(query *ent.UserQuery){ query.Select(user.FieldID) }). OnlyX(ctx) fmt.Println(petWithOwnerId.Edges.Owner.ID) // Output: 1 }
Además de ser muy verboso, recuperar la mascota con el dueño de esta manera era ineficiente en términos de consultas a la base de datos. Si ejecutamos la consulta con .Debug() podemos ver las consultas SQL que genera Ent para satisfacer esta llamada:
El soporte para campos de borde simplifica enormemente y mejora la eficiencia de este flujo. Con esta funcionalidad, los desarrolladores pueden definir el campo de clave externa como parte de Fields() del esquema, y usando el modificador .Field(..) en la definición del borde, indicar a Ent que exponga y mapee la columna externa a este campo. Así que en nuestro esquema de ejemplo, lo modificaríamos así:
// user.go stays the same // pet.go // Fields of the Pet. func(Pet)Fields()[]ent.Field { return[]ent.Field{ field.String("name"). NotEmpty(), field.Int("owner_id"),// <-- explicitly add the field we want to contain the FK } } // Edges of the Pet. func(Pet)Edges()[]ent.Edge { return[]ent.Edge{ edge.To("owner", User.Type). Field("owner_id").// <-- tell ent which field holds the reference to the owner Unique(). Required(), } }
Para actualizar nuestro código cliente necesitamos volver a ejecutar la generación de código:
go generate ./...
Ahora podemos modificar nuestra consulta para que sea mucho más simple:
funcTest(t *testing.T){ ctx := context.Background() c := enttest.Open(t, dialect.SQLite,"file:ent?mode=memory&cache=shared&_fk=1") defer c.Close() u := c.User.Create(). SetUserName("rotem"). SaveX(ctx) p := c.Pet.Create(). SetOwner(u). SetName("donut"). SaveX(ctx) petWithOwnerId := c.Pet.GetX(ctx, p.ID)// <-- Simply retrieve the Pet fmt.Println(petWithOwnerId.OwnerID) // Output: 1 }
Al ejecutar con el modificador .Debug() podemos ver que las consultas SQL tienen más sentido ahora:
Si ya usas Ent con un esquema existente, probablemente ya tengas relaciones O2M cuyas columnas de clave externa ya existen en tu base de datos. Dependiendo de cómo configuraste tu esquema, es posible que estén almacenadas en una columna con nombre diferente al campo que ahora añades. Por ejemplo, quieres crear un campo owner_id, pero Ent creó automáticamente la columna de clave externa como pet_owner.
Para verificar qué nombre de columna está usando Ent para este campo, puedes mirar en el archivo ./ent/migrate/schema.go:
PetsColumns =[]*schema.Column{ {Name:"id", Type: field.TypeInt, Increment:true}, {Name:"name", Type: field.TypeString}, {Name:"pet_owner", Type: field.TypeInt, Nullable:true},// <-- this is our FK }
Para permitir una migración fluida, debes indicar explícitamente a Ent que siga usando el nombre de columna existente. Puedes hacer esto usando el modificador StorageKey (ya sea en el campo o en el borde). Por ejemplo:
// In schema/pet.go: // Fields of the Pet. func(Pet)Fields()[]ent.Field { return[]ent.Field{ field.String("name"). NotEmpty(), field.Int("owner_id"). StorageKey("pet_owner"),// <-- explicitly set the column name } }
En un futuro próximo planeamos implementar Schema Versioning, que almacenará el historial de cambios de esquema junto al código. Tener esta información permitirá a Ent soportar estas migraciones de forma automática y predecible.
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto.
¿Encontraste un error? Reportar problema →
El estado de Go en Facebook Connectivity Tel Aviv
Hace 20 meses, me uní al equipo de Facebook Connectivity (FBC) en Tel Aviv tras aproximadamente 5 años
programando en Go e implementándolo en varias empresas.
Me incorporé a un equipo que trabajaba en un nuevo proyecto y necesitábamos elegir un lenguaje
para esta misión. Comparamos varias opciones y decidimos usar Go.
Desde entonces, Go se ha expandido a otros proyectos de FBC con gran éxito,
contando ya con unos 15 ingenieros especializados en Go solo en Tel Aviv. Los nuevos servicios ahora se desarrollan en Go.
Durante mis 5 años anteriores a Facebook, la mayor parte de mi trabajo fue en herramientas de infraestructura y microservicios sin
mucho modelado de datos. Los servicios que requerían poca interacción con bases de datos SQL
usaban soluciones open-source existentes, pero para modelos de datos complejos
se recurría a otros lenguajes con ORMs robustos, como Python con SQLAlchemy.
En Facebook nos gusta conceptualizar nuestros modelos de datos como grafos. Hemos tenido buenas experiencias
con este enfoque internamente.
La falta de un ORM basado en grafos adecuado para Go nos llevó a desarrollar uno con estos principios:
Esquema como código: definir tipos, relaciones y restricciones debe hacerse en código Go (no mediante struct tags)
y validarse con una herramienta CLI. Contamos con experiencia previa en herramientas similares internamente.
API estáticamente tipada y explícita mediante generación de código: las APIs repletas de interface{} afectan
la productividad de los desarrolladores, especialmente a los nuevos en el proyecto.
Consultas, agregaciones y recorridos de grafos deben ser simples: los desarrolladores no quieren lidiar
con consultas SQL crudas ni terminología SQL.
Los predicados deben estar estáticamente tipados. Evitar cadenas de texto por todas partes.
Soporte completo para context.Context: esto proporciona visibilidad en nuestros sistemas de trazas y logs,
y es crucial para características como la cancelación.
Independiente del almacenamiento: mantuvimos la capa de almacenamiento dinámica usando plantillas de generación de código,
ya que el desarrollo comenzó con Gremlin (AWS Neptune) y luego migró a MySQL.
ent es un framework de entidades (ORM) para Go construido con los principios anteriores.
ent permite definir fácilmente cualquier modelo de datos o estructura de grafos en código Go; la
configuración del esquema es verificada por entc (el generador de código de ent) que produce una API idiomática
y estáticamente tipada que mantiene productivos y satisfechos a los desarrolladores Go.
Es compatible con MySQL, MariaDB, PostgreSQL, SQLite y bases de datos de grafos basadas en Gremlin.