Saltar al contenido principal

· 17 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

En esta entrada de blog, exploraremos cómo construir un sistema RAG (Retrieval Augmented Generation) utilizando Ent, Atlas y pgvector.

RAG es una técnica que potencia los modelos generativos incorporando un paso de recuperación. En lugar de depender únicamente del conocimiento interno del modelo, podemos recuperar documentos o datos relevantes de una fuente externa y usar esa información para producir respuestas más precisas y contextualizadas. Este enfoque es especialmente útil al construir aplicaciones como sistemas de preguntas y respuestas, chatbots o cualquier escenario donde se necesite conocimiento actualizado o específico de un dominio.

Configuración de nuestro esquema de Ent

Comencemos el tutorial inicializando el módulo de Go que usaremos para nuestro proyecto:

go mod init github.com/rotemtam/entrag # Feel free to replace the module path with your own

En este proyecto usaremos Ent, un framework de entidades para Go, para definir nuestro esquema de base de datos. La base de datos almacenará los documentos que queremos recuperar (divididos en fragmentos de tamaño fijo) y los vectores que representan cada fragmento. Inicializa el proyecto de Ent ejecutando el siguiente comando:

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

Este comando crea marcadores de posición para nuestros modelos de datos. Nuestro proyecto debería tener este aspecto:

├── ent
│ ├── generate.go
│ └── schema
│ ├── chunk.go
│ └── embedding.go
├── go.mod
└── go.sum

A continuación, definamos el esquema para el modelo Chunk. Abre el archivo ent/schema/chunk.go y define el esquema de la siguiente manera:

ent/schema/chunk.go
package schema

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

// Chunk holds the schema definition for the Chunk entity.
type Chunk struct {
ent.Schema
}

// Fields of the Chunk.
func (Chunk) Fields() []ent.Field {
return []ent.Field{
field.String("path"),
field.Int("nchunk"),
field.Text("data"),
}
}

// Edges of the Chunk.
func (Chunk) Edges() []ent.Edge {
return []ent.Edge{
edge.To("embedding", Embedding.Type).StorageKey(edge.Column("chunk_id")).Unique(),
}
}

Este esquema define una entidad Chunk con tres campos: path, nchunk y data. El campo path almacena la ruta del documento, nchunk almacena el número de fragmento y data almacena los datos de texto fragmentados. También definimos una relación con la entidad Embedding, que almacenará la representación vectorial del fragmento.

Antes de continuar, instalemos el paquete pgvector. pgvector es una extensión de PostgreSQL que proporciona soporte para operaciones vectoriales y búsqueda por similitud. La necesitaremos para almacenar y recuperar las representaciones vectoriales de nuestros fragmentos.

go get github.com/pgvector/pgvector-go

A continuación, definamos el esquema para el modelo Embedding. Abre el archivo ent/schema/embedding.go y define el esquema de la siguiente manera:

ent/schema/embedding.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"github.com/pgvector/pgvector-go"
)

// Embedding holds the schema definition for the Embedding entity.
type Embedding struct {
ent.Schema
}

// Fields of the Embedding.
func (Embedding) Fields() []ent.Field {
return []ent.Field{
field.Other("embedding", pgvector.Vector{}).
SchemaType(map[string]string{
dialect.Postgres: "vector(1536)",
}),
}
}

// Edges of the Embedding.
func (Embedding) Edges() []ent.Edge {
return []ent.Edge{
edge.From("chunk", Chunk.Type).Ref("embedding").Unique().Required(),
}
}

func (Embedding) Indexes() []ent.Index {
return []ent.Index{
index.Fields("embedding").
Annotations(
entsql.IndexType("hnsw"),
entsql.OpClass("vector_l2_ops"),
),
}
}

Este esquema define una entidad Embedding con un único campo embedding de tipo pgvector.Vector. El campo embedding almacena la representación vectorial del fragmento. También definimos una relación con la entidad Chunk y un índice en el campo embedding usando el tipo de índice hnsw y la clase de operadores vector_l2_ops. Este índice nos permitirá realizar búsquedas de similitud eficientes sobre los embeddings.

Finalmente, generemos el código de Ent ejecutando los siguientes comandos:

go mod tidy
go generate ./...

Ent generará el código necesario para nuestros modelos basándose en las definiciones del esquema.

Configuración de la base de datos

A continuación, configuremos la base de datos PostgreSQL. Usaremos Docker para ejecutar una instancia de PostgreSQL localmente. Como necesitamos la extensión pgvector, usaremos la imagen de Docker pgvector/pgvector:pg17, que viene con la extensión preinstalada.

docker run --rm --name postgres -e POSTGRES_PASSWORD=pass -p 5432:5432 -d pgvector/pgvector:pg17

Utilizaremos Atlas, una herramienta de esquema de base de datos como código que se integra con Ent, para gestionar nuestro esquema de base de datos. Instala Atlas ejecutando el siguiente comando:

curl -sSfL https://atlasgo.io/install.sh | sh

Para otras opciones de instalación, consulta la documentación de instalación de Atlas.

Como vamos a gestionar extensiones, necesitamos una cuenta Atlas Pro. Puedes registrarte para una prueba gratuita ejecutando:

atlas login
[Trabajando sin herramienta de migración]

Si prefieres omitir Atlas, puedes aplicar el esquema directamente a la base de datos usando las sentencias de este archivo

Ahora, creemos nuestra configuración base base.pg.hcl que proporciona la extensión vector para el esquema público:

base.pg.hcl
schema "public" {
}

extension "vector" {
schema = schema.public
}

Definamos nuestra configuración de Atlas que combina el archivo base.pg.hcl con el esquema de Ent:

atlas.hcl
data "composite_schema" "schema" {
schema {
url = "file://base.pg.hcl"
}
schema "public" {
url = "ent://ent/schema"
}
}

env "local" {
url = getenv("DB_URL")
schema {
src = data.composite_schema.schema.url
}
dev = "docker://pgvector/pg17/dev"
}

Esta configuración define un esquema compuesto que incluye el archivo base.pg.hcl y el esquema de Ent. También definimos un entorno llamado local que usa el esquema compuesto para desarrollo local. El campo dev especifica la URL de la Base de Datos de Desarrollo, que Atlas utiliza para normalizar esquemas y realizar cálculos.

Apliquemos el esquema a la base de datos ejecutando:

export DB_URL='postgresql://postgres:pass@localhost:5432/postgres?sslmode=disable'
atlas schema apply --env local

Atlas cargará el estado deseado de la base de datos desde nuestra configuración, lo comparará con el estado actual y creará un plan de migración:

Planning migration statements (5 in total):

-- create extension "vector":
-> CREATE EXTENSION "vector" WITH SCHEMA "public" VERSION "0.8.0";
-- create "chunks" table:
-> CREATE TABLE "public"."chunks" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"path" character varying NOT NULL,
"nchunk" bigint NOT NULL,
"data" text NOT NULL,
PRIMARY KEY ("id")
);
-- create "embeddings" table:
-> CREATE TABLE "public"."embeddings" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"embedding" public.vector(1536) NOT NULL,
"chunk_id" bigint NOT NULL,
PRIMARY KEY ("id"),
CONSTRAINT "embeddings_chunks_embedding" FOREIGN KEY ("chunk_id") REFERENCES "public"."chunks" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION
);
-- create index "embedding_embedding" to table: "embeddings":
-> CREATE INDEX "embedding_embedding" ON "public"."embeddings" USING hnsw ("embedding" vector_l2_ops);
-- create index "embeddings_chunk_id_key" to table: "embeddings":
-> CREATE UNIQUE INDEX "embeddings_chunk_id_key" ON "public"."embeddings" ("chunk_id");

-------------------------------------------

Analyzing planned statements (5 in total):

-- non-optimal columns alignment:
-- L4: Table "chunks" has 8 redundant bytes of padding per row. To reduce disk space,
the optimal order of the columns is as follows: "id", "nchunk", "path",
"data" https://atlasgo.io/lint/analyzers#PG110
-- ok (370.25µs)

-------------------------
-- 114.306667ms
-- 5 schema changes
-- 1 diagnostic

-------------------------------------------

? Approve or abort the plan:
▸ Approve and apply
Abort

Además de planificar el cambio, Atlas proporciona diagnósticos y sugerencias para optimizar el esquema. En este caso sugiere reordenar las columnas en la tabla chunks para reducir espacio en disco. Como no nos preocupa el espacio en este tutorial, podemos continuar seleccionando Approve and apply.

Para verificar que el esquema se aplicó correctamente, podemos reejecutar atlas schema apply. Atlas mostrará:

Schema is synced, no changes to be made

Configuración inicial del CLI

Con el esquema de base de datos listo, creemos nuestra aplicación CLI. Para este tutorial usaremos la librería alecthomas/kong para construir una pequeña aplicación que cargue, indexe y consulte documentos.

Primero instala la librería kong:

go get github.com/alecthomas/kong

Crea un archivo cmd/entrag/main.go con este contenido:

cmd/entrag/main.go
package main

import (
"fmt"
"os"

"github.com/alecthomas/kong"
)

// CLI holds global options and subcommands.
type CLI struct {
// DBURL is read from the environment variable DB_URL.
DBURL string `kong:"env='DB_URL',help='Database URL for the application.'"`
OpenAIKey string `kong:"env='OPENAI_KEY',help='OpenAI API key for the application.'"`

// Subcommands
Load *LoadCmd `kong:"cmd,help='Load command that accepts a path.'"`
Index *IndexCmd `kong:"cmd,help='Create embeddings for any chunks that do not have one.'"`
Ask *AskCmd `kong:"cmd,help='Ask a question about the indexed documents'"`
}

func main() {
var cli CLI
app := kong.Parse(&cli,
kong.Name("entrag"),
kong.Description("Ask questions about markdown files."),
kong.UsageOnError(),
)
if err := app.Run(&cli); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
}

Crea otro archivo cmd/entrag/rag.go con este contenido:

cmd/entrag/rag.go
package main

type (
// LoadCmd loads the markdown files into the database.
LoadCmd struct {
Path string `help:"path to dir with markdown files" type:"existingdir" required:""`
}
// IndexCmd creates the embedding index on the database.
IndexCmd struct {
}
// AskCmd is another leaf command.
AskCmd struct {
// Text is the positional argument for the ask command.
Text string `kong:"arg,required,help='Text for the ask command.'"`
}
)

Verifica que la aplicación CLI funciona ejecutando:

go run ./cmd/entrag --help

Si todo está configurado correctamente, verás la ayuda de la aplicación:

Usage: entrag <command> [flags]

Ask questions about markdown files.

Flags:
-h, --help Show context-sensitive help.
--dburl=STRING Database URL for the application ($DB_URL).
--open-ai-key=STRING OpenAI API key for the application ($OPENAI_KEY).

Commands:
load --path=STRING [flags]
Load command that accepts a path.

index [flags]
Create embeddings for any chunks that do not have one.

ask <text> [flags]
Ask a question about the indexed documents

Run "entrag <command> --help" for more information on a command.

Cargar documentos en la base de datos

Necesitamos archivos Markdown para cargar. Crea un directorio data y añade archivos Markdown. Para este ejemplo, descargué el repositorio ent/ent y usé el directorio docs como fuente.

Implementemos el comando LoadCmd para cargar los archivos. Añade este código en cmd/entrag/rag.go:

cmd/entrag/rag.go
const (
tokenEncoding = "cl100k_base"
chunkSize = 1000
)

// Run is the method called when the "load" command is executed.
func (cmd *LoadCmd) Run(ctx *CLI) error {
client, err := ctx.entClient()
if err != nil {
return fmt.Errorf("failed opening connection to postgres: %w", err)
}
tokTotal := 0
return filepath.WalkDir(ctx.Load.Path, func(path string, d fs.DirEntry, err error) error {
if filepath.Ext(path) == ".mdx" || filepath.Ext(path) == ".md" {
chunks := breakToChunks(path)
for i, chunk := range chunks {
tokTotal += len(chunk)
client.Chunk.Create().
SetData(chunk).
SetPath(path).
SetNchunk(i).
SaveX(context.Background())
}
}
return nil
})
}

func (c *CLI) entClient() (*ent.Client, error) {
return ent.Open("postgres", c.DBURL)
}

