1. Conceito de Modelo e Campo

1.1. Introdução à Definição de Modelo

Em um framework ORM, um modelo é usado para descrever a relação de mapeamento entre os tipos de entidades na aplicação e as tabelas do banco de dados. O modelo define as propriedades e relacionamentos da entidade, bem como as configurações específicas do banco de dados associadas a elas. No framework ent, os modelos são tipicamente usados para descrever tipos de entidades em um grafo, como User ou Group.

As definições de modelos geralmente incluem descrições dos campos (ou propriedades) e arestas (ou relacionamentos) da entidade, bem como algumas opções específicas do banco de dados. Essas descrições podem nos ajudar a definir a estrutura, propriedades e relacionamentos da entidade, e podem ser usadas para gerar a estrutura da tabela do banco de dados correspondente com base no modelo.

1.2. Visão Geral do Campo

Os campos são a parte do modelo que representa as propriedades da entidade. Eles definem as propriedades da entidade, como nome, idade, data, etc. No framework ent, os tipos de campos incluem vários tipos de dados básicos, como inteiro, string, booleano, tempo, etc., além de alguns tipos específicos do SQL, como UUID, []byte, JSON, etc.

A tabela abaixo mostra os tipos de campos suportados pelo framework ent:

Tipo Descrição
int Tipo inteiro
uint8 Tipo inteiro sem sinal de 8 bits
float64 Tipo ponto flutuante
bool Tipo booleano
string Tipo string
time.Time Tipo tempo
UUID Tipo UUID
[]byte Tipo array de bytes (apenas SQL)
JSON Tipo JSON (apenas SQL)
Enum Tipo Enum (apenas SQL)
Outros Outros tipos (por exemplo, Intervalo Postgres)

2. Detalhes das Propriedades do Campo

2.1. Tipos de Dados

O tipo de dados de um atributo ou campo em um modelo de entidade determina a forma dos dados que podem ser armazenados. Esta é uma parte crucial da definição do modelo no framework ent. Abaixo estão alguns tipos de dados comumente usados no framework ent.

import (
    "time"

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

// Esquema do usuário.
type User struct {
    ent.Schema
}

// Campos do usuário.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age"),             // Tipo inteiro
        field.String("name"),         // Tipo string
        field.Bool("active"),         // Tipo booleano
        field.Float("score"),         // Tipo ponto flutuante
        field.Time("created_at"),     // Tipo timestamp
    }
}
  • int: Representa valores inteiros, que podem ser int8, int16, int32, int64, etc.
  • string: Representa dados de string.
  • bool: Representa valores booleanos, normalmente usados como flags.
  • float64: Representa números de ponto flutuante, também pode usar float32.
  • time.Time: Representa tempo, normalmente usado para carimbos de data ou dados de data.

Esses tipos de campos serão mapeados para os tipos correspondentes suportados pelo banco de dados subjacente. Além disso, o ent suporta tipos mais complexos como UUID, JSON, enumerações (Enum), e suporte para tipos de banco de dados especiais como []byte (apenas SQL) e Outros (apenas SQL).

2.2. Valores Padrão

Os campos podem ser configurados com valores padrão. Se o valor correspondente não for especificado ao criar uma entidade, o valor padrão será usado. O valor padrão pode ser um valor fixo ou um valor gerado dinamicamente por uma função. Use o método .Default para definir um valor padrão estático, ou use .DefaultFunc para definir um valor padrão gerado dinamicamente.

// Esquema do usuário.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Time("created_at").
            Default(time.Now),  // Um valor padrão fixo de time.Now
        field.String("role").
            Default("user"),   // Um valor de string constante
        field.Float("score").
            DefaultFunc(func() float64 {
                return 10.0  // Um valor padrão gerado por uma função
            }),
    }
}

2.3. Opcionalidade de Campo e Valores Nulos

Por padrão, os campos são obrigatórios. Para declarar um campo opcional, utilize o método .Optional(). Campos opcionais serão declarados como campos nulos no banco de dados. A opção Nillable permite que os campos sejam explicitamente definidos como nil, distinguindo entre o valor zero de um campo e um estado não definido.

// Esquema do usuário.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("apelido").Optional(), // Campo opcional não é obrigatório
        field.Int("idade").Optional().Nillable(), // Campo nulo pode ser definido como nil
    }
}

Ao usar o modelo definido acima, o campo idade pode aceitar tanto valores nil para indicar não definido, quanto valores zero não nulos.

2.4. Unicidade de Campo

Campos únicos garantem que não haja valores duplicados na tabela do banco de dados. Use o método Unique() para definir um campo único. Ao estabelecer a integridade dos dados como um requisito crítico, como para e-mails ou nomes de usuário de usuários, campos únicos devem ser utilizados.

// Esquema do usuário.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("email").Unique(),  // Campo único para evitar endereços de e-mail duplicados
    }
}

Isso criará uma restrição única no banco de dados subjacente para evitar a inserção de valores duplicados.

2.5. Indexação de Campo

A indexação de campo é usada para melhorar o desempenho de consultas ao banco de dados, especialmente em bancos de dados grandes. No framework ent, o método .Indexes() pode ser utilizado para criar índices.

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

// Esquema do usuário.
func (User) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email"),  // Criar um índice no campo 'email'
        index.Fields("nome", "idade").Unique(), // Criar um índice composto único
    }
}

