Saltar al contenido principal

Preguntas Frecuentes (FAQ)

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

Preguntas

¿Cómo crear una entidad a partir de una estructura T?
¿Cómo crear un validador a nivel de estructura (o mutación)?
¿Cómo escribir una extensión de registro de auditoría?
¿Cómo escribir predicados personalizados?
¿Cómo añadir predicados personalizados a los activos de generación de código?
¿Cómo definir un campo de dirección de red en PostgreSQL?
¿Cómo personalizar campos temporales al tipo DATETIME en MySQL?
¿Cómo usar un generador personalizado de IDs?
¿Cómo usar un ID global único personalizado (XID)?
¿Cómo definir un campo de tipo de datos espacial en MySQL?
¿Cómo extender los modelos generados?
¿Cómo extender los constructores generados?
¿Cómo almacenar objetos Protobuf en una columna BLOB?
¿Cómo añadir restricciones CHECK a una tabla?
¿Cómo definir un campo numérico de precisión personalizada?
¿Cómo configurar dos o más DB para separar lectura y escritura?
¿Cómo configurar json.Marshal para incluir las claves edges en el objeto de nivel superior?

Respuestas

¿Cómo crear una entidad a partir de una estructura T?

Los diferentes constructores no admiten la opción de configurar los campos de la entidad (o relaciones) a partir de una estructura T dada.
La razón es que no hay forma de distinguir entre valores cero y valores reales al actualizar la base de datos (por ejemplo, &ent.T{Age: 0, Name: ""}).
Establecer estos valores podría asignar valores incorrectos en la base de datos o actualizar columnas innecesarias.

Sin embargo, la opción de plantilla externa te permite extender los activos de generación de código predeterminados añadiendo lógica personalizada.
Por ejemplo, para generar un método en cada constructor de creación que acepte una estructura como entrada y configure el constructor, usa la siguiente plantilla:

{{ range $n := $.Nodes }}
{{ $builder := $n.CreateName }}
{{ $receiver := $n.CreateReceiver }}

func ({{ $receiver }} *{{ $builder }}) Set{{ $n.Name }}(input *{{ $n.Name }}) *{{ $builder }} {
{{- range $f := $n.Fields }}
{{- $setter := print "Set" $f.StructField }}
{{ $receiver }}.{{ $setter }}(input.{{ $f.StructField }})
{{- end }}
return {{ $receiver }}
}
{{ end }}

¿Cómo crear un validador a nivel de mutación?

Para implementar un validador a nivel de mutación, puedes usar hooks de esquema para validar cambios aplicados en un tipo de entidad, o usar hooks de transacción para validar mutaciones aplicadas en múltiples tipos de entidades (por ejemplo, una mutación de GraphQL). Por ejemplo:

// A VersionHook is a dummy example for a hook that validates the "version" field
// is incremented by 1 on each update. Note that this is just a dummy example, and
// it doesn't promise consistency in the database.
func VersionHook() ent.Hook {
type OldSetVersion interface {
SetVersion(int)
Version() (int, bool)
OldVersion(context.Context) (int, error)
}
return func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
ver, ok := m.(OldSetVersion)
if !ok {
return next.Mutate(ctx, m)
}
oldV, err := ver.OldVersion(ctx)
if err != nil {
return nil, err
}
curV, exists := ver.Version()
if !exists {
return nil, fmt.Errorf("version field is required in update mutation")
}
if curV != oldV+1 {
return nil, fmt.Errorf("version field must be incremented by 1")
}
// Add an SQL predicate that validates the "version" column is equal
// to "oldV" (ensure it wasn't changed during the mutation by others).
return next.Mutate(ctx, m)
})
}
}

¿Cómo escribir una extensión de registro de auditoría?

La forma preferida para escribir esta extensión es usando ent.Mixin. Usa la opción Fields para configurar campos compartidos entre todos los esquemas que importen el esquema mixto, y la opción Hooks para adjuntar un hook de mutación a todas las mutaciones aplicadas en estos esquemas. Aquí tienes un ejemplo basado en una discusión en el seguimiento de incidencias del repositorio:

// AuditMixin implements the ent.Mixin for sharing
// audit-log capabilities with package schemas.
type AuditMixin struct{
mixin.Schema
}

// Fields of the AuditMixin.
func (AuditMixin) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Immutable().
Default(time.Now),
field.Int("created_by").
Optional(),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now),
field.Int("updated_by").
Optional(),
}
}

// Hooks of the AuditMixin.
func (AuditMixin) Hooks() []ent.Hook {
return []ent.Hook{
hooks.AuditHook,
}
}

