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 serint8
,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 usarfloat32
. -
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 dadoi
. -
Max(i)
: Valida si el valor del campo es menor que un valor máximo dadoi
.
-
- 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.