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:
El campo al que queremos añadir aún no existe en el documento JSON.
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:
- MySQL
- PostgeSQL
- SQLite
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
UPDATE "t" SET "c" = CASE
WHEN
(("c"->'a')::jsonb IS NULL
OR ("c"->'a')::jsonb = 'null'::jsonb)
THEN
jsonb_set("c", '{a}', 'new_val', true)
ELSE
jsonb_set("c", '{a}', "c"->'a' || 'new_val', true)
END
UPDATE `t` SET `c` = CASE
WHEN
(JSON_TYPE(`c`, '$') IS NULL
OR JSON_TYPE(`c`, '$') = 'null')
THEN
JSON_ARRAY(?)
ELSE
JSON_INSERT(`c`, '$[#]', ?)
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!
- Suscríbete a nuestro Newsletter
- Síguenos en Twitter
- Únete a #ent en Gophers Slack
- Únete al Servidor de Discord de Ent