Este código define el método Run para LoadCmd. Lee los archivos Markdown de la ruta especificada, los divide en fragmentos de 1000 tokens y los guarda en la base de datos. Usamos entClient para crear un cliente Ent con la URL de base de datos especificada en las opciones CLI.

Para la implementación de breakToChunks, consulta el código completo en el repositorio entrag, que se basa casi por completo en la introducción a RAG en Go de Eli Bendersky.

Finalmente, ejecutemos el comando load para cargar los archivos markdown en la base de datos:

go run ./cmd/entrag load --path=data

Cuando termine el comando, deberías ver los fragmentos cargados en la base de datos. Para verificar, ejecuta:

docker exec -it postgres psql -U postgres -d postgres -c "SELECT COUNT(*) FROM chunks;"

Deberías ver algo similar a:

  count
-------
276
(1 row)

Indexando los embeddings

Ahora que hemos cargado los documentos en la base de datos, necesitamos crear embeddings para cada fragmento. Usaremos la API de OpenAI para generar los embeddings. Primero instala el paquete openai:

go get github.com/sashabaranov/go-openai

Si no tienes una clave API de OpenAI, puedes registrarte en la Plataforma OpenAI y generar una clave API.

Leeremos esta clave desde la variable de entorno OPENAI_KEY, así que configúrala:

export OPENAI_KEY=<your OpenAI API key>

A continuación, implementemos el comando IndexCmd para crear embeddings. Abre el archivo cmd/entrag/rag.go y añade el siguiente código:

cmd/entrag/rag.go
// Run is the method called when the "index" command is executed.
func (cmd *IndexCmd) Run(cli *CLI) error {
client, err := cli.entClient()
if err != nil {
return fmt.Errorf("failed opening connection to postgres: %w", err)
}
ctx := context.Background()
chunks := client.Chunk.Query().
Where(
chunk.Not(
chunk.HasEmbedding(),
),
).
Order(ent.Asc(chunk.FieldID)).
AllX(ctx)
for _, ch := range chunks {
log.Println("Created embedding for chunk", ch.Path, ch.Nchunk)
embedding := getEmbedding(ch.Data)
_, err := client.Embedding.Create().
SetEmbedding(pgvector.NewVector(embedding)).
SetChunk(ch).
Save(ctx)
if err != nil {
return fmt.Errorf("error creating embedding: %v", err)
}
}
return nil
}

// getEmbedding invokes the OpenAI embedding API to calculate the embedding
// for the given string. It returns the embedding.
func getEmbedding(data string) []float32 {
client := openai.NewClient(os.Getenv("OPENAI_KEY"))
queryReq := openai.EmbeddingRequest{
Input: []string{data},
Model: openai.AdaEmbeddingV2,
}
queryResponse, err := client.CreateEmbeddings(context.Background(), queryReq)
if err != nil {
log.Fatalf("Error getting embedding: %v", err)
}
return queryResponse.Data[0].Embedding
}

Hemos definido el método Run para el comando IndexCmd. Este método consulta los fragmentos sin embeddings, genera sus embeddings usando la API de OpenAI y los guarda en la base de datos.

Finalmente, ejecutemos el comando index para crear los embeddings:

go run ./cmd/entrag index

Deberías ver registros similares a:

2025/02/13 13:04:42 Created embedding for chunk /Users/home/entr/data/md/aggregate.md 0
2025/02/13 13:04:43 Created embedding for chunk /Users/home/entr/data/md/ci.mdx 0
2025/02/13 13:04:44 Created embedding for chunk /Users/home/entr/data/md/ci.mdx 1
2025/02/13 13:04:45 Created embedding for chunk /Users/home/entr/data/md/ci.mdx 2
2025/02/13 13:04:46 Created embedding for chunk /Users/home/entr/data/md/code-gen.md 0
2025/02/13 13:04:47 Created embedding for chunk /Users/home/entr/data/md/code-gen.md 1

Haciendo preguntas

Ahora que tenemos los documentos cargados y sus embeddings, podemos implementar el comando AskCmd para hacer preguntas. Abre cmd/entrag/rag.go y añade:

cmd/entrag/rag.go
// Run is the method called when the "ask" command is executed.
func (cmd *AskCmd) Run(ctx *CLI) error {
client, err := ctx.entClient()
if err != nil {
return fmt.Errorf("failed opening connection to postgres: %w", err)
}
question := cmd.Text
emb := getEmbedding(question)
embVec := pgvector.NewVector(emb)
embs := client.Embedding.
Query().
Order(func(s *sql.Selector) {
s.OrderExpr(sql.ExprP("embedding <-> $1", embVec))
}).
WithChunk().
Limit(5).
AllX(context.Background())
b := strings.Builder{}
for _, e := range embs {
chnk := e.Edges.Chunk
b.WriteString(fmt.Sprintf("From file: %v\n", chnk.Path))
b.WriteString(chnk.Data)
}
query := fmt.Sprintf(`Use the below information from the ent docs to answer the subsequent question.
Information:
%v

Question: %v`, b.String(), question)
oac := openai.NewClient(ctx.OpenAIKey)
resp, err := oac.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: openai.GPT4o,
Messages: []openai.ChatCompletionMessage{

{
Role: openai.ChatMessageRoleUser,
Content: query,
},
},
},
)
if err != nil {
return fmt.Errorf("error creating chat completion: %v", err)
}
choice := resp.Choices[0]
out, err := glamour.Render(choice.Message.Content, "dark")
fmt.Print(out)
return nil
}

Aquí es donde convergen todas las partes. Tras preparar nuestra base de datos con documentos y sus embeddings, podemos preguntar sobre ellos. Analicemos el comando AskCmd:

emb := getEmbedding(question)
embVec := pgvector.NewVector(emb)
embs := client.Embedding.
Query().
Order(func(s *sql.Selector) {
s.OrderExpr(sql.ExprP("embedding <-> $1", embVec))
}).
WithChunk().
Limit(5).
AllX(context.Background())

Comenzamos transformando la pregunta del usuario en un vector usando la API de OpenAI. Con este vector buscamos los embeddings más similares en nuestra base de datos. Consultamos los embeddings ordenados por similitud usando el operador <-> de pgvector y limitamos a los 5 mejores resultados.

for _, e := range embs {
chnk := e.Edges.Chunk
b.WriteString(fmt.Sprintf("From file: %v\n", chnk.Path))
b.WriteString(chnk.Data)
}
query := fmt.Sprintf(`Use the below information from the ent docs to answer the subsequent question.
Information:
%v

Question: %v`, b.String(), question)

Preparamos la información de los 5 fragmentos principales como contexto para la pregunta. Formateamos la pregunta y el contexto en una única cadena.

oac := openai.NewClient(ctx.OpenAIKey)
resp, err := oac.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: openai.GPT4o,
Messages: []openai.ChatCompletionMessage{

{
Role: openai.ChatMessageRoleUser,
Content: query,
},
},
},
)
if err != nil {
return fmt.Errorf("error creating chat completion: %v", err)
}
choice := resp.Choices[0]
out, err := glamour.Render(choice.Message.Content, "dark")
fmt.Print(out)

Luego usamos la API de OpenAI para generar una respuesta. Pasamos la pregunta y contexto a la API y recibimos una respuesta. Finalmente, formateamos la respuesta con el paquete glamour para mostrarla en terminal.

Antes de ejecutar ask, instala el paquete glamour:

go get github.com/charmbracelet/glamour

Finalmente, ejecutemos ask para hacer una pregunta sobre los documentos:

go run ./cmd/entrag ask "tl;dr What is Ent?"

Y nuestro sistema RAG responde:

  Ent is an open-source entity framework (ORM) for the Go programming language. It
allows developers to define data models or graph-structures in Go code. Ent
emphasizes principles such as schema as code, a statically typed and explicit
API generated through codegen, simple queries and graph traversals, statically
typed predicates, and storage agnosticism. It supports various databases,
including MySQL, MariaDB, PostgreSQL, SQLite, and Gremlin-based graph databases,
and aims to enhance productivity in Go development.

¡Increíble! Hemos construido un sistema RAG usando Ent, Atlas y pgvector. Ahora podemos hacer preguntas sobre los documentos cargados y recibir respuestas con contexto.

Aquí tienes más preguntas y respuestas con nuestro práctico sistema RAG:

go run ./cmd/entrag ask "how to define order field in entgql"
To define an order field in entgql , you need to annotate a field in your Ent schema using the entgql.Annotation with the OrderField . Here's a step-by- step guide on how you can do it:
  1. Choose a Comparable Field: Select the field in your schema that you want to be orderable. This could be a text field, a timestamp, an integer, an enum, etc.

  2. Annotate the Field: Use the entgql.OrderField annotation on the chosen field. The order field name should be uppercase and match its corresponding enum value in the GraphQL schema.

  3. Update your Schema: Here’s an example of how to annotate your fields in the ent/schema :

    func (Todo) Fields() []ent.Field {
    return []ent.Field{
    field.Text("text").
    NotEmpty().
    Annotations(
    entgql.OrderField("TEXT"),
    ),
    field.Time("created_at").
    Default(time.Now).
    Immutable().
    Annotations(
    entgql.OrderField("CREATED_AT"),
    ),
    field.Enum("status").
    NamedValues(
    "InProgress", "IN_PROGRESS",
    "Completed", "COMPLETED",
    ).
    Default("IN_PROGRESS").
    Annotations(
    entgql.OrderField("STATUS"),
    ),
    field.Int("priority").
    Default(0).
    Annotations(
    entgql.OrderField("PRIORITY"),
    ),
    }
    }
  4. Alternatively, Multiple Order Fields: If you want to enable ordering by multiple fields, you can use the entgql.MultiOrder() annotation:

    func (Todo) Annotations() []schema.Annotation {
    return []schema.Annotation{
    entgql.MultiOrder(),
    }
    }
  5. Generate GraphQL Types: Ensure that the GraphQL types match this setup in your GraphQL schema, for example:

    enum OrderDirection {
    ASC
    DESC
    }
    enum TodoOrderField {
    CREATED_AT
    PRIORITY
    STATUS
    TEXT
    }
    input TodoOrder {
    direction: OrderDirection!
    field: TodoOrderField
    }
  6. Add orderBy Argument to Queries: Include the orderBy parameter in your GraphQL queries to allow clients to sort items:

    type Query {
    todos(
    after: Cursor
    first: Int
    before: Cursor
    last: Int
    orderBy: TodoOrder
    ): TodoConnection!
    }

    By following these steps, you ensure that your Ent-based application can support sorting in your GraphQL API based on various fields.

go run ./cmd/entrag ask "what's the difference between privacy rules and interceptors"
Privacy rules and interceptors serve different purposes within the Ent framework, each providing distinct functionalities:
  1. Privacy Rules:
  • Purpose: Privacy rules are primarily concerned with enforcing access control policies for data queries and mutations. They determine whether an operation is allowed or denied based on predefined rules.
  • Implementation: They are implemented as part of a privacy policy using the ent.Policy interface which includes EvalQuery and EvalMutation methods.
  • Operation: Privacy rules evaluate whether specific conditions are met before allowing or denying access to the data. They can return decisions like privacy.Allow , privacy.Deny , or privacy.Skip to control the flow of evaluation.
  • Use Case: Ideal for managing access control by ensuring that users can only perform certain operations if they meet the specified criteria.
  1. Interceptors:
  • Purpose: Interceptors act as middleware for Ent queries, allowing modification and customization of query behaviors. They can be used to augment or modify queries during different stages of their lifecycle.
  • Implementation: Implemented as interfaces or using the ent.InterceptFunc adapter. They intercept and potentially modify queries by working on the read-path.
  • Operation: Interceptors modify or enhance queries, typically without the access control logic inherent in privacy rules. They provide hooks to execute custom logic pre and post query execution.
  • Use Case: Suitable for generic transformations or modifications to queries, such as adding default filters, query limitations, or logging operations without focusing on access control.

In summary, while privacy rules focus on access control, interceptors are about managing and modifying the query execution process.

Para finalizar

En esta entrada de blog, hemos explorado cómo construir un sistema RAG usando Ent, Atlas y pgvector. Un agradecimiento especial a Eli Bendersky por su informativa entrada de blog y por sus excelentes escritos sobre Go a lo largo de los años!

· 3 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

En resumen

Crea una visualización de tu esquema Ent con un solo comando:

atlas schema inspect \
-u ent://ent/schema \
--dev-url "sqlite://demo?mode=memory&_fk=1" \
--visualize

¡Hola a todos!

Hace unos meses, presentamos entviz, una herramienta interesante que permite visualizar tus esquemas Ent. Dado su éxito y popularidad, decidimos integrarla directamente en Atlas, el motor de migraciones que utiliza Ent.

Desde el lanzamiento de la v0.13.0 de Atlas, ahora puedes visualizar tus esquemas Ent directamente desde Atlas sin necesidad de instalar herramientas adicionales.

Visualizaciones Privadas vs. Públicas

Anteriormente, solo podías compartir visualizaciones de tu esquema en el Atlas Public Playground. Aunque esto resulta práctico para compartir esquemas, no es viable para muchos equipos que trabajan con esquemas sensibles que no pueden hacerse públicos.

Con esta nueva versión, puedes publicar fácilmente tu esquema directamente en tu espacio de trabajo privado en Atlas Cloud. Esto significa que solo tú y tu equipo tendréis acceso a la visualización del esquema.

Visualizando tu Esquema Ent con Atlas

Para visualizar tu esquema Ent con Atlas, primero instala su última versión:

curl -sSfL https://atlasgo.io/install.sh | sh

Para otras opciones de instalación, consulta la documentación de instalación de Atlas.

Luego, ejecuta este comando para generar la visualización de tu esquema Ent:

atlas schema inspect \
-u ent://ent/schema \
--dev-url "sqlite://demo?mode=memory&_fk=1" \
--visualize

Analicemos este comando:

  • atlas schema inspect - este comando inspecciona esquemas desde múltiples fuentes y los exporta en varios formatos. En este caso, lo usamos para examinar un esquema Ent.

  • -u ent://ent/schema - URL del esquema Ent a inspeccionar. Usamos el cargador ent:// apuntando al directorio local ./ent/schema.

  • --dev-url "sqlite://demo?mode=memory&_fk=1" - Atlas necesita una base de datos vacía llamada Dev Database para normalizar esquemas y realizar cálculos. Aquí usamos una base SQLite en memoria; pero para otros drivers puedes usar docker://mysql/8/dev (MySQL) o docker://postgres/15/?search_path=public (PostgreSQL).

Al ejecutar el comando, verás esta salida:

Use the arrow keys to navigate: ↓ ↑ → ←
? Where would you like to share your schema visualization?:
▸ Publicly (gh.atlasgo.cloud)
Your personal workspace (requires 'atlas login')

Si quieres compartir tu esquema públicamente, selecciona la primera opción. Para compartirlo de forma privada, elige la segunda opción y ejecuta atlas login para acceder a tu cuenta (gratuita) de Atlas.

Para finalizar

En este artículo hemos mostrado cómo visualizar fácilmente tus esquemas Ent con Atlas. Esperamos que esta funcionalidad te resulte útil y estamos deseando conocer tus comentarios.

[Para más noticias y actualizaciones de Ent:]

· 19 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Ent es un framework de entidades de código abierto para Go. Es similar a los ORMs tradicionales, pero tiene características distintivas que lo han hecho muy popular en la comunidad de Go. Ent fue liberado como código abierto por Ariel en 2019, cuando trabajaba en Facebook. Ent surgió de las dificultades para gestionar el desarrollo de aplicaciones con modelos de datos muy grandes y complejos, y funcionó con éxito dentro de Facebook durante un año antes de su publicación. Tras graduarse del programa de código abierto de Facebook, Ent se unió a la Linux Foundation en septiembre de 2021.

Este tutorial está dirigido a principiantes en Ent y Go que quieran comenzar construyendo un proyecto simple: un sistema de gestión de contenido mínimo.

En los últimos años, Ent se ha convertido en uno de los ORMs de más rápido crecimiento en Go:

Source: @ossinsight_bot on Twitter, November 2022

Algunas de las características más destacadas de Ent son:

  • API de Go con tipos seguros para trabajar con tu base de datos. Olvídate de usar interface{} o reflexión para interactuar con tu base de datos. Usa Go puro que tu editor entienda y tu compilador verifique.

  • Modela tus datos con semántica de grafos - Ent utiliza semántica de grafos para modelar los datos de tu aplicación. Esto facilita enormemente recorrer conjuntos de datos complejos con una API simple.

    Supongamos que queremos obtener todos los usuarios que están en grupos relacionados con perros. Aquí hay dos formas de escribir esto con Ent:

    // Start traversing from the topic.
    client.Topic.Query().
    Where(topic.Name("dogs")).
    QueryGroups().
    QueryUsers().
    All(ctx)

    // OR: Start traversing from the users and filter.
    client.User.Query().
    Where(
    user.HasGroupsWith(
    group.HasTopicsWith(
    topic.Name("dogs"),
    ),
    ),
    ).
    All(ctx)
  • Generación automática de servidores - ya necesites GraphQL, gRPC o una capa API compatible con OpenAPI, Ent puede generar el código necesario para crear un servidor de alto rendimiento sobre tu base de datos. Ent generará tanto los esquemas de terceros (tipos GraphQL, mensajes Protobuf, etc.) como código optimizado para las tareas repetitivas de lectura y escritura en la base de datos.

  • Integrado con Atlas - Ent se construye con una rica integración con Atlas, una herramienta robusta de gestión de esquemas con muchas capacidades avanzadas. Atlas puede planificar automáticamente migraciones de esquemas, así como verificarlas en CI o implementarlas en producción por ti. (Revelación completa: Ariel y yo somos los creadores y mantenedores)

Requisitos previos

[Repositorio de apoyo]

Puedes encontrar el código mostrado en este tutorial en este repositorio.

Paso 1: Configurar el esquema de la base de datos

Puedes encontrar el código descrito en este paso en este commit.

Comencemos inicializando nuestro proyecto usando go mod init:

go mod init github.com/rotemtam/ent-blog-example

Go confirma que se ha creado nuestro nuevo módulo:

go: creating new go.mod: module github.com/rotemtam/ent-blog-example

Lo primero que abordaremos en nuestro proyecto de demostración será configurar nuestra base de datos. Crearemos nuestro modelo de datos de la aplicación usando Ent. Vamos a instalarlo con go get:

go get -u entgo.io/ent@master

Una vez instalado, podemos usar la CLI de Ent para inicializar los modelos de los dos tipos de entidades con los que trabajaremos en este tutorial: User y Post.

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

Observa que se crean varios archivos:

.
`-- ent
|-- generate.go
`-- schema
|-- post.go
`-- user.go

2 directories, 3 files

Ent creó la estructura básica de nuestro proyecto:

  • generate.go: veremos en un momento cómo este archivo se usa para invocar el motor de generación de código de Ent.

  • El directorio schema, con un ent.Schema básico para cada entidad solicitada.

Continuemos definiendo el esquema para nuestras entidades. Esta es la definición del esquema para User:

// Fields of the User.  
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.String("email").
Unique(),
field.Time("created_at").
Default(time.Now),
}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("posts", Post.Type),
}
}

Observa que definimos tres campos: name, email y created_at (que toma el valor predeterminado de time.Now()). Como esperamos que los emails sean únicos en nuestro sistema, añadimos esa restricción en el campo email. Además, definimos una arista llamada posts hacia el tipo Post. Las aristas se usan en Ent para definir relaciones entre entidades. Al trabajar con bases de datos relacionales, las aristas se traducen en claves foráneas y tablas de asociación.

// Post holds the schema definition for the Post entity.
type Post struct {
ent.Schema
}

// Fields of the Post.
func (Post) Fields() []ent.Field {
return []ent.Field{
field.String("title"),
field.Text("body"),
field.Time("created_at").
Default(time.Now),
}
}

// Edges of the Post.
func (Post) Edges() []ent.Edge {
return []ent.Edge{
edge.From("author", User.Type).
Unique().
Ref("posts"),
}
}

En el esquema Post, definimos también tres campos: title, body y created_at. Además, definimos una arista llamada author desde Post hacia la entidad User. Marcamos esta arista como Unique porque en nuestro incipiente sistema, cada publicación solo puede tener un autor. Usamos Ref para indicar a Ent que la referencia inversa de esta arista es la arista posts en User.

El poder de Ent proviene de su motor de generación de código. Al desarrollar con Ent, cada vez que hacemos cambios en el esquema de la aplicación, debemos invocar el motor de generación de código para regenerar nuestro código de acceso a la base de datos. Esto es lo que permite a Ent mantener una API Go segura en tipos y eficiente para nosotros.

Veámoslo en acción, ejecuta:

go generate ./...

Observa que se crearon muchos archivos Go nuevos para nosotros:

.
`-- ent
|-- client.go
|-- context.go
|-- ent.go
|-- enttest
| `-- enttest.go
/// .. Truncated for brevity
|-- user_query.go
`-- user_update.go

9 directories, 29 files
información

Si te interesa ver cómo es el esquema real de la base de datos para nuestra aplicación, puedes usar una herramienta útil llamada entviz:

go run -mod=mod ariga.io/entviz ./ent/schema

Para ver el resultado, haz clic aquí.

Una vez definido nuestro modelo de datos, creemos el esquema de la base de datos.

Para instalar la última versión de Atlas, simplemente ejecuta uno de los siguientes comandos en tu terminal, o visita el sitio web de Atlas:

curl -sSf https://atlasgo.sh | sh

Con Atlas instalado, podemos crear el script de migración inicial:

atlas migrate diff add_users_posts \
--dir "file://ent/migrate/migrations" \
--to "ent://ent/schema" \
--dev-url "docker://mysql/8/ent"

Observa que se crearon dos archivos nuevos:

ent/migrate/migrations
|-- 20230226150934_add_users_posts.sql
`-- atlas.sum

El archivo SQL (el nombre real variará en tu máquina según la fecha y hora en que ejecutes atlas migrate diff) contiene las sentencias SQL DDL necesarias para configurar el esquema de la base de datos en una base de datos MySQL vacía:

-- create "users" table  
CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `created_at` timestamp NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `email` (`email`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
-- create "posts" table
CREATE TABLE `posts` (`id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `body` longtext NOT NULL, `created_at` timestamp NOT NULL, `user_posts` bigint NULL, PRIMARY KEY (`id`), INDEX `posts_users_posts` (`user_posts`), CONSTRAINT `posts_users_posts` FOREIGN KEY (`user_posts`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE SET NULL) CHARSET utf8mb4 COLLATE utf8mb4_bin;

Para configurar nuestro entorno de desarrollo, usemos Docker para ejecutar un contenedor local de mysql:

docker run --rm --name entdb -d -p 3306:3306 -e MYSQL_DATABASE=ent -e MYSQL_ROOT_PASSWORD=pass mysql:8

Finalmente, ejecutemos el script de migración en nuestra base de datos local:

atlas migrate apply --dir file://ent/migrate/migrations \
--url mysql://root:pass@localhost:3306/ent

Atlas reporta que creó las tablas correctamente:

Migrating to version 20230220115943 (1 migrations in total):

