Ent ORM en Go: schema como código, edges, queries tipadas y hooks

Ent es el ORM de Meta para Go con un enfoque radicalmente diferente: el schema se escribe en código Go puro, no en YAML ni en anotaciones. Ent genera a partir de ese schema un cliente completamente tipado donde los errores de query se detectan en compilación, no en runtime.

Instalación e inicialización

go get entgo.io/ent/cmd/ent
go run entgo.io/ent/cmd/ent new Usuario Articulo

Esto crea ent/schema/usuario.go y ent/schema/articulo.go. Tras definir los schemas, generas el cliente con:

go generate ./ent

Definir el schema como código Go

// ent/schema/usuario.go
package schema

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

type Usuario struct {
    ent.Schema
}

func (Usuario) Fields() []ent.Field {
    return []ent.Field{
        field.String("nombre").NotEmpty(),
        field.String("email").Unique(),
        field.Int("edad").Positive().Optional(),
        field.Bool("activo").Default(true),
        field.Time("creado_en").Immutable(),
    }
}

func (Usuario) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("articulos", Articulo.Type),
    }
}

// ent/schema/articulo.go
func (Articulo) Fields() []ent.Field {
    return []ent.Field{
        field.String("titulo").NotEmpty(),
        field.Text("cuerpo"),
        field.Bool("publicado").Default(false),
    }
}

func (Articulo) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("autor", Usuario.Type).Ref("articulos").Unique().Required(),
    }
}

Crear el cliente y ejecutar migraciones

package main

import (
    "context"
    "log"

    _ "github.com/go-sql-driver/mysql"
    "tumodulo/ent"
)

func main() {
    client, err := ent.Open("mysql", "progra7net:pass@tcp(localhost:3306)/progra7net?parseTime=True")
    if err != nil {
        log.Fatalf("error abriendo cliente: %v", err)
    }
    defer client.Close()

    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("error en migración: %v", err)
    }
}

CRUD con el cliente tipado

// Crear un usuario
u, err := client.Usuario.Create().
    SetNombre("Ana García").
    SetEmail("[email protected]").
    SetEdad(30).
    Save(ctx)
if err != nil {
    return fmt.Errorf("error creando usuario: %w", err)
}
fmt.Println("ID del nuevo usuario:", u.ID)

// Consultar con predicados
usuarios, err := client.Usuario.Query().
    Where(
        usuario.EdadGT(18),
        usuario.ActivoEQ(true),
        usuario.NombreContains("García"),
    ).
    Order(ent.Asc(usuario.FieldNombre)).
    Limit(10).
    All(ctx)

// Cargar relaciones (edges)
arts, err := client.Usuario.Query().
    Where(usuario.ID(u.ID)).
    QueryArticulos().
    Where(articulo.PublicadoEQ(true)).
    All(ctx)

// Actualizar
u, err = u.Update().
    SetEdad(31).
    AddArticulos(art).
    Save(ctx)

// Eliminar
err = client.Usuario.DeleteOne(u).Exec(ctx)

Hooks: controlar mutaciones

Los hooks interceptan operaciones de escritura antes o después de que ocurran. Son ideales para auditoría, validación o efectos secundarios:

func HookAuditoriaUsuario() ent.Hook {
    return func(next ent.Mutator) ent.Mutator {
        return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
            // antes de la operación
            op := m.Op()
            if op == ent.OpCreate {
                fmt.Println("creando nuevo usuario")
            }

            v, err := next.Mutate(ctx, m)

            // después de la operación
            if err == nil && op == ent.OpDeleteOne {
                fmt.Println("usuario eliminado")
            }
            return v, err
        })
    }
}

// Registrar el hook
client.Usuario.Use(HookAuditoriaUsuario())

// Hook solo para Create
client.Usuario.Use(hook.On(HookAuditoriaUsuario(), ent.OpCreate))

Transacciones

tx, err := client.Tx(ctx)
if err != nil {
    return err
}

u, err := tx.Usuario.Create().
    SetNombre("Luis").
    SetEmail("[email protected]").
    Save(ctx)
if err != nil {
    return tx.Rollback()
}

_, err = tx.Articulo.Create().
    SetTitulo("Primer artículo").
    SetCuerpo("Contenido...").
    SetAutor(u).
    Save(ctx)
if err != nil {
    return tx.Rollback()
}

return tx.Commit()

Ent genera código distinto para cada base de datos (SQLite, MySQL, PostgreSQL, MariaDB) en función del driver que uses. El cliente tipado es lo que diferencia a Ent del resto de ORMs Go: no hay interface{} ni reflexión en las queries del día a día.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP