Saltar al contenido principal

Introducción rápida

[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

ent es un framework de entidades simple pero potente para Go que facilita la creación y mantenimiento de aplicaciones con grandes modelos de datos, siguiendo estos principios:

  • Modela esquemas de base de datos como estructuras gráficas fácilmente.

  • Define esquemas mediante código Go programático.

  • Tipado estático basado en generación de código.

  • Consultas de base de datos y recorridos gráficos sencillos de escribir.

  • Fácil de extender y personalizar usando plantillas de Go.

gopher-esquema-como-codigo

Configurar un entorno Go

Si tu directorio de proyecto está fuera de GOPATH o no estás familiarizado con GOPATH, configura un proyecto con módulos Go así:

go mod init entdemo

Crea tu primer esquema

Ve al directorio raíz de tu proyecto y ejecuta:

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

Este comando generará el esquema para User en el directorio entdemo/ent/schema/:

entdemo/ent/schema/user.go

package schema

import "entgo.io/ent"

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

// Fields of the User.
func (User) Fields() []ent.Field {
return nil
}

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

Añade 2 campos al esquema User:

entdemo/ent/schema/user.go
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}

Ejecuta go generate desde el directorio raíz del proyecto así:

go generate ./ent

Esto genera los siguientes archivos:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... truncated
├── schema
│ └── user.go
├── tx.go
├── user
│ ├── user.go
│ └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

Crea tu primera entidad

Para empezar, crea un nuevo Client para ejecutar migraciones de esquemas e interactuar con tus entidades:

entdemo/start.go
package main

import (
"context"
"log"

"entdemo/ent"

_ "github.com/mattn/go-sqlite3"
)

func main() {
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 auto migration tool.
if err := client.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

Tras ejecutar la migración de esquema, estamos listos para crear nuestro usuario. Para este ejemplo, llamaremos a esta función CreateUser:

entdemo/start.go
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Create().
SetAge(30).
SetName("a8m").
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", u)
return u, nil
}

Consulta tus entidades

ent genera un paquete para cada esquema de entidad que contiene sus predicados, valores por defecto, validadores e información adicional sobre elementos de almacenamiento (nombres de columna, claves primarias, etc).

entdemo/start.go
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
u, err := client.User.
Query().
Where(user.Name("a8m")).
// `Only` fails if no user found,
// or more than 1 user returned.
Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed querying user: %w", err)
}
log.Println("user returned: ", u)
return u, nil
}

Añade tu primera arista (relación)

En esta parte del tutorial, declararemos una arista (relación) hacia otra entidad en el esquema.
Creemos 2 entidades adicionales llamadas Car y Group con algunos campos. Usaremos la CLI de ent para generar los esquemas iniciales:

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

Y luego añadimos manualmente el resto de campos:

entdemo/ent/schema/car.go
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
entdemo/ent/schema/group.go
// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
// Regexp validation for group name.
Match(regexp.MustCompile("[a-zA-Z_]+$")),
}
}

Definamos nuestra primera relación. Una arista desde User hacia Car que indica que un usuario puede tener uno o más coches, pero un coche tiene solo un dueño (relación uno a muchos).

er-usuario-coches

Añadamos la arista "cars" al esquema User y ejecutemos go generate ./ent:

entdemo/ent/schema/user.go
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}

Continuamos nuestro ejemplo creando 2 coches y asignándolos a un usuario.

entdemo/start.go
func CreateCars(ctx context.Context, client *ent.Client) (*ent.User, error) {
// Create a new car with model "Tesla".
tesla, err := client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating car: %w", err)
}
log.Println("car was created: ", tesla)

// Create a new car with model "Ford".
ford, err := client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating car: %w", err)
}
log.Println("car was created: ", ford)

// Create a new user, and add it the 2 cars.
a8m, err := client.User.
Create().
SetAge(30).
SetName("a8m").
AddCars(tesla, ford).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed creating user: %w", err)
}
log.Println("user was created: ", a8m)
return a8m, nil
}

Pero, ¿qué hay de consultar el borde (relación) cars? Así es como lo hacemos:

entdemo/start.go
func QueryCars(ctx context.Context, a8m *ent.User) error {
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println("returned cars:", cars)

// What about filtering specific cars.
ford, err := a8m.QueryCars().
Where(car.Model("Ford")).
Only(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
log.Println(ford)
return nil
}

Añade tu primera arista inversa (retroreferencia)

Imagina que tenemos un objeto Car y queremos obtener su dueño: el usuario al que pertenece. Para esto tenemos otro tipo de arista llamada "arista inversa" que se define con la función edge.From.

er-coches-propietario

La nueva arista en el diagrama es translúcida para enfatizar que no creamos otra arista en la base de datos. Es solo una retroreferencia a la arista real (relación).

Añadamos una arista inversa llamada owner al esquema Car, referenciémosla a la arista cars en el esquema User, y ejecutemos go generate ./ent.

entdemo/ent/schema/car.go
// Edges of the Car.
func (Car) Edges() []ent.Edge {
return []ent.Edge{
// Create an inverse-edge called "owner" of type `User`
// and reference it to the "cars" edge (in User schema)
// explicitly using the `Ref` method.
edge.From("owner", User.Type).
Ref("cars").
// setting the edge to unique, ensure
// that a car can have only one owner.
Unique(),
}
}

Continuaremos con el ejemplo de usuarios/coches anterior consultando la relación inversa.

entdemo/start.go
func QueryCarUsers(ctx context.Context, a8m *ent.User) error {
cars, err := a8m.QueryCars().All(ctx)
if err != nil {
return fmt.Errorf("failed querying user cars: %w", err)
}
// Query the inverse edge.
for _, c := range cars {
owner, err := c.QueryOwner().Only(ctx)
if err != nil {
return fmt.Errorf("failed querying car %q owner: %w", c.Model, err)
}
log.Printf("car %q owner: %q\n", c.Model, owner.Name)
}
return nil
}

Visualizar el esquema

Si has llegado a este punto, has ejecutado con éxito la migración del esquema y creado varias entidades en la base de datos. Para ver el esquema SQL generado por Ent, instala Atlas y ejecuta el siguiente comando:

Instalar Atlas

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

Para instalar la última versión de Atlas, simplemente ejecuta uno de los siguientes comandos en tu terminal, o visita el sitio web de Atlas:

curl -sSf https://atlasgo.sh | sh

Inspect The Ent Schema

atlas schema inspect \
-u "ent://ent/schema" \
--dev-url "sqlite://file?mode=memory&_fk=1" \
-w

ERD and SQL Schema

erd

Crea tu segunda arista

Crear tu segunda relación

er-group-users

Como puedes ver, cada entidad de grupo puede tener muchos usuarios, y un usuario puede estar conectado a muchos grupos; una relación simple de "muchos a muchos". En la ilustración anterior, el esquema Group es el propietario del borde (relación) users, y la entidad User tiene una referencia inversa/borde inverso a esta relación llamada groups. Definamos esta relación en nuestros esqumas:

entdemo/ent/schema/group.go
// Edges of the Group.
func (Group) Edges() []ent.Edge {
return []ent.Edge{
edge.To("users", User.Type),
}
}
entdemo/ent/schema/user.go
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
// Create an inverse-edge called "groups" of type `Group`
// and reference it to the "users" edge (in Group schema)
// explicitly using the `Ref` method.
edge.From("groups", Group.Type).
Ref("users"),
}
}

Ejecutamos ent en el directorio de esquemas para regenerar los recursos.

go generate ./ent

Ejecuta tu primer recorrido de grafo

Ejecutar tu primer recorrido de grafo

re-graph

