import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
TL;DR
- 大多数关系型数据库支持包含非结构化 JSON 值的列。
- Ent 对在关系型数据库中处理 JSON 值有很好的支持。
- 如何以原子方式向 JSON 数组添加值。
- Ent 最近新增了对在 JSON 字段中原子添加值的支持。
SQL 数据库中的 JSON 值
虽然它们主要被用于存储结构化表格数据,但几乎所有现代关系型数据库都支持使用 JSON 列在表列中存储非结构化数据。例如,在 MySQL 中可以创建如下表:
CREATE TABLE t1 (jdoc JSON);
在此列中,用户可以存储任意模式的 JSON 对象:
INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}');
由于 JSON 文档始终可以表示为字符串,它们可以存储在普通的 VARCHAR 或 TEXT 列中。然而,当列被声明为 JSON 类型时,数据库会强制执行 JSON 语法的正确性。例如,如果我们尝试向此 MySQL 表写入一个不正确的 JSON 文档:
INSERT INTO t1 VALUES('[1, 2,');
我们将收到以下错误:
ERROR 3140 (22032) at line 2: Invalid JSON text:
"Invalid value." at position 6 in value (or column) '[1, 2,'.
此外,可以在 SELECT 语句中使用 JSON Path 表达式访问存储在 JSON 文档中的值,并在谓词(WHERE 子句)中过滤数据:
select c->'$.hello' as greeting from t where c->'$.hello' = 'world';;
得到:
+--------------+
| greeting |
+--------------+
| "world" |
+--------------+
1 row in set (0.00 sec)
Ent 中的 JSON 值
使用 Ent,用户可以在模式中使用 field.JSON 定义 JSON 字段,传入所需字段名称和后备 Go 类型。例如:
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 提供了方便的 API,用于读取和写入 JSON 列的值,以及使用 sqljson 进行谓词操作:
func TestEntJSON(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.Background()
// 插入一个具有两个评论的用户。
client.User.Create().
SetTags([]schema.Tag{
{Name: "hello", Created: time.Now()},
{Name: "goodbye", Created: time.Now()},
}).
SaveX(ctx)
// 统计拥有多于零个标签的用户数量。
count := client.User.Query().
Where(func(s *sql.Selector) {
s.Where(
sqljson.LenGT(user.FieldTags, 0),
)
}).
CountX(ctx)
fmt.Printf("计数: %d", count)
// 输出: count: 1
}
在 JSON 列中追加值
在许多情况下,向 JSON 列中的列表追加值是很有用的。最好以 原子 的方式实现追加,即不需要读取当前值并写入整个新值。原因是如果两个调用尝试同时追加值,它们都将从数据库读取相同的当前值,然后大约同时写入自己的更新版本。除非使用 乐观锁,否则两次写入都会成功,但最终结果只会包含其中一个新值。
为了解决此竞争条件,我们可以让数据库处理同步,使用巧妙的 UPDATE 查询。这个思路类似于在许多应用中递增计数器的做法。你不需要先运行:
SELECT points from leaderboard where user='rotemtam'
读取结果(假设为 1000),在处理过程中递增值(1000+1=1001),然后手工写回:
UPDATE leaderboard SET points=1001 where user='rotemtam'
开发者可以使用类似的查询:
UPDATE leaderboard SET points=points+1 where user='rotemtam'
避免使用乐观锁或其他机制同步写入。让我们看看如何使用数据库的能力安全并发地完成相同操作。
在构造此查询时需要注意两点:首先,处理 JSON 值的语法在不同数据库供应商之间略有差异,如下面示例所示;其次,向 JSON 文档中的列表追加值的查询必须处理至少两个边缘情况:
- 我们想追加的字段尚不存在于 JSON 文档中。
- 字段已存在,但设置为 JSON
null。
以下是针对不同方言,在表 t 的列 c 中向字段 a 追加值 new_val 的查询示例:
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
使用 Ent 向 JSON 字段追加值
Ent 最近新增了对在 JSON 列中原子追加值的支持。让我们看看如何使用它。
如果 JSON 字段的后备类型是切片,例如:
// User 的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
// 代码高亮开始
field.JSON("tags", []string{}),
// 代码高亮结束
}
}
Ent 将在更新的变更构造器上生成 AppendTags 方法。你可以这样使用它:
func TestAppend(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.Background()
// 插入一个具有两个标签的用户。
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]
}
如果 JSON 字段的后备类型是包含列表的结构体,例如:
type Meta struct {
Tags []string `json:"tags"'`
}
// User 的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
field.JSON("meta", &Meta{}),
}
}
你可以使用自定义的 sql/modifier 选项,让 Ent 生成 Modify 方法,然后按下方式使用:
func TestAppendSubfield(t *testing.T) {
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
ctx := context.Background()
// 插入一个具有两个标签的用户。
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]
}
结语
在这篇文章中,我们讨论了 SQL 和 Ent 中的 JSON 字段。接下来,我们讨论了如何在流行的 SQL 数据库中原子地向 JSON 字段追加值。最后,我们展示了如何使用 Ent 实现此功能。你认为 Remove/Slice 操作是必要的吗?请告诉我们您的想法!
- 订阅我们的 Newsletter
- 在 Twitter 上关注我们
- 加入我们在 Gophers Slack (#ent) 上的讨论
- 加入我们的 Ent Discord Server