Saltar al contenido principal

Campos

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

Resumen rápido

Los campos (o propiedades) en el esquema son los atributos del nodo. Por ejemplo, un User con 4 campos: age, name, username y created_at:

campos-propiedades

Los campos se devuelven desde el esquema usando el método Fields. Por ejemplo:

package schema

import (
"time"

"entgo.io/ent"
"entgo.io/ent/schema/field"
)

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age"),
field.String("name"),
field.String("username").
Unique(),
field.Time("created_at").
Default(time.Now),
}
}

Todos los campos son obligatorios por defecto, y pueden hacerse opcionales usando el método Optional.

Tipos

El framework actualmente soporta los siguientes tipos:

  • Todos los tipos numéricos de Go. Como int, uint8, float64, etc.

  • bool

  • string

  • time.Time

  • UUID

  • []byte (solo SQL).

  • JSON (solo SQL).

  • Enum (solo SQL).

  • Other (solo SQL).

package schema

import (
"time"
"net/url"

"github.com/google/uuid"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.Float("rank").
Optional(),
field.Bool("active").
Default(false),
field.String("name").
Unique(),
field.Time("created_at").
Default(time.Now),
field.JSON("url", &url.URL{}).
Optional(),
field.JSON("strings", []string{}).
Optional(),
field.Enum("state").
Values("on", "off").
Optional(),
field.UUID("uuid", uuid.UUID{}).
Default(uuid.New),
}
}

Para más detalles sobre cómo cada tipo se mapea a su tipo de base de datos, consulta la sección Migración.

Campo ID

El campo id está incorporado en el esquema y no necesita declaración. En bases de datos SQL, su tipo por defecto es int (pero puede cambiarse con una opción de generación de código) y se autoincrementa en la base de datos.

Para configurar el campo id como único en todas las tablas, usa la opción WithGlobalUniqueID al ejecutar la migración del esquema.

Si necesitas una configuración diferente para el campo id, o el valor de id debe ser proporcionado por la aplicación al crear la entidad (por ejemplo, un UUID), anula la configuración incorporada de id. Por ejemplo:

// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.Int("id").
StructTag(`json:"oid,omitempty"`),
}
}

// Fields of the Blob.
func (Blob) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.UUID{}).
Default(uuid.New).
StorageKey("oid"),
}
}

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("id").
MaxLen(25).
NotEmpty().
Unique().
Immutable(),
}
}

Si necesitas establecer una función personalizada para generar IDs, puedes usar DefaultFunc para especificar una función que siempre se ejecutará al crear el recurso. Consulta el FAQ relacionado para más información.

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int64("id").
DefaultFunc(func() int64 {
// An example of a dumb ID generator - use a production-ready alternative instead.
return time.Now().Unix() << 8 | atomic.AddInt64(&counter, 1) % 256
}),
}
}

Tipo de base de datos

Cada dialecto de base de datos tiene su propio mapeo de tipo Go a tipo de base de datos. Por ejemplo, el dialecto MySQL crea campos float64 como columnas double en la base de datos. Sin embargo, existe una opción para anular este comportamiento predeterminado usando el método SchemaType.

package schema

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

// Card schema.
type Card struct {
ent.Schema
}

// Fields of the Card.
func (Card) Fields() []ent.Field {
return []ent.Field{
field.Float("amount").
SchemaType(map[string]string{
dialect.MySQL: "decimal(6,2)", // Override MySQL.
dialect.Postgres: "numeric", // Override Postgres.
}),
}
}

Tipo Go

El tipo predeterminado para los campos son los tipos básicos de Go. Por ejemplo, para campos de texto el tipo es string, y para campos de tiempo es time.Time. El método GoType permite anular el tipo ent predeterminado con uno personalizado.

El tipo personalizado debe ser un tipo convertible al tipo básico de Go, un tipo que implemente la interfaz ValueScanner, o tener un ValueScanner externo. Además, si el tipo proporcionado implementa la interfaz Validator y no se han establecido validadores, se usará el validador del tipo.

package schema

import (
"database/sql"

"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/schema/field"
"github.com/shopspring/decimal"
)

// Amount is a custom Go type that's convertible to the basic float64 type.
type Amount float64

// Card schema.
type Card struct {
ent.Schema
}

// Fields of the Card.
func (Card) Fields() []ent.Field {
return []ent.Field{
field.Float("amount").
GoType(Amount(0)),
field.String("name").
Optional().
// A ValueScanner type.
GoType(&sql.NullString{}),
field.Enum("role").
// A convertible type to string.
GoType(role.Role("")),
field.Float("decimal").
// A ValueScanner type mixed with SchemaType.
GoType(decimal.Decimal{}).
SchemaType(map[string]string{
dialect.MySQL: "decimal(6,2)",
dialect.Postgres: "numeric",
}),
}
}

ValueScanner externo

Ent permite adjuntar un ValueScanner personalizado para tipos básicos o personalizados de Go. Esto permite usar campos estándar del esquema manteniendo el control sobre cómo se almacenan en la base de datos sin implementar la interfaz ValueScanner. Adicionalmente, esta opción permite usar GoType que no implemente ValueScanner, como *url.URL.

nota

Actualmente, esta opción solo está disponible para campos de texto y numéricos, pero se extenderá a otros tipos en el futuro.

Fields with a custom Go type that implements the encoding.TextMarshaller and encoding.TextUnmarshaller interfaces can use the field.TextValueScanner as a ValueScanner. This ValueScanner calls MarshalText and UnmarshalText for writing and reading field values from the database:

field.String("big_int").
GoType(&big.Int{}).
ValueScanner(field.TextValueScanner[*big.Int]{})

Otros Campos (Other Field)

"Other" representa un campo que no encaja bien en ningún tipo estándar. Ejemplos típicos son tipos Range de Postgres o tipos Geospatial.

package schema

import (
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/schema/field"

"github.com/jackc/pgtype"
)

// User schema.
type User struct {
ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Other("duration", &pgtype.Tstzrange{}).
SchemaType(map[string]string{
dialect.Postgres: "tstzrange",
}),
}
}

Valores Predeterminados

Los campos no únicos admiten valores predeterminados mediante los métodos Default y UpdateDefault. También puedes usar DefaultFunc para especificar un generador personalizado.

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now),
field.String("name").
Default("unknown"),
field.String("cuid").
DefaultFunc(cuid.New),
field.JSON("dirs", []http.Dir{}).
Default([]http.Dir{"/tmp"}),
}
}

Literales o expresiones específicas de SQL (como llamadas a funciones) pueden añadirse a la configuración de valores predeterminados usando la anotación entsql.Annotation:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
// Add a new field with CURRENT_TIMESTAMP
// as a default value to all previous rows.
field.Time("created_at").
Default(time.Now).
Annotations(
entsql.Default("CURRENT_TIMESTAMP"),
),
// Add a new field with a default value
// expression that works on all dialects.
field.String("field").
Optional().
Annotations(
entsql.DefaultExpr("lower(other_field)"),
),
// Add a new field with custom default value
// expression for each dialect.
field.String("default_exprs").
Optional().
Annotations(
entsql.DefaultExprs(map[string]string{
dialect.MySQL: "TO_BASE64('ent')",
dialect.SQLite: "hex('ent')",
dialect.Postgres: "md5('ent')",
}),
),
}
}

Si tu DefaultFunc también devuelve un error, es mejor manejarlo adecuadamente usando schema-hooks. Consulta este FAQ para más información.

Validadores

Un validador de campo es una función de tipo func(T) error definida en el esquema mediante el método Validate, que se aplica al valor del campo antes de crear o actualizar la entidad.

Los tipos admitidos para validadores son string y todos los tipos numéricos.

package schema

import (
"errors"
"regexp"
"strings"
"time"

"entgo.io/ent"
"entgo.io/ent/schema/field"
)


// Group schema.
type Group struct {
ent.Schema
}

// Fields of the group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Match(regexp.MustCompile("[a-zA-Z_]+$")).
Validate(func(s string) error {
if strings.ToLower(s) == s {
return errors.New("group name must begin with uppercase")
}
return nil
}),
}
}

Aquí otro ejemplo de validador reutilizable:

import (
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/field"
)

// MaxRuneCount validates the rune length of a string by using the unicode/utf8 package.
func MaxRuneCount(maxLen int) func(s string) error {
return func(s string) error {
if utf8.RuneCountInString(s) > maxLen {
return errors.New("value is more than the max length")
}
return nil
}
}

field.String("name").
// If using a SQL-database: change the underlying data type to varchar(10).
Annotations(entsql.Annotation{
Size: 10,
}).
Validate(MaxRuneCount(10))
field.String("nickname").
// If using a SQL-database: change the underlying data type to varchar(20).
Annotations(entsql.Annotation{
Size: 20,
}).
Validate(MaxRuneCount(20))

Validadores Integrados

El framework incluye varios validadores predefinidos para cada tipo:

  • Tipos numéricos:

    • Positive()
    • Negative()
    • NonNegative()
    • Min(i) - Valida que el valor sea > i.
    • Max(i) - Valida que el valor sea < i.
    • Range(i, j) - Valida que el valor esté en el rango [i, j].
  • string:

    • MinLen(i)
    • MaxLen(i)
    • Match(regexp.Regexp)
    • NotEmpty
  • []byte:

    • MaxLen(i)
    • MinLen(i)
    • NotEmpty

