1. Conceptos básicos de modelos y campos

1.1. Introducción a la Definición de Modelo

En un marco de ORM, un modelo se utiliza para describir la relación de asignación entre tipos de entidad en la aplicación y tablas de base de datos. El modelo define las propiedades y relaciones de la entidad, así como las configuraciones específicas de la base de datos asociadas con ellas. En el marco de ent, los modelos se utilizan típicamente para describir tipos de entidad en un gráfico, como Usuario o Grupo.

Las definiciones de modelos suelen incluir descripciones de los campos (o propiedades) y aristas (o relaciones) de la entidad, así como algunas opciones específicas de la base de datos. Estas descripciones pueden ayudarnos a definir la estructura, propiedades y relaciones de la entidad, y se pueden utilizar para generar la estructura de tabla de base de datos correspondiente basada en el modelo.

1.2. Visión general de campos

Los campos son la parte del modelo que representa las propiedades de la entidad. Ellos definen las propiedades de la entidad, como nombre, edad, fecha, etc. En el marco de ent, los tipos de campo incluyen varios tipos de datos básicos, como entero, cadena, booleano, tiempo, etc., así como algunos tipos específicos de SQL, como UUID, []byte, JSON, etc.

La tabla a continuación muestra los tipos de campo admitidos por el marco de ent:

Tipo Descripción
int Tipo entero
uint8 Tipo entero sin signo de 8 bits
float64 Tipo de punto flotante
bool Tipo booleano
string Tipo cadena
time.Time Tipo tiempo
UUID Tipo UUID
[]byte Tipo arreglo de bytes (solo SQL)
JSON Tipo JSON (solo SQL)
Enum Tipo de enumeración (solo SQL)
Otros Otros tipos (por ejemplo, Rango de Postgres)

2. Detalles de las propiedades de campo

2.1. Tipos de datos

El tipo de datos de un atributo o campo en un modelo de entidad determina la forma de los datos que se pueden almacenar. Este es una parte crucial de la definición del modelo en el marco de ent. A continuación se muestran algunos tipos de datos comúnmente utilizados en el marco ent.

import (
    "time"

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

// Esquema de Usuario.
type Usuario struct {
    ent.Schema
}

// Campos de Usuario.
func (Usuario) Fields() []ent.Field {
    return []ent.Field{
        field.Int("edad"),             // Tipo entero
        field.String("nombre"),         // Tipo cadena
        field.Bool("activo"),           // Tipo booleano
        field.Float("puntaje"),         // Tipo de punto flotante
        field.Time("creado_en"),        // Tipo de marca de tiempo
    }
}
  • int: Representa valores enteros, que pueden ser int8, int16, int32, int64, etc.
  • string: Representa datos de tipo cadena.
  • bool: Representa valores booleanos, típicamente utilizados como banderas.
  • float64: Representa números de punto flotante, también se puede usar float32.
  • time.Time: Representa tiempo, típicamente utilizado para marcas de tiempo o datos de fecha.

Estos tipos de campo se asignarán a los tipos correspondientes admitidos por la base de datos subyacente. Además, ent admite tipos más complejos como UUID, JSON, enumeraciones (Enum), y admite tipos de base de datos especiales como []byte (solo SQL) y Otros (solo SQL).

2.2. Valores predeterminados

Los campos se pueden configurar con valores predeterminados. Si el valor correspondiente no se especifica al crear una entidad, se utilizará el valor predeterminado. El valor predeterminado puede ser un valor fijo o un valor generado dinámicamente a partir de una función. Utilice el método .Default para establecer un valor predeterminado estático, o use .DefaultFunc para establecer un valor predeterminado generado dinámicamente.

// Esquema de Usuario.
func (Usuario) Fields() []ent.Field {
    return []ent.Field{
        field.Time("creado_en").
            Default(time.Now),  // Un valor predeterminado fijo de time.Now
        field.String("rol").
            Default("usuario"),   // Un valor de cadena constante
        field.Float("puntaje").
            DefaultFunc(func() float64 {
                return 10.0  // Un valor predeterminado generado por una función
            }),
    }
}

2.3. Opción de Campo y Valores Nulos

Por defecto, los campos son obligatorios. Para declarar un campo opcional, utiliza el método .Optional(). Los campos opcionales se declararán como campos anulables en la base de datos. La opción Nillable permite que los campos se establezcan explícitamente como nil, distinguiendo entre el valor cero de un campo y un estado no establecido.

// Esquema de Usuario.
func (Usuario) Fields() []ent.Field {
    return []ent.Field{
        field.String("apodo").Optional(), // El campo opcional no es requerido
        field.Int("edad").Optional().Nillable(), // El campo anulable puede ser establecido como nil
    }
}

Al usar el modelo definido anteriormente, el campo edad puede aceptar tanto valores nil para indicar un estado no establecido, como valores cero no nil.

2.4. Unicidad de Campo

Los campos únicos garantizan que no haya valores duplicados en la tabla de la base de datos. Utiliza el método Unique() para definir un campo único. Cuando se establece la integridad de los datos como un requisito crítico, como en el caso de correos electrónicos de usuario o nombres de usuario, se deben utilizar campos únicos.

// Esquema de Usuario.
func (Usuario) Fields() []ent.Field {
    return []ent.Field{
        field.String("email").Unique(),  // Campo único para evitar direcciones de correo electrónico duplicadas
    }
}

Esto creará una restricción única en la base de datos subyacente para evitar la inserción de valores duplicados.

2.5. Indexación de Campos

La indexación de campos se utiliza para mejorar el rendimiento de las consultas a la base de datos, especialmente en bases de datos grandes. En el marco ent, se puede utilizar el método .Indexes() para crear índices.

import "entgo.io/ent/schema/index"

// Esquema de Usuario.
func (Usuario) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email"),  // Crear un índice en el campo 'email'
        index.Fields("nombre", "edad").Unique(), // Crear un índice compuesto único
    }
}

Los índices se pueden utilizar para campos consultados con frecuencia, pero es importante tener en cuenta que demasiados índices pueden provocar una disminución en el rendimiento de las operaciones de escritura. Por lo tanto, la decisión de crear índices debe equilibrarse en función de las circunstancias reales.

2.6. Etiquetas Personalizadas

En el marco ent, puedes utilizar el método StructTag para agregar etiquetas personalizadas a los campos de la estructura de entidad generada. Estas etiquetas son muy útiles para implementar operaciones como la codificación JSON y la codificación XML. En el ejemplo a continuación, añadiremos etiquetas personalizadas JSON y XML para el campo nombre.

// Campos de Usuario.
func (Usuario) Fields() []ent.Field {
    return []ent.Field{
        field.String("nombre").
            // Agregar etiquetas personalizadas usando el método StructTag
            // Aquí, establece la etiqueta JSON para el campo nombre como 'usuario' y omítela cuando el campo esté vacío (omitempty)
            // Además, establece la etiqueta XML para la codificación como 'nombre'
            StructTag(`json:"usuario,omitempty" xml:"nombre"`),
    }
}

Al codificar con JSON o XML, la opción omitempty indica que si el campo nombre está vacío, entonces este campo se omitirá del resultado de la codificación. Esto es muy útil para reducir el tamaño del cuerpo de respuesta al escribir APIs.

Esto también demuestra cómo establecer múltiples etiquetas para el mismo campo simultáneamente. Las etiquetas JSON utilizan la clave json, las etiquetas XML utilizan la clave xml y se separan por espacios. Estas etiquetas serán utilizadas por funciones de librerías como encoding/json y encoding/xml al analizar la estructura para codificar o decodificar.

3. Validación de Campo y Restricciones

La validación de campo es un aspecto importante del diseño de bases de datos para garantizar la consistencia y validez de los datos. En esta sección, exploraremos el uso de validadores integrados, validadores personalizados y diversas restricciones para mejorar la integridad y calidad de los datos en el modelo de entidad.

3.1. Validadores integrados

El marco proporciona una serie de validadores integrados para realizar comprobaciones comunes de validez de datos en diferentes tipos de campos. El uso de estos validadores integrados puede simplificar el proceso de desarrollo y definir rápidamente rangos o formatos de datos válidos para los campos.

Aquí tienes algunos ejemplos de validadores integrados para campos:

  • Validadores para tipos numéricos:
    • Positive(): Valida si el valor del campo es un número positivo.
    • Negative(): Valida si el valor del campo es un número negativo.
    • NonNegative(): Valida si el valor del campo es un número no negativo.
    • Min(i): Valida si el valor del campo es mayor que un valor mínimo dado i.
    • Max(i): Valida si el valor del campo es menor que un valor máximo dado i.
  • Validadores para el tipo string:
    • MinLen(i): Valida la longitud mínima de una cadena.
    • MaxLen(i): Valida la longitud máxima de una cadena.
    • Match(regexp.Regexp): Valida si la cadena coincide con la expresión regular dada.
    • NotEmpty: Valida si la cadena no está vacía.

Echemos un vistazo a un ejemplo práctico de código. En este ejemplo, se crea un modelo Usuario, que incluye un campo de tipo entero no negativo edad y un campo email con un formato fijo:

func (Usuario) Campos() []ent.Campo {
    return []ent.Campo{
        campo.Int("edad").
            Positive(),
        campo.String("email").
            Match(regexp.MustCompile(`^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`)),
    }
}

3.2. Validadores personalizados

Si bien los validadores integrados pueden manejar muchos requisitos de validación comunes, a veces puede que necesites lógica de validación más compleja. En esos casos, puedes escribir validadores personalizados para cumplir con reglas comerciales específicas.

Un validador personalizado es una función que recibe un valor de campo y devuelve un error. Si el error devuelto no está vacío, indica un fallo de validación. El formato general de un validador personalizado es el siguiente:

func (Usuario) Campos() []ent.Campo {
    return []ent.Campo{
        campo.String("teléfono").
            Validar(func(s string) error {
                // Verificar si el número de teléfono cumple con el formato esperado
                coincidido, _ := regexp.MatchString(`^\+?[1-9]\d{1,14}$`, s)
                if !coincidido {
                    return errors.New("Formato de número de teléfono incorrecto")
                }
                return nil
            }),
    }
}

Como se muestra arriba, hemos creado un validador personalizado para validar el formato de un número de teléfono.

3.3. Restricciones

Las restricciones son reglas que imponen reglas específicas en un objeto de base de datos. Pueden usarse para garantizar la corrección y consistencia de los datos, como prevenir la entrada de datos no válidos o definir las relaciones de datos.

Las restricciones comunes de la base de datos incluyen:

  • Restricción de clave primaria: Garantiza que cada registro en la tabla sea único.
  • Restricción única: Asegura que el valor de una columna o una combinación de columnas sea único en la tabla.
  • Restricción de clave externa: Define las relaciones entre tablas, garantizando la integridad referencial.
  • Restricción de verificación: Asegura que el valor de un campo cumple una condición específica.

En el modelo de entidad, puedes definir restricciones para mantener la integridad de los datos de la siguiente manera:

func (Usuario) Campos() []ent.Campo {
    return []ent.Campo{
        campo.String("nombre_de_usuario").
            Único(), // Restricción única para asegurar que el nombre de usuario sea único en la tabla.
        campo.String("email").
            Único(), // Restricción única para asegurar que el email sea único en la tabla.
    }
}

func (Usuario) Bordes() []ent.Borde {
    return []ent.Borde{
        borde.Hacia("amigos", Usuario.Tipo).
            Único(), // Restricción de clave externa, creando una relación de borde única con otro usuario.
    }
}

En resumen, la validación de campos y las restricciones son cruciales para garantizar una buena calidad de datos y evitar errores de datos inesperados. Utilizar las herramientas proporcionadas por el marco ent puede hacer que este proceso sea más sencillo y confiable.