-- migrating version 20230220115943
-> CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, `created_at` timestamp NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `email` (`email`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
-> CREATE TABLE `posts` (`id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `body` longtext NOT NULL, `created_at` timestamp NOT NULL, `post_author` bigint NULL, PRIMARY KEY (`id`), INDEX `posts_users_author` (`post_author`), CONSTRAINT `posts_users_author` FOREIGN KEY (`post_author`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE SET NULL) CHARSET utf8mb4 COLLATE utf8mb4_bin;
-- ok (55.972329ms)

-------------------------
-- 67.18167ms
-- 1 migrations
-- 2 sql statements

Paso 2: Poblar nuestra base de datos

información

El código para este paso se puede encontrar en este commit.

Mientras desarrollamos nuestro sistema de gestión de contenidos, sería triste cargar una página web y no ver contenido. Comencemos insertando datos en nuestra base de datos y aprendiendo algunos conceptos de Ent.

Para acceder a nuestra base de datos MySQL local, necesitamos un controlador. Usa go get para obtenerlo:

go get -u github.com/go-sql-driver/mysql

Crea un archivo llamado main.go y añade este script básico de inicialización:

package main

import (
"context"
"flag"
"fmt"
"log"

"github.com/rotemtam/ent-blog-example/ent"

_ "github.com/go-sql-driver/mysql"
"github.com/rotemtam/ent-blog-example/ent/user"
)

func main() {
// Read the connection string to the database from a CLI flag.
var dsn string
flag.StringVar(&dsn, "dsn", "", "database DSN")
flag.Parse()

// Instantiate the Ent client.
client, err := ent.Open("mysql", dsn)
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()

ctx := context.Background()
// If we don't have any posts yet, seed the database.
if !client.Post.Query().ExistX(ctx) {
if err := seed(ctx, client); err != nil {
log.Fatalf("failed seeding the database: %v", err)
}
}
// ... Continue with server start.
}

func seed(ctx context.Context, client *ent.Client) error {
// Check if the user "rotemtam" already exists.
r, err := client.User.Query().
Where(
user.Name("rotemtam"),
).
Only(ctx)
switch {
// If not, create the user.
case ent.IsNotFound(err):
r, err = client.User.Create().
SetName("rotemtam").
SetEmail("r@hello.world").
Save(ctx)
if err != nil {
return fmt.Errorf("failed creating user: %v", err)
}
case err != nil:
return fmt.Errorf("failed querying user: %v", err)
}
// Finally, create a "Hello, world" blogpost.
return client.Post.Create().
SetTitle("Hello, World!").
SetBody("This is my first post").
SetAuthor(r).
Exec(ctx)
}

Como puedes ver, este programa primero verifica si existe alguna entidad Post en la base de datos. Si no existe, ejecuta la función seed. Esta función utiliza Ent para recuperar el usuario llamado rotemtam de la base de datos y, si no existe, intenta crearlo. Finalmente, la función crea una entrada de blog con este usuario como autor.

Ejecútalo:

 go run main.go -dsn "root:pass@tcp(localhost:3306)/ent?parseTime=true"

Paso 3: Creando la página de inicio

información

El código descrito en este paso se encuentra en este commit

Vamos a crear ahora la página principal del blog. Constará de varias partes:

  1. La vista - una plantilla Go html/template que renderiza el HTML real que verán los usuarios.

  2. El código del servidor - contiene los manejadores de peticiones HTTP que comunicarán los navegadores de nuestros usuarios y renderizarán nuestras plantillas con datos obtenidos de la base de datos.

  3. El enrutador - registra diferentes rutas a manejadores.

  4. Una prueba unitaria - para verificar que nuestro servidor se comporta correctamente.

La vista

Go incluye un excelente motor de plantillas con dos variantes: text/template para renderizar texto genérico y html/template que añade características de seguridad adicionales para prevenir inyección de código al trabajar con documentos HTML. Más información aquí.

Creemos nuestra primera plantilla para mostrar una lista de entradas del blog. Crea un archivo llamado templates/list.tmpl:

<html>
<head>
<title>My Blog</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">

</head>
<body>
<div class="col-lg-8 mx-auto p-4 py-md-5">
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
<span class="fs-4">Ent Blog Demo</span>
</a>
</header>

<main>
<div class="row g-5">
<div class="col-md-12">
{{- range . }}
<h2>{{ .Title }}</h2>
<p>
{{ .CreatedAt.Format "2006-01-02" }} by {{ .Edges.Author.Name }}
</p>
<p>
{{ .Body }}
</p>
{{- end }}
</div>

</div>
</main>
<footer class="pt-5 my-5 text-muted border-top">
<p>
This is the Ent Blog Demo. It is a simple blog application built with Ent and Go. Get started:
</p>
<pre>go get entgo.io/ent</pre>
</footer>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
crossorigin="anonymous"></script>
</body>
</html>

Aquí usamos una versión modificada de la Plantilla Inicial de Bootstrap como base de nuestra interfaz. Destacamos las partes importantes: en nuestro manejador index, pasaremos a esta plantilla un slice de objetos Post.

Dentro de la plantilla Go, los datos que pasamos están disponibles como ".". Esta línea usa range para iterar sobre cada entrada:

{{- range . }}

Luego, mostramos el título, fecha de creación y nombre del autor mediante la relación Author:

<h2>{{ .Title }}</h2>
<p>
{{ .CreatedAt.Format "2006-01-02" }} by {{ .Edges.Author.Name }}
</p>

Finalmente, mostramos el cuerpo de la entrada y cerramos el bucle.

    <p>
{{ .Body }}
</p>
{{- end }}

Tras definir la plantilla, debemos hacerla disponible en nuestro programa. Incrustamos esta plantilla en nuestro binario usando el paquete embed (documentación):

var (  
//go:embed templates/*
resources embed.FS
tmpl = template.Must(template.ParseFS(resources, "templates/*"))
)

Código del servidor

Continuamos definiendo un tipo llamado server y su constructor newServer. Esta estructura tendrá métodos receptores para cada manejador HTTP y vinculará el cliente Ent creado al inicio con el código del servidor.

type server struct {
client *ent.Client
}

func newServer(client *ent.Client) *server {
return &server{client: client}
}

Definamos ahora el manejador para nuestra página principal del blog. Esta página debe contener una lista de todas las entradas disponibles:

// index serves the blog home page
func (s *server) index(w http.ResponseWriter, r *http.Request) {
posts, err := s.client.Post.
Query().
WithAuthor().
All(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := tmpl.Execute(w, posts); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

Centrémonos en el código de Ent que recupera las entradas de la base de datos:

// s.client.Post contains methods for interacting with Post entities
s.client.Post.
// Begin a query.
Query().
// Retrieve the entities using the `Author` edge. (a `User` instance)
WithAuthor().
// Run the query against the database using the request context.
All(r.Context())

El enrutador

Para gestionar las rutas de nuestra aplicación, usemos go-chi, una popular biblioteca de enrutamiento para Go.

go get -u github.com/go-chi/chi/v5

Definimos la función newRouter que configura nuestro enrutador:

// newRouter creates a new router with the blog handlers mounted.
func newRouter(srv *server) chi.Router {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", srv.index)
return r
}

En esta función, primero instanciamos un nuevo chi.Router, luego registramos dos middlewares:

  • middleware.Logger es un registrador de acceso básico que imprime información sobre cada petición que maneja nuestro servidor.

  • middleware.Recoverer se recupera de los pánicos en nuestros manejadores, evitando que todo el servidor se caiga debido a un error en la aplicación.

Finalmente, registramos la función index de la estructura server para manejar solicitudes GET en la ruta / de nuestro servidor.

Una prueba unitaria

Antes de conectar todo, escribamos una prueba unitaria simple para verificar que nuestro código funciona como se espera.

Para simplificar nuestras pruebas instalaremos el controlador SQLite para Go, que nos permite usar una base de datos en memoria:

go get -u github.com/mattn/go-sqlite3

A continuación, instalamos testify, una biblioteca de utilidades que se usa comúnmente para escribir aserciones en pruebas.

go get github.com/stretchr/testify 

Con estas dependencias instaladas, crea un nuevo archivo llamado main_test.go:

package main

import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"

_ "github.com/mattn/go-sqlite3"
"github.com/rotemtam/ent-blog-example/ent/enttest"
"github.com/stretchr/testify/require"
)

func TestIndex(t *testing.T) {
// Initialize an Ent client that uses an in memory SQLite db.
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()

// seed the database with our "Hello, world" post and user.
err := seed(context.Background(), client)
require.NoError(t, err)

// Initialize a server and router.
srv := newServer(client)
r := newRouter(srv)

// Create a test server using the `httptest` package.
ts := httptest.NewServer(r)
defer ts.Close()

// Make a GET request to the server root path.
resp, err := ts.Client().Get(ts.URL)

// Assert we get a 200 OK status code.
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)

// Read the response body and assert it contains "Hello, world!"
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(body), "Hello, World!")
}

Ejecuta la prueba para verificar que nuestro servidor funciona correctamente:

go test ./...

Observa que nuestra prueba pasa:

ok      github.com/rotemtam/ent-blog-example    0.719s
? github.com/rotemtam/ent-blog-example/ent [no test files]
? github.com/rotemtam/ent-blog-example/ent/enttest [no test files]
? github.com/rotemtam/ent-blog-example/ent/hook [no test files]
? github.com/rotemtam/ent-blog-example/ent/migrate [no test files]
? github.com/rotemtam/ent-blog-example/ent/post [no test files]
? github.com/rotemtam/ent-blog-example/ent/predicate [no test files]
? github.com/rotemtam/ent-blog-example/ent/runtime [no test files]
? github.com/rotemtam/ent-blog-example/ent/schema [no test files]
? github.com/rotemtam/ent-blog-example/ent/user [no test files]

Integrando todo

Finalmente, actualicemos nuestra función main para integrar todo:

func main() {  
// Read the connection string to the database from a CLI flag.
var dsn string
flag.StringVar(&dsn, "dsn", "", "database DSN")
flag.Parse()

// Instantiate the Ent client.
client, err := ent.Open("mysql", dsn)
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()

ctx := context.Background()
// If we don't have any posts yet, seed the database.
if !client.Post.Query().ExistX(ctx) {
if err := seed(ctx, client); err != nil {
log.Fatalf("failed seeding the database: %v", err)
}
}
srv := newServer(client)
r := newRouter(srv)
log.Fatal(http.ListenAndServe(":8080", r))
}

¡Ahora podemos ejecutar nuestra aplicación y maravillarnos con nuestro logro: una página principal de blog funcional!

 go run main.go -dsn "root:pass@tcp(localhost:3306)/test?parseTime=true"

Paso 4: Añadiendo contenido

[]

Puedes seguir los cambios de este paso en este commit.

Ningún sistema de gestión de contenido estaría completo sin la capacidad, bueno, de gestionar contenido. Demostremos cómo podemos añadir soporte para publicar nuevas entradas en nuestro blog.

Empecemos creando el manejador del backend:

// add creates a new blog post.
func (s *server) add(w http.ResponseWriter, r *http.Request) {
author, err := s.client.User.Query().Only(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := s.client.Post.Create().
SetTitle(r.FormValue("title")).
SetBody(r.FormValue("body")).
SetAuthor(author).
Exec(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
http.Redirect(w, r, "/", http.StatusFound)
}

Como puedes ver, el manejador actualmente carga el único usuario de la tabla users (ya que aún no hemos creado un sistema de gestión de usuarios ni capacidades de inicio de sesión). Only fallará a menos que se recupere exactamente un resultado de la base de datos.

A continuación, nuestro manejador crea una nueva entrada, estableciendo los campos de título y cuerpo con los valores recuperados de r.FormValue. Aquí es donde Go almacena toda la entrada del formulario pasada a una solicitud HTTP.

Después de crear el manejador, debemos conectarlo a nuestro enrutador:

// newRouter creates a new router with the blog handlers mounted.
func newRouter(srv *server) chi.Router {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", srv.index)
r.Post("/add", srv.add)
return r
}

A continuación, podemos añadir un componente HTML <form> que será usado por nuestro usuario para escribir su contenido:

<div class="col-md-12">
<hr/>
<h2>Create a new post</h2>
<form action="/add" method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input name="title" type="text" class="form-control" id="title" placeholder="Once upon a time..">
</div>
<div class="mb-3">
<label for="body" class="form-label">Body</label>
<textarea name="body" class="form-control" id="body" rows="8"></textarea>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary mb-3">Post</button>
</div>
</form>
</div>

Además, añadamos un toque agradable: mostrar las entradas del blog de la más nueva a la más antigua. Para hacer esto, modifica el manejador index para ordenar las entradas en orden descendente usando la columna created_at:

posts, err := s.client.Post.
Query().
WithAuthor().
Order(ent.Desc(post.FieldCreatedAt)).
All(ctx)

Finalmente, añadamos otra prueba unitaria que verifique que el flujo de añadir entradas funciona como se espera:

func TestAdd(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
err := seed(context.Background(), client)
require.NoError(t, err)

srv := newServer(client)
r := newRouter(srv)

ts := httptest.NewServer(r)
defer ts.Close()

// Post the form.
resp, err := ts.Client().PostForm(ts.URL+"/add", map[string][]string{
"title": {"Testing, one, two."},
"body": {"This is a test"},
})
require.NoError(t, err)
// We should be redirected to the index page and receive 200 OK.
require.Equal(t, http.StatusOK, resp.StatusCode)

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)

// The home page should contain our new post.
require.Contains(t, string(body), "This is a test")
}

Ejecutemos la prueba:

go test ./...

¡Y todo funciona!

ok      github.com/rotemtam/ent-blog-example    0.493s
? github.com/rotemtam/ent-blog-example/ent [no test files]
? github.com/rotemtam/ent-blog-example/ent/enttest [no test files]
? github.com/rotemtam/ent-blog-example/ent/hook [no test files]
? github.com/rotemtam/ent-blog-example/ent/migrate [no test files]
? github.com/rotemtam/ent-blog-example/ent/post [no test files]
? github.com/rotemtam/ent-blog-example/ent/predicate [no test files]
? github.com/rotemtam/ent-blog-example/ent/runtime [no test files]
? github.com/rotemtam/ent-blog-example/ent/schema [no test files]
? github.com/rotemtam/ent-blog-example/ent/user [no test files]

Una prueba unitaria que pasa es genial, pero verifiquemos nuestros cambios visualmente:

¡Nuestro formulario aparece - genial! Después de enviarlo:

Nuestra nueva entrada se muestra. ¡Bien hecho!

Para finalizar

En esta publicación demostramos cómo construir una aplicación web simple con Ent y Go. Nuestra aplicación es definitivamente básica, pero cubre muchas de las bases que necesitarás al construir una aplicación: definir tu modelo de datos, gestionar el esquema de tu base de datos, escribir código de servidor, definir rutas y construir una interfaz de usuario.

Como suele pasar con el contenido introductorio, solo hemos tocado la punta del iceberg de lo que puedes hacer con Ent, pero espero que hayas probado algunas de sus características principales.

[Para más noticias y actualizaciones de Ent:]

· 3 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

En resumen

Para obtener un enlace público a la visualización de tu esquema de Ent, ejecuta:

go run -mod=mod ariga.io/entviz ./path/to/ent/schema 

Visualizando esquemas de Ent

Ent permite a los desarrolladores construir modelos de datos complejos usando semántica de grafos: en lugar de definir tablas, columnas, tablas de asociación y claves foráneas, los modelos de Ent se definen simplemente en términos de Nodos y Relaciones:

package schema

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

// User schema.
type User struct {
ent.Schema
}

// Fields of the user.
func (User) Fields() []ent.Field {
return []ent.Field{
// ...
}
}

// Edges of the user.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type),
}
}

Modelar datos de esta forma tiene muchas ventajas, como poder recorrer fácilmente el grafo de datos de una aplicación mediante una API intuitiva, generar automáticamente servidores GraphQL y mucho más.

Aunque Ent puede usar bases de datos de grafos como capa de almacenamiento, la mayoría de usuarios de Ent utilizan bases de datos relacionales comunes como MySQL, PostgreSQL o MariaDB para sus aplicaciones. En estos casos, los desarrolladores a menudo se preguntan: ¿qué esquema de base de datos real creará Ent a partir del esquema de mi aplicación?

Tanto si eres un nuevo usuario de Ent aprendiendo los fundamentos de creación de esquemas, como si eres un experto optimizando el esquema de base de datos resultante por razones de rendimiento, poder visualizar fácilmente el esquema de base de datos subyacente a tu esquema de Ent puede ser muy útil.

Presentamos el nuevo entviz

Hace año y medio compartimos una extensión de Ent llamada entviz. Esa extensión permitía a los usuarios generar documentos HTML locales simples con diagramas entidad-relación que describían el esquema de Ent de una aplicación.

Hoy nos complace compartir una herramienta súper interesante con el mismo nombre creada por Pedro Henrique (crossworth) que aborda el mismo problema desde un enfoque completamente nuevo. Con el (nuevo) entviz solo necesitas ejecutar un simple comando de Go:

go run -mod=mod ariga.io/entviz ./path/to/ent/schema 

La herramienta analizará tu esquema de Ent y creará una visualización en el Atlas Playground, generando un enlace público y compartible:

Here is a public link to your schema visualization:
https://gh.atlasgo.cloud/explore/saved/60129542154

En este enlace podrás ver tu esquema visualmente como un diagrama ERD o textualmente como un documento SQL o Atlas HCL.

Para finalizar

En esta entrada de blog hemos analizado situaciones donde puede resultarte útil obtener rápidamente una visualización del esquema de tu aplicación con Ent. También hemos mostrado cómo crear estas visualizaciones usando entviz. Si te gusta la idea, ¡nos encantaría que lo probaras hoy mismo y nos dieras tu feedback!

[Para más noticias y actualizaciones sobre Ent:]

· 8 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Cambiar el tipo de una columna en un esquema de base de datos puede parecer trivial a primera vista, pero en realidad es una operación arriesgada que puede causar problemas de compatibilidad entre el servidor y la base de datos. En esta publicación, exploraré cómo los desarrolladores pueden realizar este tipo de cambios sin causar tiempo de inactividad en su aplicación.

Recientemente, mientras trabajaba en una funcionalidad para Ariga Cloud, necesité cambiar el tipo de un campo de Ent de un blob no estructurado a un campo JSON estructurado. Cambiar el tipo de columna era necesario para poder utilizar JSON Predicates y realizar consultas más eficientes.

El esquema original se veía así en el diagrama de visualización de nuestro producto en la nube:

tutorial image 1

En nuestro caso, no podíamos simplemente copiar los datos directamente a la nueva columna, ya que los datos no son compatibles con el nuevo tipo de columna (los datos blob pueden no ser convertibles a JSON).

En el pasado, se consideraba aceptable detener el servidor, migrar el esquema de la base de datos a la siguiente versión, y solo entonces iniciar el servidor con la nueva versión compatible con el nuevo esquema de base de datos. Hoy en día, los requisitos empresariales suelen dictar que las aplicaciones deben ofrecer alta disponibilidad, lo que deja a los equipos de ingeniería el desafío de ejecutar cambios como este sin tiempo de inactividad.

El patrón común para satisfacer este tipo de requisito, como se define en "Diseño Evolutivo de Bases de Datos" de Martin Fowler, es utilizar una "fase de transición".

Una fase de transición es "un período de tiempo durante el cual la base de datos admite tanto el patrón de acceso antiguo como los nuevos simultáneamente. Esto permite que los sistemas antiguos migren a las nuevas estructuras a su propio ritmo", como ilustra este diagrama:

tutorial image 2 Crédito: martinfowler.com

Planeamos el cambio en 5 pasos simples, todos compatibles con versiones anteriores:

  • Crear una nueva columna llamada meta_json con tipo JSON.

  • Implementar una versión de la aplicación que realice escrituras duales. Cada nuevo registro o actualización se escribe tanto en la nueva columna como en la antigua, mientras que las lecturas siguen ocurriendo desde la columna antigua.

  • Migrar los datos de la columna antigua a la nueva.

  • Implementar una versión de la aplicación que lea desde la nueva columna.

  • Eliminar la columna antigua.

Migraciones versionadas

En nuestro proyecto utilizamos el flujo de migraciones versionadas de Ent para gestionar el esquema de la base de datos. Las migraciones versionadas proporcionan a los equipos un control granular sobre cómo se realizan los cambios en el esquema de la base de datos de la aplicación. Este nivel de control será muy útil para implementar nuestro plan. Si tu proyecto usa Migraciones Automáticas y quieres seguir el proceso, primero actualiza tu proyecto para usar migraciones versionadas.

nota

Lo mismo se puede hacer con migraciones automáticas utilizando la función Migraciones de Datos, sin embargo, esta publicación se centra en migraciones versionadas

Creando una columna JSON con Ent:

Primero, añadiremos un nuevo tipo JSON de Ent al esquema de usuario.

types/types.go
type Meta struct {
CreateTime time.Time `json:"create_time"`
UpdateTime time.Time `json:"update_time"`
}
ent/schema/user.go
func (User) Fields() []ent.Field {
return []ent.Field{
field.Bytes("meta"),
field.JSON("meta_json", &types.Meta{}).Optional(),
}
}

Luego, ejecutamos la generación de código para actualizar el esquema de la aplicación:

go generate ./...

A continuación, ejecutamos nuestro script de planificación automática de migraciones que genera un conjunto de archivos de migración con las sentencias SQL necesarias para actualizar la base de datos a la versión más reciente.

go run -mod=mod ent/migrate/main.go add_json_meta_column

El archivo de migración resultante que describe el cambio:

-- modify "users" table
ALTER TABLE `users` ADD COLUMN `meta_json` json NULL;

Ahora aplicaremos el archivo de migración creado usando Atlas:

atlas migrate apply \
--dir "file://ent/migrate/migrations"
--url mysql://root:pass@localhost:3306/ent

Como resultado, obtenemos el siguiente esquema en nuestra base de datos:

imagen 3 del tutorial

Comenzar a escribir en ambas columnas

Tras generar el tipo JSON, comenzaremos a escribir en la nueva columna:

-       err := client.User.Create().
- SetMeta(input.Meta).
- Exec(ctx)
+ var meta types.Meta
+ if err := json.Unmarshal(input.Meta, &meta); err != nil {
+ return nil, err
+ }
+ err := client.User.Create().
+ SetMetaJSON(&meta).
+ Exec(ctx)

Para garantizar que los valores escritos en la nueva columna meta_json se repliquen en la antigua, podemos utilizar la función Schema Hooks de Ent. Esto requiere añadir una importación en blanco ent/runtime en tu main para registrar el hook y evitar importaciones circulares:

// Hooks of the User.
func (User) Hooks() []ent.Hook {
return []ent.Hook{
hook.On(
func(next ent.Mutator) ent.Mutator {
return hook.UserFunc(func(ctx context.Context, m *gen.UserMutation) (ent.Value, error) {
meta, ok := m.MetaJSON()
if !ok {
return next.Mutate(ctx, m)
}
if b, err := json.Marshal(meta); err != nil {
return nil, err
}
m.SetMeta(b)
return next.Mutate(ctx, m)
})
},
ent.OpCreate,
),
}
}

Tras asegurar las escrituras en ambos campos, podemos desplegar en producción con seguridad.

Rellenar valores desde la columna antigua

Ahora en nuestra base de datos de producción tenemos dos columnas: una almacena el objeto meta como blob y otra como JSON. La segunda columna puede tener valores nulos ya que se añadió recientemente, por lo que debemos rellenarla con los valores de la columna antigua.

Para ello, creamos manualmente un archivo de migración SQL que rellenará la nueva columna JSON con los datos de la antigua columna blob.

nota

También puedes escribir código Go que genere este archivo de migración de datos usando el WriteDriver.

Crea un nuevo archivo de migración vacío:

atlas migrate new --dir file://ent/migrate/migrations

Para cada fila en la tabla de usuarios con valor JSON nulo (filas añadidas antes de crear la nueva columna), intentamos analizar el objeto meta como JSON válido. Si tiene éxito, rellenamos meta_json con el valor resultante; de lo contrario, lo marcamos como vacío.

Nuestro siguiente paso es editar el archivo de migración:

UPDATE users
SET meta_json = CASE
-- when meta is valid json stores it as is.
WHEN JSON_VALID(cast(meta as char)) = 1 THEN cast(cast(meta as char) as json)
-- if meta is not valid json, store it as an empty object.
ELSE JSON_SET('{}')
END
WHERE meta_json is null;

Recalcula el hash del directorio de migraciones tras modificar un archivo:

atlas migrate hash --dir "file://ent/mirate/migrations"

Podemos probar el archivo de migración ejecutando todas las migraciones anteriores en una base de datos local, poblándola con datos temporales y aplicando la última migración para verificar su funcionamiento.

Tras las pruebas, aplicamos el archivo de migración:

atlas migrate apply \
--dir "file://ent/migrate/migrations"
--url mysql://root:pass@localhost:3306/ent

Ahora realizaremos otro despliegue en producción.

Redirigir lecturas a la nueva columna y eliminar la antigua

Al tener valores en meta_json, podemos cambiar las lecturas del campo antiguo al nuevo.

En lugar de decodificar los datos desde user.meta en cada lectura, usa directamente el campo meta_json:

-       var meta types.Meta
- if err = json.Unmarshal(user.Meta, &meta); err != nil {
- return nil, err
- }
- if meta.CreateTime.Before(time.Unix(0, 0)) {
- return nil, errors.New("invalid create time")
- }
+ if user.MetaJSON.CreateTime.Before(time.Unix(0, 0)) {
+ return nil, errors.New("invalid create time")
+ }

Tras redirigir las lecturas, desplegamos los cambios en producción.

Eliminar la columna antigua

Ahora podemos eliminar el campo de la columna antigua del esquema Ent, pues ya no lo usamos.

func (User) Fields() []ent.Field {
return []ent.Field{
- field.Bytes("meta"),
field.JSON("meta_json", &types.Meta{}).Optional(),
}
}

Genera nuevamente el esquema Ent con la función Drop Column activada.

go run -mod=mod ent/migrate/main.go drop_user_meta_column

Tras crear correctamente el nuevo campo, redirigir escrituras, rellenar datos y eliminar la columna antigua, estamos listos para el despliegue final. ¡Solo queda fusionar nuestro código en control de versiones y desplegar en producción!

Para finalizar

En esta publicación hemos explicado cómo cambiar el tipo de una columna en producción sin tiempo de inactividad usando migraciones versionadas de Atlas integradas con Ent.

¿Tienes preguntas? ¿Necesitas ayuda para comenzar? Únete a nuestro Servidor de Discord de Ent.

[Para más noticias y actualizaciones de Ent:]

· 7 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

En resumen

  • La mayoría de bases de datos relacionales admiten columnas con valores JSON no estructurados.

  • Ent ofrece un excelente soporte para trabajar con valores JSON en bases de datos relacionales.

  • Cómo añadir valores a un array JSON de forma atómica.

  • Ent ha añadido recientemente soporte para añadir valores de forma atómica a campos en valores JSON.

Valores JSON en bases de datos SQL

Aunque son conocidas principalmente por almacenar datos tabulares estructurados, prácticamente todas las bases de datos relacionales modernas admiten columnas JSON para almacenar datos no estructurados en columnas de tablas. Por ejemplo, en MySQL puedes crear una tabla como:

CREATE TABLE t1 (jdoc JSON);

En esta columna, los usuarios pueden almacenar objetos JSON con esquemas arbitrarios:

INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}');

Como los documentos JSON siempre pueden expresarse como cadenas, podrían almacenarse en columnas VARCHAR o TEXT normales. Sin embargo, cuando una columna se declara con el tipo JSON, la base de datos impone la corrección sintáctica del JSON. Por ejemplo, si intentamos escribir un documento JSON incorrecto en esta tabla de MySQL:

INSERT INTO t1 VALUES('[1, 2,');

Recibiremos este error:

ERROR 3140 (22032) at line 2: Invalid JSON text:
"Invalid value." at position 6 in value (or column) '[1, 2,'.

Además, los valores almacenados dentro de documentos JSON pueden accederse en sentencias SELECT usando expresiones JSON Path, y también usarse en predicados (cláusulas WHERE) para filtrar datos:

select c->'$.hello' as greeting from t where c->'$.hello' = 'world';;

Para obtener:

+--------------+
| greeting |
+--------------+
| "world" |
+--------------+
1 row in set (0.00 sec)

Valores JSON en Ent

Con Ent, los usuarios pueden definir campos JSON en esquemas usando field.JSON pasando el nombre de campo deseado junto con el tipo Go subyacente. Por ejemplo:

type Tag struct {
Name string `json:"name"`
Created time.Time `json:"created"`
}

func (User) Fields() []ent.Field {
return []ent.Field{
field.JSON("tags", []Tag{}),
}
}

Ent proporciona una API conveniente para leer y escribir valores en columnas JSON, así como aplicar predicados (usando sqljson):

func TestEntJSON(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.Background()
// Insert a user with two comments.
client.User.Create().
SetTags([]schema.Tag{
{Name: "hello", Created: time.Now()},
{Name: "goodbye", Created: time.Now()},
}).
SaveX(ctx)

// Count how many users have more than zero tags.
count := client.User.Query().
Where(func(s *sql.Selector) {
s.Where(
sqljson.LenGT(user.FieldTags, 0),
)
}).
CountX(ctx)
fmt.Printf("count: %d", count)
// Prints: count: 1
}

Añadir valores a campos en columnas JSON

En muchos casos, es útil añadir un valor a una lista en una columna JSON. Preferiblemente, los añadidos se implementan de forma atómica, es decir, sin necesidad de leer el valor actual y escribir el nuevo valor completo. La razón es que si dos llamadas intentan añadir el valor concurrentemente, ambas leerán el mismo valor actual desde la base de datos, y escribirán su propia versión actualizada aproximadamente al mismo tiempo. A menos que se use bloqueo optimista, ambas escrituras tendrán éxito, pero el resultado final solo incluirá uno de los nuevos valores.

Para superar esta condición de carrera, podemos dejar que la base de datos gestione la sincronización entre ambas llamadas usando una consulta UPDATE inteligente. La intuición para esta solución es similar a cómo se incrementan contadores en muchas aplicaciones. En lugar de ejecutar:

SELECT points from leaderboard where user='rotemtam' 

Leer el resultado (digamos que es 1000), incrementar el valor en proceso (1000+1=1001) y escribir la nueva suma literalmente:

UPDATE leaderboard SET points=1001 where user='rotemtam'

Los desarrolladores pueden usar una consulta como:

UPDATE leaderboard SET points=points+1 where user='rotemtam'

Para evitar la necesidad de sincronizar escrituras usando bloqueo optimista u otro mecanismo, veamos cómo podemos aprovechar de manera similar la capacidad de la base de datos para realizarlas concurrentemente de forma segura.

Hay dos aspectos a considerar al construir esta consulta. Primero, la sintaxis para trabajar con valores JSON varía un poco entre diferentes proveedores de bases de datos, como verás en los ejemplos siguientes. Segundo, una consulta para añadir un valor a una lista en un documento JSON debe manejar al menos dos casos límite:

  1. El campo al que queremos añadir aún no existe en el documento JSON.

  2. El campo existe pero está establecido como JSON null.

Este sería un ejemplo de consulta para añadir un valor new_val a un campo llamado a en una columna c de la tabla t en diferentes dialectos:

UPDATE `t` SET `c` = CASE
WHEN
(JSON_TYPE(JSON_EXTRACT(`c`, '$.a')) IS NULL
OR JSON_TYPE(JSON_EXTRACT(`c`, '$.a')) = 'NULL')
THEN
JSON_SET(`c`, '$.a', JSON_ARRAY('new_val'))
ELSE
JSON_ARRAY_APPEND(`c`, '$.a', 'new_val')
END

Añadir valores a campos JSON con Ent

Ent ha añadido recientemente soporte para añadir valores atómicamente a campos en columnas JSON. Veamos cómo se puede utilizar.

Si el tipo subyacente del campo JSON es un slice, como:

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.JSON("tags", []string{}),
}
}

Ent generará un método AppendTags en los constructores de mutación de actualización. Puedes usarlo así:

func TestAppend(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.Background()
// Insert a user with two tags.
u := client.User.Create().
SetTags([]string{"hello", "world"}).
SaveX(ctx)

u.Update().AppendTags([]string{"goodbye"}).ExecX(ctx)

again := client.User.GetX(ctx, u.ID)
fmt.Println(again.Tags)
// Prints: [hello world goodbye]
}

Si el tipo subyacente del campo JSON es una estructura que contiene una lista, como:

type Meta struct {
Tags []string `json:"tags"'`
}

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.JSON("meta", &Meta{}),
}
}

