Saltar al contenido principal

Anunciamos el soporte para campos de borde en v0.7.0

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

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 del soporte para campos de borde

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:

func Test(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:

SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`pet_owner` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2 
SELECT DISTINCT `users`.`id` FROM `users` WHERE `users`.`id` IN (?)

En este ejemplo, Ent primero recupera la mascota con ID 1, luego vuelve a obtener redundante el campo id de la tabla users para usuarios con ID 1.

Con soporte para campos de borde

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:

func Test(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:

SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`owner_id` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2

¡Hurra 🎉!

Migrando esquemas existentes a campos de borde

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.

Conclusión

El soporte para campos de borde está disponible inmediatamente y puede instalarse con go get -u entgo.io/ent@v0.7.0.

Muchas gracias 🙏 a todas las personas que se tomaron el tiempo de dar su opinión y ayudaron a diseñar adecuadamente esta característica: Alex Snast, Ruben de Vries, Marwan Sulaiman, Andy Day, Sebastian Fekete y Joe Harvey.

Para más noticias y actualizaciones de Ent: