1. Conceitos Básicos de Entidade e Associação
No framework ent
, uma entidade refere-se à unidade básica de dados gerenciada no banco de dados, que geralmente corresponde a uma tabela no banco de dados. Os campos na entidade correspondem às colunas na tabela, enquanto as associações (arestas) entre entidades são usadas para descrever os relacionamentos e dependências entre entidades. As associações de entidade formam a base para a construção de modelos de dados complexos, permitindo a representação de relacionamentos hierárquicos, como relacionamentos pai-filho e relacionamentos de propriedade.
O framework ent
fornece um conjunto rico de APIs, permitindo que os desenvolvedores definam e gerenciem essas associações no esquema da entidade. Através dessas associações, podemos facilmente expressar e operar na lógica de negócios complexa entre os dados.
2. Tipos de Associações de Entidade em ent
2.1 Associação Um para Um (O2O)
Uma associação um para um refere-se a uma correspondência de um para um entre duas entidades. Por exemplo, no caso de usuários e contas bancárias, cada usuário pode ter apenas uma conta bancária, e cada conta bancária também pertence apenas a um usuário. O framework ent
usa os métodos edge.To
e edge.From
para definir tais associações.
Primeiro, podemos definir uma associação um para um apontando para Card
dentro do esquema User
:
// Arestas do Usuário.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("card", Card.Type). // Aponta para a entidade Card, definindo o nome da associação como "card"
Unique(), // O método Unique garante que esta seja uma associação um para um
}
}
Em seguida, definimos a associação inversa de volta para User
dentro do esquema Card
:
// Arestas do Cartão.
func (Card) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type). // Aponta de volta para User a partir de Card, definindo o nome da associação como "owner"
Ref("card"). // O método Ref especifica o nome da associação inversa correspondente
Unique(), // Marcado como único para garantir que um cartão corresponda a um único proprietário
}
}
2.2 Associação Um para Muitos (O2M)
Uma associação um para muitos indica que uma entidade pode estar associada a várias outras entidades, mas essas entidades só podem apontar de volta para uma única entidade. Por exemplo, um usuário pode ter vários animais de estimação, mas cada animal de estimação só tem um proprietário.
No ent
, ainda usamos edge.To
e edge.From
para definir esse tipo de associação. O exemplo abaixo define uma associação um para muitos entre usuários e animais de estimação:
// Arestas do Usuário.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type), // Associação um para muitos de User para Pet
}
}
Na entidade Pet
, definimos uma associação muitos para um de volta para User
:
// Arestas do Animal de Estimação.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type). // Associação muitos para um de Pet para User
Ref("pets"). // Especifica o nome da associação inversa de pet para proprietário
Unique(), // Garante que um proprietário possa ter vários animais de estimação
}
}
2.3 Associação Muitos para Muitos (M2M)
Uma associação muitos para muitos permite que dois tipos de entidades tenham múltiplas instâncias uma da outra. Por exemplo, um aluno pode se matricular em vários cursos, e um curso também pode ter vários alunos matriculados. ent
fornece uma API para estabelecer associações de muitos para muitos:
Na entidade Student
, usamos edge.To
para estabelecer uma associação de muitos para muitos com Course
:
// Arestas do Aluno.
func (Student) Edges() []ent.Edge {
return []ent.Edge{
edge.To("courses", Course.Type), // Define uma associação de muitos para muitos de Student para Course
}
}
Da mesma forma, na entidade Course
, estabelecemos uma associação inversa para Student
na relação de muitos para muitos:
// Arestas do Curso.
func (Course) Edges() []ent.Edge {
return []ent.Edge{
edge.From("students", Student.Type). // Define uma associação de muitos para muitos de Course para Student
Ref("courses"), // Especifica o nome da associação inversa de Course para Student
}
}
Estes tipos de associações são a pedra angular da construção de modelos de dados de aplicativos complexos, e entender como definir e usá-los em ent
é crucial para estender modelos de dados e lógica de negócios.
3. Operações Básicas para Associações de Entidades
Esta seção irá demonstrar como realizar operações básicas usando ent
com as relações definidas, incluindo a criação, consulta e travessia de entidades associadas.
3.1 Criação de Entidades Associadas
Ao criar entidades, é possível definir simultaneamente as relações entre as entidades. Para relacionamentos de um-para-muitos (O2M) e muitos-para-muitos (M2M), é possível utilizar o método Add{Edge}
para adicionar entidades associadas.
Por exemplo, se tivermos uma entidade de usuário e uma entidade de animal de estimação com uma certa associação, onde um usuário pode ter vários animais de estimação, o exemplo a seguir demonstra a criação de um novo usuário e a adição de animais de estimação para ele:
// Criar um usuário e adicionar animais de estimação
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
// Criar uma instância de animal de estimação
fido := client.Pet.
Create().
SetName("Fido").
SaveX(ctx)
// Criar uma instância de usuário e associá-la ao animal de estimação
user := client.User.
Create().
SetName("Alice").
AddPets(fido). // Usar o método AddPets para associar o animal de estimação
SaveX(ctx)
return user, nil
}
Neste exemplo, primeiro criamos uma instância de animal de estimação chamada Fido, e em seguida criamos um usuário chamado Alice e associamos a instância do animal de estimação ao usuário utilizando o método AddPets
.
3.2 Consulta de Entidades Associadas
A consulta de entidades associadas é uma operação comum no ent
. Por exemplo, é possível utilizar o método Query{Edge}
para recuperar outras entidades associadas a uma entidade específica.
Continuando com o nosso exemplo de usuários e animais de estimação, veja como consultar todos os animais de estimação de um usuário:
// Consultar todos os animais de estimação de um usuário
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
pets, err := client.User.
Get(ctx, userID). // Obter a instância do usuário com base no ID do usuário
QueryPets(). // Consultar as entidades de animais de estimação associadas ao usuário
All(ctx) // Retornar todas as entidades de animais de estimação consultadas
if err != nil {
return nil, err
}
return pets, nil
}
No trecho de código acima, primeiro obtemos a instância do usuário com base no ID do usuário e em seguida chamamos o método QueryPets
para recuperar todas as entidades de animais de estimação associadas a esse usuário.
Observação: A ferramenta de geração de código do
ent
gera automaticamente a API para consultas de associação com base nas relações de entidades definidas. Recomenda-se revisar o código gerado.
4. Carregamento Antecipado
4.1 Princípios de Pré-carregamento
O pré-carregamento é uma técnica utilizada na consulta de bancos de dados para buscar e carregar entidades associadas antecipadamente. Essa abordagem é comumente empregada para obter dados relacionados a várias entidades de uma só vez, a fim de evitar múltiplas operações de consulta ao banco de dados no processamento subsequente, melhorando significativamente o desempenho da aplicação.
No framework ent
, o pré-carregamento é principalmente utilizado para lidar com relacionamentos entre entidades, como um-para-muitos e muitos-para-muitos. Ao recuperar uma entidade do banco de dados, suas entidades associadas não são carregadas automaticamente. Em vez disso, são carregadas explicitamente conforme necessário por meio do pré-carregamento. Isso é crucial para evitar o problema de consulta N+1 (ou seja, realizar consultas separadas para cada entidade pai).
No framework ent
, o pré-carregamento é realizado utilizando o método With
no construtor de consulta. Este método gera funções With...
correspondentes para cada aresta, como WithGroups
e WithPets
. Esses métodos são gerados automaticamente pelo framework ent
, e os programadores podem utilizá-los para solicitar o pré-carregamento de associações específicas.
O princípio de funcionamento do pré-carregamento de entidades é que, ao consultar a entidade principal, o ent
executa consultas adicionais para recuperar todas as entidades associadas. Posteriormente, essas entidades são populadas no campo Edges
do objeto retornado. Isso significa que o ent
pode executar múltiplas consultas ao banco de dados, pelo menos uma vez para cada aresta associada que precisa ser pré-carregada. Embora este método possa ser menos eficiente do que uma única consulta JOIN
complexa em determinados cenários, oferece maior flexibilidade e espera-se receber otimizações de desempenho em versões futuras do ent
.
4.2 Implementação do Pré-carregamento
Agora iremos demonstrar como realizar operações de pré-carregamento no framework ent
por meio de alguns exemplos de código, utilizando os modelos de usuários e animais de estimação descritos na visão geral.
Pré-carregando uma Associação Única
Suponha que queremos recuperar todos os usuários do banco de dados e pré-carregar os dados dos animais de estimação. Podemos alcançar isso escrevendo o seguinte código:
usuarios, err := cliente.User.
Query().
WithPets().
All(ctx)
if err != nil {
// Lidar com o erro
return err
}
for _, u := range usuarios {
for _, p := range u.Edges.Pets {
fmt.Printf("O usuário (%v) possui o animal de estimação (%v)\n", u.ID, p.ID)
}
}
Neste exemplo, usamos o método WithPets
para solicitar ao ent que pré-carregue as entidades de animais de estimação associadas aos usuários. Os dados pré-carregados dos animais de estimação são populados no campo Edges.Pets
, a partir do qual podemos acessar esses dados associados.
Pré-carregando Múltiplas Associações
Ent nos permite pré-carregar múltiplas associações de uma vez e até mesmo especificar o pré-carregamento de associações aninhadas, filtragem, ordenação ou limitação do número de resultados pré-carregados. Abaixo está um exemplo de pré-carregamento dos animais de estimação dos administradores e das equipes às quais eles pertencem, enquanto também pré-carregam os usuários associados às equipes:
admins, err := cliente.User.
Query().
Where(user.Admin(true)).
WithPets().
WithGroups(func(q *ent.GroupQuery) {
q.Limit(5) // Limitar para as primeiras 5 equipes
q.Order(ent.Asc(group.FieldName)) // Ordenar em ordem ascendente pelo nome da equipe
q.WithUsers() // Pré-carregar os usuários na equipe
}).
All(ctx)
if err != nil {
// Lidar com os erros
return err
}
for _, admin := range admins {
for _, p := range admin.Edges.Pets {
fmt.Printf("O administrador (%v) possui o animal de estimação (%v)\n", admin.ID, p.ID)
}
for _, g := range admin.Edges.Groups {
fmt.Printf("O administrador (%v) pertence à equipe (%v)\n", admin.ID, g.ID)
for _, u := range g.Edges.Users {
fmt.Printf("A equipe (%v) tem o membro (%v)\n", g.ID, u.ID)
}
}
}
Através deste exemplo, pode-se perceber quão poderoso e flexível é o ent. Com apenas algumas chamadas de método simples, é possível pré-carregar dados associados detalhados e organizá-los de forma estruturada. Isso proporciona grande conveniência para desenvolver aplicações orientadas a dados.