Puedes usar la opción sql/modifier para que Ent genere el método Modify, que se usa de esta manera:

func TestAppendSubfield(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.Background()
// Insert a user with two tags.
u := client.User.Create().
SetMeta(&schema.Meta{
Tags: []string{"hello", "world"},
}).
SaveX(ctx)

u.Update().
Modify(func(u *sql.UpdateBuilder) {
sqljson.Append(u, user.FieldMeta, []string{"goodbye"}, sqljson.Path("tags"))
}).
ExecX(ctx)

again := client.User.GetX(ctx, u.ID)
fmt.Println(again.Meta.Tags)
// Prints: [hello world goodbye]
}

Para finalizar

En este artículo hemos hablado sobre campos JSON en SQL y Ent en general. Después, hemos explicado cómo añadir valores a un campo JSON de forma atómica en bases de datos SQL populares. Finalmente, hemos mostrado cómo hacerlo usando Ent. ¿Crees que son necesarias operaciones de Eliminar/Segmentar? ¡Dinos tu opinión!

[Para más noticias y actualizaciones de Ent:]

· 8 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Para garantizar la calidad de su software, los equipos suelen implementar flujos de trabajo de Integración Continua, comúnmente conocidos como CI. Con CI, los equipos ejecutan continuamente un conjunto de verificaciones automatizadas en cada cambio del código base. Durante la CI, los equipos pueden ejecutar varios tipos de verificaciones:

  • Compilación o construcción de la versión más reciente para asegurar que no está dañada.

  • Linting para hacer cumplir los estándares de estilo de código aceptados.

  • Pruebas unitarias que verifican que los componentes individuales funcionan según lo previsto y que los cambios en el código no provocan regresiones en otras áreas.

  • Escaneos de seguridad para asegurar que no se introducen vulnerabilidades conocidas en el código.

  • ¡Y mucho más!