// A AuditHook is an example for audit-log hook.
func AuditHook(next ent.Mutator) ent.Mutator {
// AuditLogger wraps the methods that are shared between all mutations of
// schemas that embed the AuditLog mixin. The variable "exists" is true, if
// the field already exists in the mutation (e.g. was set by a different hook).
type AuditLogger interface {
SetCreatedAt(time.Time)
CreatedAt() (value time.Time, exists bool)
SetCreatedBy(int)
CreatedBy() (id int, exists bool)
SetUpdatedAt(time.Time)
UpdatedAt() (value time.Time, exists bool)
SetUpdatedBy(int)
UpdatedBy() (id int, exists bool)
}
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
ml, ok := m.(AuditLogger)
if !ok {
return nil, fmt.Errorf("unexpected audit-log call from mutation type %T", m)
}
usr, err := viewer.UserFromContext(ctx)
if err != nil {
return nil, err
}
switch op := m.Op(); {
case op.Is(ent.OpCreate):
ml.SetCreatedAt(time.Now())
if _, exists := ml.CreatedBy(); !exists {
ml.SetCreatedBy(usr.ID)
}
case op.Is(ent.OpUpdateOne | ent.OpUpdate):
ml.SetUpdatedAt(time.Now())
if _, exists := ml.UpdatedBy(); !exists {
ml.SetUpdatedBy(usr.ID)
}
}
return next.Mutate(ctx, m)
})
}

¿Cómo escribir predicados personalizados?

Los usuarios pueden proporcionar predicados personalizados para aplicar a la consulta antes de su ejecución. Por ejemplo:

pets := client.Pet.
Query().
Where(predicate.Pet(func(s *sql.Selector) {
s.Where(sql.InInts(pet.OwnerColumn, 1, 2, 3))
})).
AllX(ctx)

users := client.User.
Query().
Where(predicate.User(func(s *sql.Selector) {
s.Where(sqljson.ValueContains(user.FieldTags, "tag"))
})).
AllX(ctx)

Para más ejemplos, visita la página de predicados o busca en el rastreador de incidencias del repositorio ejemplos más avanzados como la incidencia-842.

¿Cómo agregar predicados personalizados a los recursos de generación de código?

La opción de plantilla permite extender o modificar los recursos predeterminados de generación de código. Para generar predicados tipados seguros como en el ejemplo anterior, utiliza la plantilla de la siguiente manera:

{{/* A template that adds the "<F>Glob" predicate for all string fields. */}}
{{ define "where/additional/strings" }}
{{ range $f := $.Fields }}
{{ if $f.IsString }}
{{ $func := print $f.StructField "Glob" }}
// {{ $func }} applies the Glob predicate on the {{ quote $f.Name }} field.
func {{ $func }}(pattern string) predicate.{{ $.Name }} {
return predicate.{{ $.Name }}(func(s *sql.Selector) {
s.Where(sql.P(func(b *sql.Builder) {
b.Ident(s.C({{ $f.Constant }})).WriteString(" glob" ).Arg(pattern)
}))
})
}
{{ end }}
{{ end }}
{{ end }}

¿Cómo definir un campo de dirección de red en PostgreSQL?

Las opciones GoType y SchemaType permiten definir campos específicos de la base de datos. Por ejemplo, para definir un campo macaddr, usa esta configuración:

func (T) Fields() []ent.Field {
return []ent.Field{
field.String("mac").
GoType(&MAC{}).
SchemaType(map[string]string{
dialect.Postgres: "macaddr",
}).
Validate(func(s string) error {
_, err := net.ParseMAC(s)
return err
}),
}
}

// MAC represents a physical hardware address.
type MAC struct {
net.HardwareAddr
}

// Scan implements the Scanner interface.
func (m *MAC) Scan(value any) (err error) {
switch v := value.(type) {
case nil:
case []byte:
m.HardwareAddr, err = net.ParseMAC(string(v))
case string:
m.HardwareAddr, err = net.ParseMAC(v)
default:
err = fmt.Errorf("unexpected type %T", v)
}
return
}

// Value implements the driver Valuer interface.
func (m MAC) Value() (driver.Value, error) {
return m.HardwareAddr.String(), nil
}

Nota: si la base de datos no soporta el tipo macaddr (ej. SQLite en pruebas), el campo recurre a su tipo nativo (es decir, string).

Ejemplo con inet:

func (T) Fields() []ent.Field {
return []ent.Field{
field.String("ip").
GoType(&Inet{}).
SchemaType(map[string]string{
dialect.Postgres: "inet",
}).
Validate(func(s string) error {
if net.ParseIP(s) == nil {
return fmt.Errorf("invalid value for ip %q", s)
}
return nil
}),
}
}