Os índices podem ser utilizados para campos frequentemente consultados, mas é importante observar que a criação de muitos índices pode levar a uma diminuição no desempenho das operações de escrita. Portanto, a decisão de criar índices deve ser equilibrada com base nas circunstâncias reais.

2.6. Tags Personalizadas

No framework ent, você pode usar o método StructTag para adicionar tags personalizadas aos campos da estrutura da entidade gerada. Essas tags são muito úteis para implementar operações como codificação JSON e codificação XML. No exemplo abaixo, adicionaremos tags personalizadas em JSON e XML para o campo nome.

// Campos do usuário.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nome").
            // Adicionar tags personalizadas usando o método StructTag
            // Aqui, defina a tag JSON para o campo nome como 'username' e ignore-o quando o campo estiver vazio (omitempty)
            // Além disso, defina a tag XML para codificação como 'name'
            StructTag(`json:"username,omitempty" xml:"name"`),
    }
}

Ao codificar com JSON ou XML, a opção omitempty indica que se o campo nome estiver vazio, então este campo será omitido do resultado da codificação. Isso é muito útil para reduzir o tamanho do corpo da resposta ao escrever APIs.

Isso também demonstra como definir várias tags para o mesmo campo simultaneamente. As tags JSON usam a chave json, as tags XML usam a chave xml e são separadas por espaços. Essas tags serão usadas por funções de biblioteca como encoding/json e encoding/xml ao analisar a estrutura para codificar ou decodificar.

3. Validação de Campo e Restrições

A validação de campo é um aspecto importante do design do banco de dados para garantir a consistência e validade dos dados. Nesta seção, iremos explorar o uso de validadores integrados, validadores personalizados e várias restrições para melhorar a integridade e qualidade dos dados no modelo de entidade.

3.1. Validadores Integrados

O framework fornece uma série de validadores integrados para realizar verificações comuns de validade de dados em diferentes tipos de campos. Utilizar esses validadores integrados pode simplificar o processo de desenvolvimento e definir rapidamente intervalos ou formatos válidos de dados para os campos.

Aqui estão alguns exemplos de validadores de campo integrados:

  • Validadores para tipos numéricos:
    • Positive(): Valida se o valor do campo é um número positivo.
    • Negative(): Valida se o valor do campo é um número negativo.
    • NonNegative(): Valida se o valor do campo é um número não negativo.
    • Min(i): Valida se o valor do campo é maior que um valor mínimo i fornecido.
    • Max(i): Valida se o valor do campo é menor que um valor máximo i fornecido.
  • Validadores para o tipo string:
    • MinLen(i): Valida o comprimento mínimo de uma string.
    • MaxLen(i): Valida o comprimento máximo de uma string.
    • Match(regexp.Regexp): Valida se a string corresponde à expressão regular fornecida.
    • NotEmpty: Valida se a string não está vazia.

Vamos dar uma olhada em um exemplo prático de código. Neste exemplo, um modelo User é criado, que inclui um campo de tipo inteiro não negativo age e um campo email com um formato fixo:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("email").
            Match(regexp.MustCompile(`^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`)),
    }
}

3.2. Validadores Personalizados

Enquanto os validadores integrados podem lidar com muitos requisitos comuns de validação, às vezes você pode precisar de lógica de validação mais complexa. Nesses casos, você pode escrever validadores personalizados para atender a regras de negócios específicas.

Um validador personalizado é uma função que recebe um valor de campo e retorna um error. Se o error retornado não estiver vazio, indica falha na validação. O formato geral de um validador personalizado é o seguinte:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("phone").
            Validate(func(s string) error {
                // Verifica se o número de telefone atende ao formato esperado
                correspondido, _ := regexp.MatchString(`^\+?[1-9]\d{1,14}$`, s)
                if !correspondido {
                    return errors.New("Formato de número de telefone incorreto")
                }
                return nil
            }),
    }
}

Como mostrado acima, criamos um validador personalizado para validar o formato de um número de telefone.

3.3. Restrições

Restrições são regras que impõem regras específicas em um objeto de banco de dados. Elas podem ser usadas para garantir a correção e consistência dos dados, como impedir a entrada de dados inválidos ou definir os relacionamentos dos dados.

As restrições de banco de dados comuns incluem:

  • Restrição de chave primária: Garante que cada registro na tabela seja único.
  • Restrição única: Garante que o valor de uma coluna ou uma combinação de colunas seja única na tabela.
  • Restrição de chave estrangeira: Define os relacionamentos entre tabelas, garantindo integridade referencial.
  • Restrição de verificação: Garante que o valor de um campo atenda a uma condição específica.

No modelo de entidade, você pode definir restrições para manter a integridade dos dados da seguinte forma:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("username").
            Unique(), // Restrição única para garantir que o nome de usuário seja único na tabela.
        field.String("email").
            Unique(), // Restrição única para garantir que o e-mail seja único na tabela.
    }
}

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("friends", User.Type).
            Unique(), // Restrição de chave estrangeira, criando um relacionamento de borda único com outro usuário.
    }
}

Em resumo, a validação de campo e as restrições são cruciais para garantir a boa qualidade dos dados e evitar erros de dados inesperados. Utilizar as ferramentas fornecidas pelo framework ent pode tornar esse processo mais simples e confiável.