Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Han pasado casi 4 meses desde nuestro último lanzamiento, y por buenos motivos. La versión 0.9.0, lanzada hoy, incluye características muy esperadas. Quizás la más destacada de la lista sea una función que lleva en discusión más de año y medio y que fue una de las más solicitadas en la Encuesta de Usuarios de Ent: ¡la API de Upsert!
La versión 0.9.0 añade soporte para sentencias tipo "Upsert" usando una nueva bandera de características: sql/upsert.
Ent tiene una colección de banderas de características que pueden activarse para añadir funcionalidades adicionales
al código generado por Ent. Esto sirve tanto como mecanismo para activar opcionalmente características no necesariamente
deseadas en todos los proyectos, como para experimentar con funcionalidades que podrían integrarse en el núcleo de Ent en el futuro.
En esta publicación, presentaremos la nueva función, sus casos de uso y demostraremos cómo utilizarla.
Upsert
"Upsert" es un término común en sistemas de datos que combina "update" (actualizar) e "insert" (insertar). Normalmente se refiere a
una sentencia que intenta insertar un registro en una tabla y, si se viola una restricción de unicidad (por ejemplo, ya existe un registro
con ese ID), actualiza dicho registro en su lugar. Aunque ninguna base de datos relacional popular tiene una sentencia UPSERT específica,
la mayoría admiten formas de lograr este comportamiento.
Por ejemplo, supongamos que tenemos una tabla con esta definición en una base de datos SQLite:
CREATE TABLE users (
id integer PRIMARY KEY AUTOINCREMENT,
email varchar(255) UNIQUE,
name varchar(255)
)
Si intentamos ejecutar la misma inserción dos veces:
INSERT INTO users (email, name) VALUES ('rotem@entgo.io', 'Rotem Tamir');
INSERT INTO users (email, name) VALUES ('rotem@entgo.io', 'Rotem Tamir');
Obtenemos este error:
[2021-08-05 06:49:22] UNIQUE constraint failed: users.email
En muchos casos, es útil que las operaciones de escritura sean idempotentes, lo que significa que podemos ejecutarlas múltiples veces consecutivas dejando el sistema en el mismo estado.
En otros escenarios, no es deseable consultar si existe un registro antes de intentar crearlo. Para estas situaciones,
SQLite admite la cláusula ON CONFLICT en sentencias INSERT.
Para indicar a SQLite que sobrescriba un valor existente con el nuevo, podemos ejecutar:
INSERT INTO users (email, name) values ('rotem@entgo.io', 'Tamir, Rotem')
ON CONFLICT (email) DO UPDATE SET email=excluded.email, name=excluded.name;
Si preferimos mantener los valores existentes, podemos usar la acción de conflicto DO NOTHING:
INSERT INTO users (email, name) values ('rotem@entgo.io', 'Tamir, Rotem')
ON CONFLICT DO NOTHING;
A veces queremos fusionar ambas versiones. Podemos usar la acción DO UPDATE de forma ligeramente diferente para
lograr algo como:
INSERT INTO users (email, full_name) values ('rotem@entgo.io', 'Tamir, Rotem')
ON CONFLICT (email) DO UPDATE SET name=excluded.name || ' (formerly: ' || users.name || ')'
En este caso, tras nuestro segundo INSERT, el valor de la columna name sería: Tamir, Rotem (formerly: Rotem Tamir).
No es muy útil, pero esperamos que veas que así puedes hacer cosas interesantes.
Upsert con Ent
Supongamos que tenemos un proyecto Ent existente con una entidad similar a la tabla users descrita anteriormente:
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("email").
Unique(),
field.String("name"),
}
}
Como la API de Upsert es una función recién lanzada, asegúrate de actualizar tu versión de ent con:
go get -u entgo.io/ent@v0.9.0
Luego, añade la bandera sql/upsert a tus flags de generación de código, en ent/generate.go:
package ent
//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/upsert ./schema
A continuación, vuelve a ejecutar la generación de código para tu proyecto:
go generate ./...
Observa que se ha añadido un nuevo método llamado OnConflict al archivo ent/user_create.go:
// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
// of the `INSERT` statement. For example:
//
// client.User.Create().
// SetEmailAddress(v).
// OnConflict(
// // Update the row with the new values
// // the was proposed for insertion.
// sql.ResolveWithNewValues(),
// ).
// // Override some of the fields with custom
// // update values.
// Update(func(u *ent.UserUpsert) {
// SetEmailAddress(v+v)
// }).
// Exec(ctx)
//
func (uc *UserCreate) OnConflict(opts ...sql.ConflictOption) *UserUpsertOne {
uc.conflict = opts
return &UserUpsertOne{
create: uc,
}
}
Este método (junto con otro nuevo código generado) nos permitirá lograr el comportamiento de upsert para nuestra entidad User.
Para explorarlo, empecemos escribiendo una prueba que reproduzca el error de restricción de unicidad:
func TestUniqueConstraintFails(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.TODO()
// Create the user for the first time.
client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Rotem Tamir").
SaveX(ctx)
// Try to create a user with the same email the second time.
_, err := client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Rotem Tamir").
Save(ctx)
if !ent.IsConstraintError(err) {
log.Fatalf("expected second created to fail with constraint error")
}
log.Printf("second query failed with: %v", err)
}
La prueba pasa:
=== RUN TestUniqueConstraintFails
2021/08/05 07:12:11 second query failed with: ent: constraint failed: insert node to table "users": UNIQUE constraint failed: users.email
--- PASS: TestUniqueConstraintFails (0.00s)
Ahora veamos cómo indicar a Ent que sobrescriba los valores existentes con los nuevos en caso de conflicto:
func TestUpsertReplace(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.TODO()
// Create the user for the first time.
orig := client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Rotem Tamir").
SaveX(ctx)
// Try to create a user with the same email the second time.
// This time we set ON CONFLICT behavior, and use the `UpdateNewValues`
// modifier.
newID := client.User.Create().
SetEmail("rotem@entgo.io").
SetName("Tamir, Rotem").
OnConflict().
UpdateNewValues().
// we use the IDX method to receive the ID
// of the created/updated entity
IDX(ctx)
// We expect the ID of the originally created user to be the same as
// the one that was just updated.
if orig.ID != newID {
log.Fatalf("expected upsert to update an existing record")
}
current := client.User.GetX(ctx, orig.ID)
if current.Name != "Tamir, Rotem" {
log.Fatalf("expected upsert to replace with the new values")
}
}
Ejecutando nuestra prueba:
=== RUN TestUpsertReplace
--- PASS: TestUpsertReplace (0.00s)
Alternativamente, podemos usar el modificador Ignore para indicar a Ent que mantenga la versión anterior al resolver el conflicto.
Escribamos una prueba que demuestre esto:
func TestUpsertIgnore(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.TODO()
// Create the user for the first time.
orig := client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Rotem Tamir").
SaveX(ctx)
// Try to create a user with the same email the second time.
// This time we set ON CONFLICT behavior, and use the `Ignore`
// modifier.
client.User.
Create().
SetEmail("rotem@entgo.io").
SetName("Tamir, Rotem").
OnConflict().
Ignore().
ExecX(ctx)
current := client.User.GetX(ctx, orig.ID)
if current.FullName != orig.FullName {
log.Fatalf("expected upsert to keep the original version")
}
}
Puedes obtener más información sobre esta función en la documentación de Feature Flags o Upsert API.
Conclusión
En esta publicación, presentamos la API Upsert, una funcionalidad muy esperada que está disponible mediante una bandera de características en Ent v0.9.0. Discutimos dónde se usan comúnmente los upserts en aplicaciones y cómo se implementan en bases de datos relacionales comunes. Finalmente, mostramos un ejemplo simple de cómo empezar a usar la API Upsert con Ent.
¿Tienes preguntas? ¿Necesitas ayuda para empezar? Únete a nuestro servidor de Discord o canal de Slack.
- Suscríbete a nuestro Newsletter
- Síguenos en Twitter
- Únete a #ent en Gophers Slack
- Únete al Ent Discord Server