// Inet represents a single IP address
type Inet struct {
net.IP
}

// Scan implements the Scanner interface
func (i *Inet) Scan(value any) (err error) {
switch v := value.(type) {
case nil:
case []byte:
if i.IP = net.ParseIP(string(v)); i.IP == nil {
err = fmt.Errorf("invalid value for ip %q", v)
}
case string:
if i.IP = net.ParseIP(v); i.IP == nil {
err = fmt.Errorf("invalid value for ip %q", v)
}
default:
err = fmt.Errorf("unexpected type %T", v)
}
return
}

// Value implements the driver Valuer interface
func (i Inet) Value() (driver.Value, error) {
return i.IP.String(), nil
}

¿Cómo personalizar campos de tiempo al tipo DATETIME en MySQL?

Por defecto, los campos Time usan el tipo TIMESTAMP de MySQL en la creación del esquema, y este tipo tiene un rango de '1970-01-01 00:00:01' UTC a '2038-01-19 03:14:07' UTC (ver documentación de MySQL).

Para personalizar campos de tiempo con un rango más amplio, usa DATETIME de MySQL así:

field.Time("birth_date").
Optional().
SchemaType(map[string]string{
dialect.MySQL: "datetime",
}),

¿Cómo usar un generador personalizado de IDs?

Si estás usando un generador personalizado de IDs en lugar de IDs autoincrementales en tu base de datos (ej. Snowflake de Twitter), necesitarás crear un campo ID personalizado que llame automáticamente al generador durante la creación de recursos.

Para lograrlo, puedes usar DefaultFunc o hooks de esquema, dependiendo de tu caso de uso. Si el generador no devuelve errores, DefaultFunc es más conciso, mientras que configurar un hook en la creación de recursos te permitirá capturar errores también. Un ejemplo de cómo usar DefaultFunc está en la sección sobre el campo ID.

Aquí un ejemplo usando un generador personalizado con hooks, tomando como referencia sonyflake:

// BaseMixin to be shared will all different schemas.
type BaseMixin struct {
mixin.Schema
}

// Fields of the Mixin.
func (BaseMixin) Fields() []ent.Field {
return []ent.Field{
field.Uint64("id"),
}
}

// Hooks of the Mixin.
func (BaseMixin) Hooks() []ent.Hook {
return []ent.Hook{
hook.On(IDHook(), ent.OpCreate),
}
}

func IDHook() ent.Hook {
sf := sonyflake.NewSonyflake(sonyflake.Settings{})
type IDSetter interface {
SetID(uint64)
}
return func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
is, ok := m.(IDSetter)
if !ok {
return nil, fmt.Errorf("unexpected mutation %T", m)
}
id, err := sf.NextID()
if err != nil {
return nil, err
}
is.SetID(id)
return next.Mutate(ctx, m)
})
}
}

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

// Mixin of the User.
func (User) Mixin() []ent.Mixin {
return []ent.Mixin{
// Embed the BaseMixin in the user schema.
BaseMixin{},
}
}

¿Cómo usar un ID global único personalizado XID?

El paquete xid es una biblioteca generadora de IDs únicos globales que usa el algoritmo Mongo Object ID para generar un ID de 12 bytes y 20 caracteres sin configuración. El paquete xid incluye las interfaces database/sql sql.Scanner y driver.Valuer requeridas por Ent para la serialización.

Para almacenar un XID en cualquier campo string, usa la configuración de esquema GoType:

// Fields of type T.
func (T) Fields() []ent.Field {
return []ent.Field{
field.String("id").
GoType(xid.ID{}).
DefaultFunc(xid.New),
}
}

O como un Mixin reutilizable en múltiples esquemas:

package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"
"github.com/rs/xid"
)

// BaseMixin to be shared will all different schemas.
type BaseMixin struct {
mixin.Schema
}

// Fields of the User.
func (BaseMixin) Fields() []ent.Field {
return []ent.Field{
field.String("id").
GoType(xid.ID{}).
DefaultFunc(xid.New),
}
}

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

// Mixin of the User.
func (User) Mixin() []ent.Mixin {
return []ent.Mixin{
// Embed the BaseMixin in the user schema.
BaseMixin{},
}
}

Para usar identificadores extendidos (XIDs) con gqlgen, sigue la configuración mencionada en el rastreador de incidencias.

¿Cómo definir un campo de tipo de dato espacial en MySQL?