Opcionales

Los campos opcionales no son obligatorios al crear una entidad y se mapean a campos anulables en la base de datos. A diferencia de las relaciones, los campos son obligatorios por defecto. Para hacerlos opcionales se usa explícitamente el método Optional.

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("required_name"),
field.String("optional_name").
Optional(),
}
}

Nulables

A veces necesitas distinguir entre el valor cero de un campo y nil. Por ejemplo, si una columna contiene 0 o NULL. La opción Nillable existe precisamente para esto.

Si tienes un campo Optional de tipo T, marcarlo como Nillable generará un campo en el struct con tipo *T. Así, si la base de datos devuelve NULL, el campo será nil; en caso contrario contendrá un puntero al valor real.

Por ejemplo, con este esquema:

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("required_name"),
field.String("optional_name").
Optional(),
field.String("nillable_name").
Optional().
Nillable(),
}
}

El struct generado para la entidad User será:

ent/user.go
package ent

// User entity.
type User struct {
RequiredName string `json:"required_name,omitempty"`
OptionalName string `json:"optional_name,omitempty"`
NillableName *string `json:"nillable_name,omitempty"`
}

Campos obligatorios Nillable

Los campos Nillable también evitan valores cero al serializar JSON cuando no se han seleccionado (Select) en la consulta. Ejemplo con campos time.Time:

// Fields of the task.
func (Task) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now),
field.Time("nillable_created_at").
Default(time.Now).
Nillable(),
}
}

El struct generado para la entidad Task será:

ent/task.go
package ent

// Task entity.
type Task struct {
// CreatedAt holds the value of the "created_at" field.
CreatedAt time.Time `json:"created_at,omitempty"`
// NillableCreatedAt holds the value of the "nillable_created_at" field.
NillableCreatedAt *time.Time `json:"nillable_created_at,omitempty"`
}

Y el resultado de json.Marshal será:

b, _ := json.Marshal(Task{})
fmt.Printf("%s\n", b)
// {"created_at":"0001-01-01T00:00:00Z"}

now := time.Now()
b, _ = json.Marshal(Task{CreatedAt: now, NillableCreatedAt: &now})
fmt.Printf("%s\n", b)
// {"created_at":"2009-11-10T23:00:00Z","nillable_created_at":"2009-11-10T23:00:00Z"}

Inmutables

Los campos inmutables solo pueden definirse durante la creación de la entidad. No se generarán setters en los builders de actualización.

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now).
Immutable(),
}
}

Unicidad

Los campos pueden definirse como únicos con el método Unique. Nota: los campos únicos no pueden tener valores predeterminados.

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("nickname").
Unique(),
}
}

Comentarios

Se puede agregar un comentario a un campo usando el método .Comment(). Este comentario aparece antes del campo en el código generado de la entidad. Se admiten saltos de línea usando la secuencia de escape \n.

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Default("John Doe").
Comment("Name of the user.\n If not specified, defaults to \"John Doe\"."),
}
}

Campos Desaprobados

El método Deprecated se puede usar para marcar un campo como desaprobado. Los campos desaprobados no se seleccionan por defecto en las consultas, y sus campos en la estructura se anotan como Deprecated en el código generado.

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Deprecated("use `full_name` instead"),
}
}

Clave de Almacenamiento

Se puede configurar un nombre personalizado para almacenamiento usando el método StorageKey. Se mapea a un nombre de columna en dialectos SQL y a un nombre de propiedad en Gremlin.

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
StorageKey("old_name"),
}
}

Índices

Se pueden definir índices en múltiples campos y también en algunos tipos de relaciones. Sin embargo, debes tener en cuenta que actualmente esta es una característica exclusiva de SQL.

Lee más sobre esto en la sección Índices.

Etiquetas de Estructura

Se pueden agregar etiquetas personalizadas a las estructuras generadas usando el método StructTag. Ten en cuenta que si no se proporciona esta opción, o se proporciona pero no contiene la etiqueta json, se agregará automáticamente la etiqueta json con el nombre del campo.

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
StructTag(`gqlgen:"gql_name"`),
}
}

Campos Adicionales en Estructuras

Por defecto, ent genera el modelo de entidad con los campos configurados en el método schema.Fields. Por ejemplo, dada esta configuración de esquema:

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Optional().
Nillable(),
field.String("name").
StructTag(`gqlgen:"gql_name"`),
}
}

El modelo generado será el siguiente:

// User is the model entity for the User schema.
type User struct {
// Age holds the value of the "age" field.
Age *int `json:"age,omitempty"`
// Name holds the value of the "name" field.
Name string `json:"name,omitempty" gqlgen:"gql_name"`
}

Para agregar campos adicionales a la estructura generada que no se almacenen en la base de datos, usa plantillas externas. Por ejemplo:

{{ define "model/fields/additional" }}
{{- if eq $.Name "User" }}
// StaticField defined by template.
StaticField string `json:"static,omitempty"`
{{- end }}
{{ end }}

El modelo generado será el siguiente:

// User is the model entity for the User schema.
type User struct {
// Age holds the value of the "age" field.
Age *int `json:"age,omitempty"`
// Name holds the value of the "name" field.
Name string `json:"name,omitempty" gqlgen:"gql_name"`
// StaticField defined by template.
StaticField string `json:"static,omitempty"`
}

Campos Sensibles

Los campos de tipo string pueden definirse como sensibles usando el método Sensitive. Los campos sensibles no se imprimirán y se omitirán al codificar.

Ten en cuenta que los campos sensibles no pueden tener etiquetas de estructura.

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("password").
Sensitive(),
}
}

Campos de Enumeración

El constructor Enum permite crear campos de enumeración con una lista de valores permitidos.

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
field.Enum("size").
Values("big", "small"),
}
}

Por defecto, Ent usa tipos string simples para representar valores de enumeración en PostgreSQL y SQLite. Sin embargo, en algunos casos, puedes querer usar los tipos enum nativos de la base de datos. Sigue la guía de migración para enum para más información.

Cuando se usa un GoType personalizado, debe ser convertible al tipo básico string o debe implementar la interfaz ValueScanner.

La interfaz EnumValues también es requerida para que el tipo Go personalizado indique a Ent cuáles son los valores permitidos de la enumeración.

El siguiente ejemplo muestra cómo definir un campo Enum con un tipo Go personalizado convertible a string:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
// A convertible type to string.
field.Enum("shape").
GoType(property.Shape("")),
}
}

Implementa la interfaz EnumValues.

package property

type Shape string

const (
Triangle Shape = "TRIANGLE"
Circle Shape = "CIRCLE"
)

// Values provides list valid values for Enum.
func (Shape) Values() (kinds []string) {
for _, s := range []Shape{Triangle, Circle} {
kinds = append(kinds, string(s))
}
return
}

El siguiente ejemplo muestra cómo definir un campo Enum con un tipo Go personalizado que no es convertible a string, pero implementa la interfaz ValueScanner:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
// Add conversion to and from string
field.Enum("level").
GoType(property.Level(0)),
}
}

Implementa también la interfaz ValueScanner.

package property

import "database/sql/driver"

type Level int

const (
Unknown Level = iota
Low
High
)

func (p Level) String() string {
switch p {
case Low:
return "LOW"
case High:
return "HIGH"
default:
return "UNKNOWN"
}
}

// Values provides list valid values for Enum.
func (Level) Values() []string {
return []string{Unknown.String(), Low.String(), High.String()}
}

// Value provides the DB a string from int.
func (p Level) Value() (driver.Value, error) {
return p.String(), nil
}

// Scan tells our code how to read the enum into our type.
func (p *Level) Scan(val any) error {
var s string
switch v := val.(type) {
case nil:
return nil
case string:
s = v
case []uint8:
s = string(v)
}
switch s {
case "LOW":
*p = Low
case "HIGH":
*p = High
default:
*p = Unknown
}
return nil
}

Combinando todo:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("first_name"),
field.String("last_name"),
field.Enum("size").
Values("big", "small"),
// A convertible type to string.
field.Enum("shape").
GoType(property.Shape("")),
// Add conversion to and from string.
field.Enum("level").
GoType(property.Level(0)),
}
}

Después de generar el código, su uso es trivial:

client.User.Create().
SetFirstName("John").
SetLastName("Dow").
SetSize(user.SizeSmall).
SetShape(property.Triangle).
SetLevel(property.Low).
SaveX(context.Background())

john := client.User.Query().FirstX(context.Background())
fmt.Println(john)
// User(id=1, first_name=John, last_name=Dow, size=small, shape=TRIANGLE, level=LOW)

Anotaciones

Annotations se usa para adjuntar metadatos arbitrarios al objeto de campo durante la generación de código. Las extensiones de plantillas pueden recuperar estos metadatos y usarlos dentro de sus plantillas.

Ten en cuenta que el objeto de metadatos debe ser serializable a un valor JSON en bruto (por ejemplo, struct, map o slice).

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Time("creation_date").
Annotations(entgql.Annotation{
OrderField: "CREATED_AT",
}),
}
}

Lee más sobre anotaciones y su uso en plantillas en la documentación de plantillas.

Convención de nombres

Por convención, los nombres de campos deben usar snake_case. Los campos de estructura correspondientes generados por ent seguirán la convención de Go de usar PascalCase. Cuando se desee usar PascalCase, puedes lograrlo mediante los métodos StorageKey o StructTag.