1. Introdução ao ent

Ent é um framework de entidades desenvolvido pelo Facebook especificamente para a linguagem Go. Ele simplifica o processo de construção e manutenção de aplicativos de modelo de dados em grande escala. O framework ent segue principalmente os seguintes princípios:

  • Modelar facilmente o esquema do banco de dados como uma estrutura de gráfico.
  • Definir o esquema na forma de código da linguagem Go.
  • Implementar tipos estáticos com base na geração de código.
  • Escrever consultas de banco de dados e travessia de gráficos é muito simples.
  • Fácil de estender e personalizar usando modelos Go.

2. Configuração do Ambiente

Para começar a usar o framework ent, verifique se a linguagem Go está instalada no seu ambiente de desenvolvimento.

Se o diretório do seu projeto estiver fora do GOPATH, ou se você não estiver familiarizado com o GOPATH, você pode usar o seguinte comando para criar um novo projeto de módulo Go:

go mod init entdemo

Isso inicializará um novo módulo Go e criará um novo arquivo go.mod para o seu projeto entdemo.

3. Definindo o Primeiro Esquema

3.1. Criando Esquema Usando a ent CLI

Primeiro, você precisa executar o seguinte comando no diretório raiz do seu projeto para criar um esquema chamado User usando a ferramenta ent CLI:

go run -mod=mod entgo.io/ent/cmd/ent new User

O comando acima irá gerar o esquema User no diretório entdemo/ent/schema/:

Arquivo entdemo/ent/schema/user.go:

package schema

import "entgo.io/ent"

// User mantém a definição de esquema para a entidade User.
type User struct {
    ent.Schema
}

// Campos do User.
func (User) Fields() []ent.Field {
    return nil
}

// Arestas do User.
func (User) Edges() []ent.Edge {
    return nil
}

3.2. Adicionando Campos

Em seguida, precisamos adicionar definições de campo ao Esquema do Usuário. Abaixo está um exemplo de adição de dois campos à entidade User.

Arquivo modificado entdemo/ent/schema/user.go:

package schema

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

// Campos do User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

Este trecho de código define dois campos para o modelo User: age e name, onde age é um número inteiro positivo e name é uma string com um valor padrão de "unknown".

3.3. Gerando Entidades de Banco de Dados

Após definir o esquema, você precisa executar o comando go generate para gerar a lógica de acesso ao banco de dados subjacente.

Execute o seguinte comando no diretório raiz do seu projeto:

go generate ./ent

Este comando gerará o código Go correspondente com base no esquema previamente definido, resultando na seguinte estrutura de arquivos:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (vários arquivos omitidos por brevidade)
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

4.1. Inicializando a Conexão com o Banco de Dados

Para estabelecer uma conexão com o banco de dados MySQL, podemos usar a função Open fornecida pelo framework ent. Primeiro, importe o driver do MySQL e forneça a string de conexão correta para inicializar a conexão com o banco de dados.

package main

import (
    "context"
    "log"

    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // Importe o driver do MySQL
)

func main() {
    // Use ent.Open para estabelecer uma conexão com o banco de dados MySQL.
    // Lembre-se de substituir os espaços reservados "seu_nome_de_usuário", "sua_senha" e "seu_banco_de_dados" abaixo.
    cliente, err := ent.Open("mysql", "seu_nome_de_usuário:sua_senha@tcp(localhost:3306)/seu_banco_de_dados?parseTime=True")
    if err != nil {
        log.Fatalf("falha ao abrir conexão com o mysql: %v", err)
    }
    defer cliente.Close()

    // Execute a ferramenta de migração automática
    ctx := context.Background()
    if err := cliente.Schema.Create(ctx); err != nil {
        log.Fatalf("falha ao criar recursos de esquema: %v", err)
    }
    
    // Lógica adicional de negócios pode ser escrita aqui
}

4.2. Criando Entidades

Criar uma entidade de Usuário envolve a construção de um novo objeto de entidade e persistindo-o no banco de dados usando o método Save ou SaveX. O código a seguir demonstra como criar uma nova entidade de Usuário e inicializar dois campos age e name.