Las opciones GoType y SchemaType permiten definir campos específicos de la base de datos. Por ejemplo, para definir un campo POINT, utiliza esta configuración:

// Fields of the Location.
func (Location) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Other("coords", &Point{}).
SchemaType(Point{}.SchemaType()),
}
}
package schema

import (
"database/sql/driver"
"fmt"

"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
"github.com/paulmach/orb"
"github.com/paulmach/orb/encoding/wkb"
)

// A Point consists of (X,Y) or (Lat, Lon) coordinates
// and it is stored in MySQL the POINT spatial data type.
type Point [2]float64

// Scan implements the Scanner interface.
func (p *Point) Scan(value any) error {
bin, ok := value.([]byte)
if !ok {
return fmt.Errorf("invalid binary value for point")
}
var op orb.Point
if err := wkb.Scanner(&op).Scan(bin[4:]); err != nil {
return err
}
p[0], p[1] = op.X(), op.Y()
return nil
}

// Value implements the driver Valuer interface.
func (p Point) Value() (driver.Value, error) {
op := orb.Point{p[0], p[1]}
return wkb.Value(op).Value()
}

// FormatParam implements the sql.ParamFormatter interface to tell the SQL
// builder that the placeholder for a Point parameter needs to be formatted.
func (p Point) FormatParam(placeholder string, info *sql.StmtInfo) string {
if info.Dialect == dialect.MySQL {
return "ST_GeomFromWKB(" + placeholder + ")"
}
return placeholder
}

// SchemaType defines the schema-type of the Point object.
func (Point) SchemaType() map[string]string {
return map[string]string{
dialect.MySQL: "POINT",
}
}

Puedes encontrar un ejemplo completo en el repositorio de ejemplo.

¿Cómo extender los modelos generados?

Ent permite extender tipos generados (tanto globales como modelos) usando plantillas personalizadas. Por ejemplo, para añadir campos o métodos adicionales al modelo generado, podemos sobrescribir la plantilla model/fields/additional como en este ejemplo.

Si tus campos/métodos personalizados requieren imports adicionales, puedes añadirlos mediante plantillas personalizadas:

{{- define "import/additional/field_types" -}}
"github.com/path/to/your/custom/type"
{{- end -}}

{{- define "import/additional/client_dependencies" -}}
"github.com/path/to/your/custom/type"
{{- end -}}

¿Cómo extender los builders generados?

Consulta la sección Inyectar dependencias externas o sigue el ejemplo en GitHub.

¿Cómo almacenar objetos Protobuf en una columna BLOB?

Supongamos que tenemos un mensaje Protobuf definido:

syntax = "proto3";

package pb;

option go_package = "project/pb";

message Hi {
string Greeting = 1;
}

Añadimos métodos receptores a la estructura generada por protobuf para que implemente ValueScanner

func (x *Hi) Value() (driver.Value, error) {
return proto.Marshal(x)
}

func (x *Hi) Scan(src any) error {
if src == nil {
return nil
}
if b, ok := src.([]byte); ok {
if err := proto.Unmarshal(b, x); err != nil {
return err
}
return nil
}
return fmt.Errorf("unexpected type %T", src)
}

Añadimos un nuevo field.Bytes a nuestro esquema, configurando la estructura generada por protobuf como su GoType subyacente:

// Fields of the Message.
func (Message) Fields() []ent.Field {
return []ent.Field{
field.Bytes("hi").
GoType(&pb.Hi{}),
}
}

Verificamos que funciona:

package main

import (
"context"
"testing"

"project/ent/enttest"
"project/pb"

_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/require"
)

func TestMain(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

msg := client.Message.Create().
SetHi(&pb.Hi{
Greeting: "hello",
}).
SaveX(context.TODO())

ret := client.Message.GetX(context.TODO(), msg.ID)
require.Equal(t, "hello", ret.Hi.Greeting)
}

¿Cómo añadir restricciones CHECK a una tabla?

La opción entsql.Annotation permite añadir restricciones CHECK personalizadas a la sentencia CREATE TABLE. Para añadir restricciones CHECK a tu esquema, sigue este ejemplo:

func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
&entsql.Annotation{
// The `Check` option allows adding an
// unnamed CHECK constraint to table DDL.
Check: "website <> 'entgo.io'",

// The `Checks` option allows adding multiple CHECK constraints
// to table creation. The keys are used as the constraint names.
Checks: map[string]string{
"valid_nickname": "nickname <> firstname",
"valid_firstname": "length(first_name) > 1",
},
},
}
}

¿Cómo definir un campo numérico con precisión personalizada?

Usando GoType y SchemaType es posible definir campos numéricos con precisión personalizada. Por ejemplo, para definir un campo que use big.Int:

func (T) Fields() []ent.Field {
return []ent.Field{
field.Int("precise").
GoType(new(BigInt)).
SchemaType(map[string]string{
dialect.SQLite: "numeric(78, 0)",
dialect.Postgres: "numeric(78, 0)",
}),
}
}

type BigInt struct {
big.Int
}

func (b *BigInt) Scan(src any) error {
var i sql.NullString
if err := i.Scan(src); err != nil {
return err
}
if !i.Valid {
return nil
}
if _, ok := b.Int.SetString(i.String, 10); ok {
return nil
}
return fmt.Errorf("could not scan type %T with value %v into BigInt", src, src)
}

func (b *BigInt) Value() (driver.Value, error) {
return b.String(), nil
}

¿Cómo configurar dos o más DB para separar lectura y escritura?

Puedes envolver el dialect.Driver con tu propio driver e implementar esta lógica. Por ejemplo:

Puedes extenderlo, añadir soporte para réplicas de lectura múltiples e implementar balanceo de carga.

func main() {
// ...
wd, err := sql.Open(dialect.MySQL, "root:pass@tcp(<addr>)/<database>?parseTime=True")
if err != nil {
log.Fatal(err)
}
rd, err := sql.Open(dialect.MySQL, "readonly:pass@tcp(<addr>)/<database>?parseTime=True")
if err != nil {
log.Fatal(err)
}
client := ent.NewClient(ent.Driver(&multiDriver{w: wd, r: rd}))
defer client.Close()
// Use the client here.
}

type multiDriver struct {
r, w dialect.Driver
}

var _ dialect.Driver = (*multiDriver)(nil)

func (d *multiDriver) Query(ctx context.Context, query string, args, v any) error {
e := d.r
// Mutation statements that use the RETURNING clause.
if ent.QueryFromContext(ctx) == nil {
e = d.w
}
return e.Query(ctx, query, args, v)
}

func (d *multiDriver) Exec(ctx context.Context, query string, args, v any) error {
return d.w.Exec(ctx, query, args, v)
}

func (d *multiDriver) Tx(ctx context.Context) (dialect.Tx, error) {
return d.w.Tx(ctx)
}

func (d *multiDriver) BeginTx(ctx context.Context, opts *sql.TxOptions) (dialect.Tx, error) {
return d.w.(interface {
BeginTx(context.Context, *sql.TxOptions) (dialect.Tx, error)
}).BeginTx(ctx, opts)
}

func (d *multiDriver) Close() error {
rerr := d.r.Close()
werr := d.w.Close()
if rerr != nil {
return rerr
}
if werr != nil {
return werr
}
return nil
}

func (d *multiDriver) Dialect() string {
return d.r.Dialect()
}

¿Cómo configurar json.Marshal para incluir claves edges en el objeto principal?

Para codificar entidades sin el atributo edges, sigue estos dos pasos:

  1. Omite la etiqueta edges predeterminada generada por Ent.

  2. Extiende los modelos generados con un método MarshalJSON personalizado.

Estos pasos pueden automatizarse mediante extensiones de codegen. Un ejemplo funcional completo está disponible en el directorio examples/jsonencode.

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

package main

import (
"log"

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

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

// EncodeExtension is an implementation of entc.Extension that adds a MarshalJSON
// method to each generated type <T> and inlines the Edges field to the top level JSON.
type EncodeExtension struct {
entc.DefaultExtension
}

// Templates of the extension.
func (e *EncodeExtension) Templates() []*gen.Template {
return []*gen.Template{
gen.MustParse(gen.NewTemplate("model/additional/jsonencode").
Parse(`
{{ if $.Edges }}
// MarshalJSON implements the json.Marshaler interface.
func ({{ $.Receiver }} *{{ $.Name }}) MarshalJSON() ([]byte, error) {
type Alias {{ $.Name }}
return json.Marshal(&struct {
*Alias
{{ $.Name }}Edges
}{
Alias: (*Alias)({{ $.Receiver }}),
{{ $.Name }}Edges: {{ $.Receiver }}.Edges,
})
}
{{ end }}
`)),
}
}

// Hooks of the extension.
func (e *EncodeExtension) Hooks() []gen.Hook {
return []gen.Hook{
func(next gen.Generator) gen.Generator {
return gen.GenerateFunc(func(g *gen.Graph) error {
tag := edge.Annotation{StructTag: `json:"-"`}
for _, n := range g.Nodes {
n.Annotations.Set(tag.Name(), tag)
}
return next.Generate(g)
})
},
}
}