entdemo/start.go
func CreateGraph(ctx context.Context, client *ent.Client) error {
// First, create the users.
a8m, err := client.User.
Create().
SetAge(30).
SetName("Ariel").
Save(ctx)
if err != nil {
return err
}
neta, err := client.User.
Create().
SetAge(28).
SetName("Neta").
Save(ctx)
if err != nil {
return err
}
// Then, create the cars, and attach them to the users created above.
err = client.Car.
Create().
SetModel("Tesla").
SetRegisteredAt(time.Now()).
// Attach this car to Ariel.
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Mazda").
SetRegisteredAt(time.Now()).
// Attach this car to Ariel.
SetOwner(a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Car.
Create().
SetModel("Ford").
SetRegisteredAt(time.Now()).
// Attach this car to Neta.
SetOwner(neta).
Exec(ctx)
if err != nil {
return err
}
// Create the groups, and add their users in the creation.
err = client.Group.
Create().
SetName("GitLab").
AddUsers(neta, a8m).
Exec(ctx)
if err != nil {
return err
}
err = client.Group.
Create().
SetName("GitHub").
AddUsers(a8m).
Exec(ctx)
if err != nil {
return err
}
log.Println("The graph was created successfully")
return nil
}

Ahora que tenemos un gráfico con datos, podemos ejecutar algunas consultas en él:

  1. Obtener todos los coches de los usuarios del grupo llamado "GitHub":

    entdemo/start.go
    func QueryGithub(ctx context.Context, client *ent.Client) error {
    cars, err := client.Group.
    Query().
    Where(group.Name("GitHub")). // (Group(Name=GitHub),)
    QueryUsers(). // (User(Name=Ariel, Age=30),)
    QueryCars(). // (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
    All(ctx)
    if err != nil {
    return fmt.Errorf("failed getting cars: %w", err)
    }
    log.Println("cars returned:", cars)
    // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Mazda, RegisteredAt=<Time>),)
    return nil
    }
  2. Cambia la consulta anterior para que el origen del recorrido sea el usuario Ariel:

    entdemo/start.go
    func QueryArielCars(ctx context.Context, client *ent.Client) error {
    // Get "Ariel" from previous steps.
    a8m := client.User.
    Query().
    Where(
    user.HasCars(),
    user.Name("Ariel"),
    ).
    OnlyX(ctx)
    cars, err := a8m. // Get the groups, that a8m is connected to:
    QueryGroups(). // (Group(Name=GitHub), Group(Name=GitLab),)
    QueryUsers(). // (User(Name=Ariel, Age=30), User(Name=Neta, Age=28),)
    QueryCars(). //
    Where( //
    car.Not( // Get Neta and Ariel cars, but filter out
    car.Model("Mazda"), // those who named "Mazda"
    ), //
    ). //
    All(ctx)
    if err != nil {
    return fmt.Errorf("failed getting cars: %w", err)
    }
    log.Println("cars returned:", cars)
    // Output: (Car(Model=Tesla, RegisteredAt=<Time>), Car(Model=Ford, RegisteredAt=<Time>),)
    return nil
    }
  3. Obtener todos los grupos que tienen usuarios (consulta con predicado de búsqueda lateral):

    entdemo/start.go
    func QueryGroupWithUsers(ctx context.Context, client *ent.Client) error {
    groups, err := client.Group.
    Query().
    Where(group.HasUsers()).
    All(ctx)
    if err != nil {
    return fmt.Errorf("failed getting groups: %w", err)
    }
    log.Println("groups returned:", groups)
    // Output: (Group(Name=GitHub), Group(Name=GitLab),)
    return nil
    }

Migración de Esquema

Ent ofrece dos enfoques para ejecutar migraciones de esquema: Migraciones Automáticas y Migraciones Versionadas. Aquí tienes un breve resumen de cada enfoque:

Migraciones Automáticas

Con las Migraciones Automáticas, puedes usar la siguiente API para mantener el esquema de la base de datos alineado con los objetos de esquema definidos en el esquema SQL generado ent/migrate/schema.go:

if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}

Este enfoque es especialmente útil para prototipado, desarrollo o pruebas. Por ello, se recomienda usar el enfoque de Migraciones Versionadas para entornos de producción críticos. Al usar migraciones versionadas, sabrás de antemano qué cambios se aplicarán a tu base de datos y podrás ajustarlos según tus necesidades.

Lee más sobre este enfoque en la documentación de Migración Automática.

Migraciones Versionadas

A diferencia de las Migraciones Automáticas, el enfoque de Migraciones Versionadas usa Atlas para generar automáticamente un conjunto de archivos de migración que contienen las sentencias SQL necesarias para migrar la base de datos. Estos archivos pueden editarse para satisfacer necesidades específicas y aplicarse usando herramientas de migración existentes como Atlas, golang-migrate, Flyway y Liquibase. La API para este enfoque implica dos pasos principales.

Generación de migraciones

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

atlas migrate diff migration_name \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mysql/8/ent"

Aplicación de migraciones

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

atlas migrate apply \
--dir "file://ent/migrate/migrations" \
--url "mysql://root:pass@localhost:3306/example"

Lee más sobre este enfoque en la documentación de Migraciones Versionadas.

Ejemplo Completo

El ejemplo completo está disponible en GitHub.