De nuestras conversaciones con la comunidad de Ent, hemos aprendido que muchos equipos que usan Ent ya utilizan CI y les gustaría incorporar algunas verificaciones específicas de Ent en sus flujos de trabajo.

Para apoyar a la comunidad en este esfuerzo, añadimos una nueva guía a la documentación que recoge las mejores prácticas comunes para verificar en CI y presenta ent/contrib/ci: una GitHub Action que mantenemos y que las codifica.

En esta publicación, quiero compartir algunas de nuestras sugerencias iniciales sobre cómo podrías incorporar CI a tu proyecto Ent. Hacia el final de este artículo, compartiré detalles sobre proyectos en los que estamos trabajando y para los que nos gustaría recibir comentarios de la comunidad.

Verificar que todos los archivos generados están incluidos en el control de versiones

Ent depende en gran medida de la generación de código. Según nuestra experiencia, el código generado siempre debe incluirse en el control de código fuente. Esto se hace por dos razones:

  • Si el código generado está en el control de código fuente, puede leerse junto con el código principal de la aplicación. Tener el código generado presente durante las revisiones de código o al navegar por el repositorio es esencial para obtener una imagen completa de cómo funcionan las cosas.

  • Las diferencias entre los entornos de desarrollo de los miembros del equipo pueden detectarse y solucionarse fácilmente. Esto reduce aún más la posibilidad de problemas del tipo "funciona en mi máquina", ya que todos ejecutan el mismo código.

Si usas GitHub para el control de código fuente, es fácil verificar que todos los archivos generados están incluidos con la GitHub Action ent/contrib/ci. De lo contrario, proporcionamos un sencillo script bash que puedes integrar en tu flujo de CI existente.

Simply add a file named .github/workflows/ent-ci.yaml in your repository:

name: EntCI
on:
push:
# Run whenever code is changed in the master.
branches:
- master
# Run on PRs where something changed under the `ent/` directory.
pull_request:
paths:
- 'ent/*'
jobs:
ent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.1
- uses: actions/setup-go@v3
with:
go-version: 1.18
- uses: ent/contrib/ci@master

Linting de archivos de migración

Los cambios en el esquema Ent de tu proyecto casi siempre resultan en una modificación de tu base de datos. Si usas Migraciones Versionadas para gestionar los cambios en el esquema de tu base de datos, puedes ejecutar linting de migraciones como parte de tu flujo de integración continua. Esto se hace por varias razones:

  • El linting reproduce tu directorio de migraciones en un contenedor de base de datos para asegurar que todas las sentencias SQL son válidas y están en el orden correcto.

  • Se aplica la integridad del directorio de migraciones, asegurando que el historial no se modificó accidentalmente y que las migraciones planificadas en paralelo se unifican en un historial lineal limpio.

  • Se detectan cambios destructivos, alertándote de cualquier posible pérdida de datos que puedan causar tus migraciones mucho antes de que lleguen a tu base de datos de producción.

  • El linting detecta cambios dependientes de datos que podrían fallar durante el despliegue y requieren una revisión más cuidadosa por tu parte.

Si estás utilizando GitHub, puedes emplear la Acción Oficial de Atlas para ejecutar el análisis de migraciones durante la integración continua.

Añade un archivo .github/workflows/atlas-ci.yaml a tu repositorio con este contenido:

name: Atlas CI
on:
# Run whenever code is changed in the master branch,
# change this to your root branch.
push:
branches:
- master
# Run on PRs where something changed under the `ent/migrate/migrations/` directory.
pull_request:
paths:
- 'ent/migrate/migrations/*'
jobs:
lint:
services:
# Spin up a mysql:8.0.29 container to be used as the dev-database for analysis.
mysql:
image: mysql:8.0.29
env:
MYSQL_ROOT_PASSWORD: pass
MYSQL_DATABASE: test
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping -ppass"
--health-interval 10s
--health-start-period 10s
--health-timeout 5s
--health-retries 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.1
with:
fetch-depth: 0 # Mandatory unless "latest" is set below.
- uses: ariga/atlas-action@v0
with:
dir: ent/migrate/migrations
dir-format: golang-migrate # Or: atlas, goose, dbmate
dev-url: mysql://root:pass@localhost:3306/test

Observa que ejecutar atlas migrate lint requiere una base de datos de desarrollo limpia que se provee mediante el bloque services en el código de ejemplo anterior.

Qué viene para la CI de Ent

Para complementar este comienzo modesto, quiero compartir algunas funcionalidades que estamos probando en Ariga con la esperanza de recibir comentarios de la comunidad.

  • Análisis para Migraciones Online - muchos proyectos de Ent utilizan el mecanismo automático de migración de esquemas disponible en Ent (usando ent.Schema.Create al iniciar las aplicaciones). Asumiendo que el código fuente del proyecto está gestionado en un sistema de control de versiones (como Git), comparamos el esquema en la rama principal (master/main/etc.) con el de la rama de características actual y usamos la capacidad de comparación de esquemas de Atlas para calcular las sentencias SQL que se ejecutarán contra la base de datos. Luego podemos utilizar la capacidad de análisis de Atlas para proporcionar información sobre posibles riesgos derivados del cambio propuesto.

  • Visualización de cambios - para ayudar a los revisores a comprender el impacto de los cambios propuestos en una pull request específica, generamos un diff visual (usando un diagrama ER similar a entviz) que refleja las modificaciones en el esquema del proyecto.

  • Análisis de Esquemas - utilizando el paquete oficial go/analysis para crear analizadores que examinen el código Go del esquema de Ent y apliquen políticas (como convenciones de nomenclatura o indexación) a nivel de definición del esquema.

Para finalizar

En este artículo hemos presentado el concepto de CI y discutido formas de implementarlo en proyectos de Ent. Además, hemos mostrado verificaciones de CI que estamos probando internamente. Si te gustaría que estas verificaciones formen parte de Ent o tienes otras ideas para proporcionar herramientas de CI para Ent, contáctanos en el Servidor de Discord de Ent.

[Para más noticias y actualizaciones de Ent:]

· 9 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Hace cinco semanas lanzamos una característica muy esperada para gestionar cambios en bases de datos con Ent: Migraciones Versionadas. En el artículo de anuncio hicimos una breve introducción a los enfoques declarativo y basado en cambios para mantener los esquemas de bases de datos sincronizados con las aplicaciones que los consumen, así como sus inconvenientes y por qué el enfoque de Atlas (el motor de migraciones subyacente de Ent) para combinar lo mejor de ambos mundos en un flujo de trabajo merece la pena. Lo llamamos Creación de Migraciones Versionadas y si aún no lo has leído, ¡es buen momento para hacerlo!

Con la creación de migraciones versionadas, los archivos de migración resultantes siguen siendo "basados en cambios", pero han sido planificados de forma segura por el motor de Atlas. Esto significa que puedes seguir usando tu herramienta de gestión de migraciones favorita, como Flyway, Liquibase, golang-migrate/migrate o pressly/goose cuando desarrolles servicios con Ent.

En este artículo quiero mostrarte otra nueva característica del proyecto Atlas que llamamos Archivo de Integridad del Directorio de Migraciones, que ahora es compatible con Ent, y cómo puedes usarla con cualquiera de las herramientas de gestión de migraciones que ya conoces y te gustan.

El problema

Al usar migraciones versionadas, los desarrolladores deben evitar lo siguiente para no dañar la base de datos:

  1. Modificar retroactivamente migraciones ya ejecutadas.

  2. Cambiar accidentalmente el orden de las migraciones.

  3. Confirmar scripts SQL semánticamente incorrectos. Teóricamente, la revisión de código debería proteger a los equipos de fusionar migraciones con estos problemas. Sin embargo, en mi experiencia, muchos errores pueden pasar desapercibidos para el ojo humano, haciendo este enfoque propenso a fallos. Por ello, una forma automatizada de prevenir estos errores es mucho más segura.

El primer problema (modificar el historial) lo abordan la mayoría de herramientas guardando un hash del archivo de migración aplicado en la base de datos gestionada y comparándolo con los archivos locales. Si no coinciden, se puede abortar la migración. Sin embargo, esto ocurre muy tarde en el ciclo de desarrollo (durante el despliegue), y detectarlo antes ahorraría tiempo y recursos.

Para el segundo (y tercer) problema, considera este escenario:

Diagrama de migraciones versionadas sin conflicto

Este diagrama muestra dos errores que pasan desapercibidos. El primero es el orden de los archivos de migración.

El Equipo A y el Equipo B crean ramas para nuevas funcionalidades aproximadamente al mismo tiempo. El Equipo B genera un archivo de migración con marca de tiempo de versión x y sigue trabajando. El Equipo A genera un archivo de migración más tarde, con marca de tiempo x+1. El Equipo A completa su funcionalidad y la fusiona en master, desplegándola posiblemente en producción con la migración x+1 aplicada. Hasta aquí, todo correcto.

Ahora el Equipo B fusiona su funcionalidad con la migración versión x, que es anterior a la versión x+1 ya aplicada. Si la revisión de código no detecta esto, el archivo de migración llega a producción, y dependerá de la herramienta de gestión de migraciones específica decidir qué ocurre.

La mayoría de herramientas tienen su propia solución para este problema. Por ejemplo, pressly/goose adopta un enfoque que denominan versionado híbrido. Antes de presentarles la solución única de Atlas (Ent) para este problema, echemos un vistazo rápido al tercer punto:

Si tanto el Equipo A como el Equipo B desarrollan una funcionalidad que requiere nuevas tablas o columnas, y les dan el mismo nombre (por ejemplo users), ambos podrían generar una sentencia para crear esa tabla. Mientras que el equipo que fusiona primero tendrá una migración exitosa, la migración del segundo equipo fallará porque la tabla o columna ya existe.

La solución

Atlas tiene un enfoque único para abordar los problemas anteriores. El objetivo es detectar estos problemas lo antes posible. En nuestra opinión, el mejor lugar para hacerlo es en el control de versiones y en las fases de integración continua (CI) de un producto. La solución de Atlas es la introducción de un nuevo archivo que llamamos Archivo de Integridad del Directorio de Migraciones. Se trata simplemente de otro archivo llamado atlas.sum que se almacena junto con los archivos de migración y contiene metadatos sobre el directorio de migraciones. Su formato está inspirado en el archivo go.sum de un módulo de Go, y tendría un aspecto similar a este:

h1:KRFsSi68ZOarsQAJZ1mfSiMSkIOZlMq4RzyF//Pwf8A=
20220318104614_team_A.sql h1:EGknG5Y6GQYrc4W8e/r3S61Aqx2p+NmQyVz/2m8ZNwA=

El archivo atlas.sum contiene un resumen (sum) de todo el directorio como primera entrada, y un checksum para cada uno de los archivos de migración (implementado mediante un árbol de hash Merkle inverso de una rama). Veamos cómo podemos usar este archivo para detectar los casos anteriores en el control de versiones y CI. Nuestro objetivo es alertar sobre el hecho de que ambos equipos han añadido migraciones y que probablemente deban verificarse antes de proceder a la fusión.

:::nota Para seguir el ejemplo, ejecuta los siguientes comandos y crea rápidamente un caso práctico:

  1. Crea un módulo de Go y descarga todas las dependencias necesarias
  2. Crea un esquema básico de Usuario
  3. Habilita la función de migraciones versionadas
  4. Ejecuta la generación de código
  5. Inicia un contenedor Docker de MySQL (elimínalo con docker stop atlas-sum)
mkdir ent-sum-file
cd ent-sum-file
go mod init ent-sum-file
go install entgo.io/ent/cmd/ent@master
go run entgo.io/ent/cmd/ent new User
sed -i -E 's|^//go(.*)$|//go\1 --feature sql/versioned-migration|' ent/generate.go
go generate ./...
docker run --rm --name atlas-sum --detach --env MYSQL_ROOT_PASSWORD=pass --env MYSQL_DATABASE=ent -p 3306:3306 mysql

:::

El primer paso es indicar al motor de migraciones que cree y gestione el archivo atlas.sum usando la opción schema.WithSumFile(). El siguiente ejemplo utiliza un cliente Ent instanciado para generar nuevos archivos de migración:

package main

import (
"context"
"log"
"os"

"ent-sum-file/ent"

"ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect/sql/schema"
_ "github.com/go-sql-driver/mysql"
)

func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/ent")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Create a local migration directory.
dir, err := migrate.NewLocalDir("migrations")
if err != nil {
log.Fatalf("failed creating atlas migration directory: %v", err)
}
// Write migration diff.
err = client.Schema.NamedDiff(ctx, os.Args[1], schema.WithDir(dir), schema.WithSumFile())
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}

Tras crear un directorio de migraciones y ejecutar los comandos anteriores, deberías ver archivos de migración compatibles con golang-migrate/migrate y, además, el archivo atlas.sum con el siguiente contenido:

mkdir migrations
go run -mod=mod main.go initial
20220504114411_initial.up.sql
-- create "users" table
CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;

20220504114411_initial.down.sql
-- reverse: create "users" table
DROP TABLE `users`;

atlas.sum
h1:SxbWjP6gufiBpBjOVtFXgXy7q3pq1X11XYUxvT4ErxM=
20220504114411_initial.down.sql h1:OllnelRaqecTrPbd2YpDbBEymCpY/l6ihbyd/tVDgeY=
20220504114411_initial.up.sql h1:o/6yOczGSNYQLlvALEU9lK2/L6/ws65FrHJkEk/tjBk=

Como puedes observar, el archivo atlas.sum contiene una entrada por cada archivo de migración generado. Con la generación del archivo atlas.sum habilitada, tanto el Equipo A como el Equipo B tendrán este archivo cuando generen migraciones para un cambio en el esquema. Ahora el control de versiones mostrará un conflicto de fusión cuando el segundo equipo intente fusionar su funcionalidad.

Migraciones versionadas de Atlas sin conflicto

:::nota En los siguientes pasos invocamos la CLI de Atlas mediante go run -mod=mod ariga.io/atlas/cmd/atlas, pero también puedes instalar la CLI globalmente en tu sistema (y luego invocarla simplemente con atlas) siguiendo las instrucciones de instalación aquí. :::

Puedes verificar en cualquier momento si tu archivo atlas.sum está sincronizado con el directorio de migraciones usando el siguiente comando (que no debería mostrar errores en este momento):

go run -mod=mod ariga.io/atlas/cmd/atlas migrate validate

Sin embargo, si realizas cambios manuales en tus archivos de migración, como añadir una nueva sentencia SQL, editar una existente o incluso crear un archivo completamente nuevo, el archivo atlas.sum dejará de estar sincronizado con el contenido del directorio de migraciones. Al intentar generar nuevos archivos de migración para un cambio de esquema, el motor de migraciones de Atlas bloqueará la operación. Pruébalo creando un nuevo archivo de migración vacío y ejecutando nuevamente el main.go:

go run -mod=mod ariga.io/atlas/cmd/atlas migrate new migrations/manual_version.sql --format golang-migrate
go run -mod=mod main.go initial
# 2022/05/04 15:08:09 failed creating schema resources: validating migration directory: checksum mismatch
# exit status 1

El comando atlas migrate validate mostrará el mismo problema:

go run -mod=mod ariga.io/atlas/cmd/atlas migrate validate
# Error: checksum mismatch
#
# You have a checksum error in your migration directory.
# This happens if you manually create or edit a migration file.
# Please check your migration files and run
#
# 'atlas migrate hash --force'
#
# to re-hash the contents and resolve the error.
#
# exit status 1

Para volver a sincronizar el archivo atlas.sum con el directorio de migraciones, podemos usar nuevamente la CLI de Atlas:

go run -mod=mod ariga.io/atlas/cmd/atlas migrate hash --force

Como medida de seguridad, la CLI de Atlas no opera sobre un directorio de migraciones que no esté sincronizado con su archivo atlas.sum. Por lo tanto, necesitarás añadir el flag --force al comando.

Para casos donde un desarrollador olvide actualizar el archivo atlas.sum después de hacer cambios manuales, puedes añadir una llamada atlas migrate validate a tu CI. Estamos trabajando activamente en una acción de GitHub y solución de CI que hace esto (entre otras cosas) por ti listo para usar.

Conclusión

En este post, hemos introducido brevemente fuentes comunes de problemas en migraciones de esquemas cuando se trabaja con archivos SQL basados en cambios, y presentamos una solución basada en el proyecto Atlas para hacer las migraciones más seguras.

¿Tienes preguntas? ¿Necesitas ayuda para comenzar? Únete a nuestro Servidor de Discord de Ent.

[Para más noticias y actualizaciones de Ent:]

· 6 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

La función de "Botón de Edición" de Twitter llegó a los titulares con el tuit de encuesta de Elon Musk preguntando si los usuarios querían esta función o no.

Tuit de Elon

Sin duda, esta es una de las funciones más solicitadas de Twitter.

Como desarrollador de software, inmediatamente empecé a pensar cómo implementaría esto yo mismo. El problema de seguimiento/auditoría es muy común en muchas aplicaciones. Si tienes una entidad (por ejemplo, un Tweet) y quieres rastrear cambios en uno de sus campos (como el campo content), existen muchas soluciones comunes. Algunas bases de datos incluso tienen soluciones propietarias como el seguimiento de cambios de Microsoft o las Tablas con Versiones de Sistema de MariaDB. Sin embargo, en la mayoría de casos tendrías que "armarlo" tú mismo. Afortunadamente, Ent ofrece un sistema modular de extensiones que te permite integrar funciones como esta con solo unas líneas de código.

Twitter + Botón de Edición

if only

Introducción a Ent

Ent es un framework de entidades para Go que simplifica enormemente el desarrollo de aplicaciones grandes. Ent incluye características impresionantes desde el primer momento, como:

Con el motor de generación de código de Ent y su avanzado sistema de extensiones, puedes modularizar fácilmente tu cliente de Ent con funciones avanzadas que normalmente requieren mucho tiempo para implementar manualmente. Por ejemplo:

Enthistory

enthistory es una extensión que comenzamos a desarrollar cuando quisimos añadir un panel de "Actividad e Historial" a uno de nuestros servicios web. La función del panel es mostrar quién cambió qué y cuándo (auditoría). En Atlas, una herramienta para gestionar bases de datos mediante archivos HCL declarativos, tenemos una entidad llamada "schema" que es esencialmente un gran bloque de texto. Cualquier cambio en el esquema se registra y puede verse después en el panel de "Actividad e Historial".

Actividad e Historial

The "Activity & History" screen in Atlas

Esta función es muy común y aparece en muchas aplicaciones como Google Docs, las PRs de GitHub y las publicaciones de Facebook, pero desafortunadamente falta en el muy popular y querido Twitter.

Más de 3 millones de personas votaron a favor de añadir el "botón de edición" a Twitter, ¡así que permíteme mostrar cómo Twitter puede hacer felices a sus usuarios sin sudar la gota gorda!

Con Enthistory, solo tienes que anotar tu esquema de Ent así de simple:

func (Tweet) Fields() []ent.Field {
return []ent.Field{
field.String("content").
Annotations(enthistory.TrackField()),
field.Time("created").
Default(time.Now),
}
}

Enthistory se integra en tu cliente de Ent para garantizar que cada operación CRUD en "Tweet" quede registrada en la tabla "tweets_history", sin modificar código, y ofrece una API para consultar estos registros:

// Creating a new Tweet doesn't change. enthistory automatically modifies
// your transaction on the fly to record this event in the history table
client.Tweet.Create().SetContent("hello world!").SaveX(ctx)

// Querying history changes is as easy as querying any other entity's edge.
t, _ := client.Tweet.Get(ctx, id)
hs := client.Tweet.QueryHistory(t).WithChanges().AllX(ctx)

Veamos qué tendrías que hacer si no usaras Enthistory: Por ejemplo, imagina una app similar a Twitter. Tiene una tabla llamada "tweets" y una de sus columnas es el contenido del tuit.

idcontentcreated_atauthor_id
1Hello Twitter!2022-04-06T13:45:34+00:00123
2Hello Gophers!2022-04-06T14:03:54+00:00456

Ahora, supongamos que queremos permitir a los usuarios editar el contenido y mostrar simultáneamente los cambios en el frontend. Existen varios enfoques comunes para resolver este problema, cada uno con sus pros y contras (los analizaremos en otro post técnico). Por ahora, una solución posible sería crear una tabla "tweets_history" que registre los cambios de un tuit:

idtweet_idtimestampeventcontent
112022-04-06T12:30:00+00:00CREATEDhello world!
222022-04-06T13:45:34+00:00UPDATEDhello Twitter!

Con una tabla como la anterior, podemos registrar cambios en el tuit original "1". Si se solicita, podríamos mostrar que se publicó originalmente a las 12:30:00 con el contenido "¡hola mundo!" y se modificó a las 13:45:34 a "¡hola Twitter!".

Para implementar esto, tendríamos que modificar cada sentencia UPDATE de "tweets" para incluir un INSERT en "tweets_history". Para garantizar la corrección, deberíamos envolver ambas operaciones en una transacción, evitando así corromper el historial si la primera operación tiene éxito pero la posterior falla. También necesitaríamos asegurar que cada INSERT en "tweets" vaya acompañado de un INSERT en "tweets_history".

# INSERT is logged as "CREATE" history event
- INSERT INTO tweets (`content`) VALUES ('Hello World!');
+BEGIN;
+INSERT INTO tweets (`content`) VALUES ('Hello World!');
+INSERT INTO tweets_history (`content`, `timestamp`, `record_id`, `event`)
+VALUES ('Hello World!', NOW(), 1, 'CREATE');
+COMMIT;

# UPDATE is logged as "UPDATE" history event
- UPDATE tweets SET `content` = 'Hello World!' WHERE id = 1;
+BEGIN;
+UPDATE tweets SET `content` = 'Hello World!' WHERE id = 1;
+INSERT INTO tweets_history (`content`, `timestamp`, `record_id`, `event`)
+VALUES ('Hello World!', NOW(), 1, 'UPDATE');
+COMMIT;

Este método funciona, pero tendrías que crear otra tabla para cada entidad diferente ("comment_history", "settings_history"). Para evitarlo, Enthistory crea una única tabla "history" y otra "changes" donde registra todos los campos rastreados. Además, soporta múltiples tipos de campos sin necesidad de añadir columnas adicionales.

Versión preliminar

Enthistory aún está en fase de diseño inicial y se está probando internamente. Por tanto, todavía no lo hemos publicado como código abierto, aunque planeamos hacerlo muy pronto. Si quieres probar una versión preliminar de Enthistory, he desarrollado una aplicación simple en React con GraphQL+Enthistory para demostrar cómo podría funcionar la edición de tuits. Puedes verla aquí. ¡Te invito a compartir tus comentarios!

Para finalizar

Hemos visto cómo el sistema modular de extensiones de Ent te permite implementar funcionalidades avanzadas como si fueran un simple paquete instalable. Desarrollar tu propia extensión es divertido, sencillo y educativo! ¡Te animo a intentarlo! En el futuro, Enthistory permitirá rastrear cambios en relaciones (tablas con claves foráneas), integrarse con extensiones OpenAPI y GraphQL, y ofrecerá más métodos para su implementación subyacente.

[Para más noticias y actualizaciones de Ent:]

· 4 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Ya anunciamos anteriormente el nuevo motor de migraciones de Ent: Atlas.
Gracias a Atlas, es más fácil que nunca añadir soporte para nuevas bases de datos en Ent.
Hoy me complace anunciar que ya está disponible la compatibilidad en vista previa con TiDB, usando la última versión de Ent con Atlas activado.

Ent permite acceder a datos en múltiples tipos de bases de datos, tanto orientadas a grafos como relacionales.
Habitualmente, los usuarios emplean bases de datos relacionales estándar de código abierto como MySQL, MariaDB y PostgreSQL.
Cuando los equipos que desarrollan aplicaciones basadas en Ent crecen y necesitan manejar tráfico a mayor escala, estas bases de datos de nodo único suelen convertirse en un cuello de botella para escalar.
Por este motivo, muchos miembros de la comunidad de Ent han solicitado soporte para bases de datos NewSQL como TiDB.

TiDB

TiDB es una base de datos NewSQL de código abierto. Ofrece características que las bases de datos tradicionales no tienen, como:

  1. Escalado horizontal: Durante años, los arquitectos de software tuvieron que elegir entre las garantías de las bases relacionales y la escalabilidad horizontal de las bases NoSQL (como MongoDB o Cassandra). TiDB permite escalado horizontal manteniendo alta compatibilidad con funciones de MySQL.

  2. HTAP (Procesamiento Híbrido Transaccional/Analítico): Tradicionalmente, las bases de datos se dividían en analíticas (OLAP) y transaccionales (OLTP). TiDB rompe esta dicotomía permitiendo ambas cargas de trabajo en la misma base.

  3. Monitorización preconfigurada con Prometheus+Grafana: TiDB está construido con paradigmas cloud-native desde sus cimientos y soporta nativamente la pila de observabilidad estándar de CNCF.

Para más información, consulta la Introducción oficial a TiDB.

Hola Mundo con TiDB

Para una aplicación rápida "Hola Mundo" con Ent+TiDB, sigue estos pasos:

  1. Levanta un servidor TiDB local con Docker:
 docker run -p 4000:4000 pingcap/tidb

Ahora deberías tener una instancia de TiDB funcionando en el puerto 4000.

  1. Clona el repositorio de ejemplo hello world:
 git clone https://github.com/hedwigz/tidb-hello-world.git

En este repositorio hemos definido un esquema simple User:

ent/schema/user.go
 func (User) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now),
field.String("name"),
field.Int("age"),
}
}

Luego conectamos Ent con TiDB:

main.go
 client, err := ent.Open("mysql", "root@tcp(localhost:4000)/test?parseTime=true")
if err != nil {
log.Fatalf("failed opening connection to tidb: %v", err)
}
defer client.Close()
// Run the auto migration tool, with Atlas.
if err := client.Schema.Create(context.Background(), schema.WithAtlas(true)); err != nil {
log.Fatalf("failed printing schema changes: %v", err)
}
```

Observa que en la línea `1` nos conectamos usando el dialecto `mysql`, posible porque TiDB es [compatible con MySQL](https://docs.pingcap.com/tidb/stable/mysql-compatibility) y no requiere drivers especiales.
Existen diferencias entre TiDB y MySQL, especialmente en migraciones de esquemas como inspección del information_schema y planificación. Por esto, `Atlas` detecta automáticamente si está conectado a `TiDB` y adapta las migraciones.
Además, en la línea `7` usamos `schema.WithAtlas(true)`, que activa `Atlas` como motor de migraciones de Ent.

Finalmente, creamos un usuario y guardamos el registro en TiDB para luego consultarlo e imprimirlo.

```go title="main.go"
client.User.Create().
SetAge(30).
SetName("hedwigz").
SaveX(context.Background())
user := client.User.Query().FirstX(context.Background())
fmt.Printf("the user: %s is %d years old\n", user.Name, user.Age)
  1. Ejecuta el programa de ejemplo:
 $ go run main.go
the user: hedwigz is 30 years old

¡Woohoo! En este rápido tutorial hemos logrado:

  • Levantar una instancia local de TiDB.

  • Conectar Ent con TiDB.

  • Migrar nuestro esquema de Ent con Atlas.

  • Insertar y consultar datos en TiDB usando Ent.

Soporte en vista previa

La integración de Atlas con TiDB está bien probada con la versión v5.4.0 de TiDB (la latest en el momento de redactar) y ampliaremos este soporte en el futuro. Si usas otras versiones de TiDB o necesitas ayuda, no dudes en abrir un issue o unirte a nuestro canal de Discord.

[Para más noticias y actualizaciones de Ent:]