// A função CreateUser é usada para criar uma nova entidade de Usuário
func CreateUser(ctx context.Context, cliente *ent.Client) (*ent.User, error) {
    // Use client.User.Create() para construir a solicitação de criação de um Usuário,
    // em seguida, encadear os métodos SetAge e SetName para definir os valores dos campos da entidade.
    u, err := cliente.User.
        Create().
        SetAge(30).    // Definir idade do usuário
        SetName("a8m"). // Definir nome do usuário
        Save(ctx)     // Chame Save para salvar a entidade no banco de dados
    if err != nil {
        return nil, fmt.Errorf("falha ao criar usuário: %w", err)
    }
    log.Println("usuário foi criado: ", u)
    return u, nil
}

Na função main, você pode chamar a função CreateUser para criar uma nova entidade de usuário.

func main() {
    // ...Código omitido de estabelecimento de conexão com o banco de dados

    // Criar uma entidade de usuário
    u, err := CreateUser(ctx, cliente)
    if err != nil {
        log.Fatalf("falha ao criar usuário: %v", err)
    }
    log.Printf("usuário criado: %#v\n", u)
}

4.3. Consultando Entidades

Para consultar entidades, podemos usar o construtor de consulta gerado pelo ent. O código a seguir demonstra como consultar um usuário chamado "a8m":

// A função QueryUser é usada para consultar a entidade de usuário com um nome especificado
func QueryUser(ctx context.Context, cliente *ent.Client) (*ent.User, error) {
    // Use client.User.Query() para construir a consulta de Usuário,
    // em seguida, encadear o método Where para adicionar condições de consulta, como consultar por nome de usuário
    u, err := cliente.User.
        Query().
        Where(user.NameEQ("a8m")).      // Adicionar condição de consulta, neste caso, o nome é "a8m"
        Only(ctx)                      // O método Only indica que apenas um resultado é esperado
    if err != nil {
        return nil, fmt.Errorf("falha ao consultar usuário: %w", err)
    }
    log.Println("usuário retornado: ", u)
    return u, nil
}

Na função main, você pode chamar a função QueryUser para consultar a entidade de usuário.

func main() {
    // ...Código omitido de estabelecimento de conexão com o banco de dados e criação de usuário

    // Consultar a entidade de usuário
    u, err := QueryUser(ctx, cliente)
    if err != nil {
        log.Fatalf("falha ao consultar usuário: %v", err)
    }
    log.Printf("usuário consultado: %#v\n", u)
}

5.1. Compreensão de Pontas e Pontas Inversas

No framework ent, o modelo de dados é visualizado como uma estrutura de grafo, onde as entidades representam nós no grafo, e os relacionamentos entre entidades são representados por pontas. Uma ponta é uma conexão de uma entidade para outra, por exemplo, um Usuário pode possuir vários Carros.

As Pontas Inversas são referências reversas às pontas, representando logicamente o relacionamento inverso entre entidades, mas sem criar um novo relacionamento no banco de dados. Por exemplo, através da ponta inversa de um Carro, podemos encontrar o Usuário que possui este carro.

A principal importância das pontas e pontas inversas está em tornar a navegação entre entidades associadas muito intuitiva e direta.

Dica: No ent, as pontas correspondem às chaves estrangeiras do banco de dados tradicional e são usadas para definir relacionamentos entre tabelas.

5.2. Definindo Pontas no Esquema

Primeiramente, usaremos o CLI ent para criar o esquema inicial para Carro e Grupo:

go run -mod=mod entgo.io/ent/cmd/ent new Car Group

Em seguida, no esquema do Usuário, definimos a ponta com Carro para representar o relacionamento entre usuários e carros. Podemos adicionar uma ponta carros apontando para o tipo Carro na entidade do usuário, indicando que um usuário pode ter vários carros:

// entdemo/ent/schema/user.go

// Pontas do Usuário.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("carros", Car.Type),
    }
}

Após definir as pontas, precisamos executar go generate ./ent novamente para gerar o código correspondente.

5.3. Operando em Dados de Ponta

Criar carros associados a um usuário é um processo simples. Dada uma entidade de usuário, podemos criar uma nova entidade de carro e associá-la ao usuário:

import (
    "context"
    "log"
    "entdemo/ent"
    // Certifique-se de importar a definição do esquema para Carro
    _ "entdemo/ent/schema"
)

func CreateCarsForUser(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("falha ao obter o usuário: %v", err)
        return err
    }

    // Criar um novo carro e associá-lo ao usuário
    _, err = client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("falha ao criar carro para o usuário: %v", err)
        return err
    }

    log.Println("carro foi criado e associado ao usuário")
    return nil
}

Da mesma forma, consultar os carros de um usuário é direto. Se quisermos recuperar uma lista de todos os carros pertencentes a um usuário, podemos fazer o seguinte:

func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("falha ao obter o usuário: %v", err)
        return err
    }

    // Consultar todos os carros pertencentes ao usuário
    carros, err := user.QueryCarros().All(ctx)
    if err != nil {
        log.Fatalf("falha ao consultar carros: %v", err)
        return err
    }

    for _, carro := range carros {
        log.Printf("carro: %v, modelo: %v", carro.ID, carro.Model)
    }
    return nil
}

Através dos passos acima, não apenas aprendemos como definir pontas no esquema, mas também demonstramos como criar e consultar dados relacionados a pontas.

6. Travessia e Consulta de Grafo

6.1. Compreensão das Estruturas de Grafo

No ent, as estruturas de grafo são representadas por entidades e as pontas entre elas. Cada entidade é equivalente a um nó no grafo, e os relacionamentos entre entidades são representados por pontas, que podem ser de um para um, de um para muitos, muitos para muitos, etc. Essa estrutura de grafo torna as consultas e operações complexas em um banco de dados relacional simples e intuitivas.

6.2. Percorrendo Estruturas de Gráfico

Escrever código de Traversal de Gráfico envolve principalmente consultar e associar dados através das arestas entre entidades. Abaixo está um exemplo simples demonstrando como percorrer a estrutura de gráfico em ent:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal é um exemplo de percorrer a estrutura de gráfico
func GraphTraversal(ctx context.Context, client *ent.Client) error {
    // Consulta o usuário chamado "Ariel"
    a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("Falha na consulta do usuário: %v", err)
        return err
    }

    // Percorre todos os carros pertencentes a Ariel
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("Falha na consulta dos carros: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("Ariel tem um carro com o modelo: %s", car.Model)
    }

    // Percorre todos os grupos dos quais Ariel é membro
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("Falha na consulta dos grupos: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("Ariel é membro do grupo: %s", g.Name)
    }

    return nil
}

O código acima é um exemplo básico de percorrer um gráfico, que primeiro consulta um usuário e depois percorre os carros e grupos do usuário.

7. Visualizando o Esquema do Banco de Dados

7.1. Instalando a Ferramenta Atlas

Para visualizar o esquema do banco de dados gerado pelo ent, podemos usar a ferramenta Atlas. Os passos de instalação do Atlas são muito simples. Por exemplo, no macOS, você pode instalá-lo usando brew:

brew install ariga/tap/atlas

Nota: Atlas é uma ferramenta de migração de banco de dados universal que pode lidar com a gestão da versão das estruturas de tabela para vários bancos de dados. Uma introdução detalhada ao Atlas será fornecida em capítulos posteriores.

7.2. Gerando ERD e Esquema SQL

Usar o Atlas para visualizar e exportar esquemas é muito direto. Após instalar o Atlas, você pode usar o seguinte comando para visualizar o Diagrama de Entidade-Relacionamento (ERD):

atlas schema inspect -d [database_dsn] --format dot

Ou gerar diretamente o Esquema SQL:

atlas schema inspect -d [database_dsn] --format sql

Onde [database_dsn] aponta para o nome da fonte de dados (DSN) do seu banco de dados. Por exemplo, para um banco de dados SQLite, poderia ser:

atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot

A saída gerada por esses comandos pode ser posteriormente transformada em visualizações ou documentos usando as ferramentas respectivas.

8. Migração de Esquema

8.1. Migração Automática e Migração Versionada

ent suporta duas estratégias de migração de esquema: migração automática e migração versionada. A migração automática é o processo de inspecionar e aplicar alterações no esquema em tempo de execução, adequado para desenvolvimento e testes. A migração versionada envolve a geração de scripts de migração e requer uma revisão cuidadosa e testes antes da implantação em produção.

Dica: Para migração automática, consulte o conteúdo na seção 4.1.

8.2. Realizando Migração Versionada

O processo de migração versionada envolve a geração de arquivos de migração através do Atlas. Abaixo estão os comandos relevantes:

Para gerar arquivos de migração:

atlas migrate diff -d ent/schema/path --dir migrations/dir

Posteriormente, esses arquivos de migração podem ser aplicados ao banco de dados:

atlas migrate apply -d migrations/dir --url database_dsn

Seguindo este processo, você pode manter um histórico de migrações de banco de dados no sistema de controle de versão e garantir uma revisão cuidadosa antes de cada migração.

Dica: Consulte o código de exemplo em https://github.com/ent/ent/tree/master/examples/start