在 Go 和 GORM 中使用动态结构

翻译自 dpb587.me/post/2021/0...

我正在使用一些 JSON-LD 数据源,我需要将它们导入到数据库进行测试。由于我已经在一个 Go 项目中,所以我想弄清楚如何动态管理数据库模式。我一直在其他地方使用 GORM 包来管理数据库,所以这成为了测试 reflect 包与动态数据结构的好借口。

原型化,静态动态

传统上,Go 不是一个非常"动态"的语言,所以我开始使用一个样本记录来弄清楚它可能看起来是什么样的。从 JSON 开始...

go 复制代码
var sampleJSON = []byte(`{ "@type": "measurement",
  "@id": "a42fadde-ee76-4687-8f8f-303e083461e8",
  "at": "2020-10-29T23:17:21Z",
  "value": 79.3 }`)

然后我可以使用 reflect 包来定义一些字段。StructField 类型可以首先用来列出将要成为 struct 的字段。在这种情况下,我知道 @table 必须是一个字符串,任何其他字段都可以命名为 Field#,因为它实际上并不重要。

go 复制代码
var dynamicFields = []reflect.StructField{
	{Name: "Table",
		Type: reflect.TypeOf(""),
		Tag:  reflect.StructTag(`json:"@type" gorm:"-"`)},
	{Name: "Field0",
		Type: reflect.TypeOf(""),
		Tag:  reflect.StructTag(`json:"@id" gorm:"column:_id;"`)},
	{Name: "Field1",
		Type: reflect.TypeOf(time.Time{}),
		Tag:  reflect.StructTag(`json:"at" gorm:"column:at"`)},
	{Name: "Field2",
		Type: reflect.TypeOf(float32(0)),
		Tag:  reflect.StructTag(`json:"value" gorm:"column:value"`)}}

注意,字段既定义了 json 标签(在读取 JSON 输入时将使用)也定义了 gorm 标签(在 ORM 保存到数据库时将使用)。

一旦字段列表准备好,就可以使用 StructOf 函数来准备一个类型。从那里,我可以创建一个新的值,我可以在其中解组样本 JSON。

go 复制代码
var dynamicType = reflect.StructOf(dynamicFields)
var record = reflect.New(dynamicType).Interface()

err := json.Unmarshal(sampleJSON, &record)
if err != nil {
	panic(err)
}

fmt.Printf("%#+v\n", record)
go 复制代码
&struct {
  Table string "json:\"@type\" gorm:\"-\"";
  Field0 string "json:\"@id\" gorm:\"column:_id\"";
  Field1 time.Time "json:\"at\" gorm:\"column:at\"";
  Field2 float32 "json:\"value\" gorm:\"column:value\""
} {
  Table: "measurement",
  ID: "a42fadde-ee76-4687-8f8f-303e083461e8",
  Field0: time.Time{wall:0x0, ext:63739610241, loc:(*time.Location)(nil)},
  Field1: 79.3
}

因为它正在打印一个动态结构,所以它使用了更详细的内联格式。但是,所有的输出看起来都是正确的!

在大多数情况下,GORM 会自动提取字段,但我们会想要提取 Table 字段。由于这是一个动态结构,我们不能直接引用 sampleValue.Table;但我们可以再次使用 reflect 来使用其 FieldByName 函数获取字符串。在这种情况下,Elem 函数被用来确保我们正在查看我们的动态结构值(而不是来自 reflect 内部对象的字段)。

go 复制代码
table := reflect.ValueOf(record).Elem().FieldByName("Table").String()
fmt.Printf("%s\n", table)
go 复制代码
measurement

接下来,我可以使用 ORM 库来自动 创建/修改 表。注意,在这种情况下,可以安全地添加新字段,但不支持更改字段类型(例如,从浮点数到布尔值)。

go 复制代码
if err := db.Table(table).AutoMigrate(record); err != nil {
	panic(err)
}

最后,我们可以使用 Create 函数将记录保存到数据库中。虽然 GORM 支持在模型上定义表并避免重复,但由于这是一个动态 struct,所以直接使用 Table 函数会更容易。

go 复制代码
if err := db.Table(table).Create(record).Error; err != nil {
	panic(err)
}

深入动态

现在我们有了一些功能性的构建块,是时候让它从任意数据中工作了。

构建类型

首先,我们将使用一个函数,它可以将一个通用的 JSON 对象转换并从中构建一个 reflect.Type

go 复制代码
func buildType(recordRaw map[string]interface{}) (reflect.Type, error) {

在其中,我们准备 fields 作为 reflect.StructFields 的列表。由于需要一个 Table 字段,所以在遍历记录的 map 之前,它被硬编码。

go 复制代码
for key, value := range recordRaw {

在循环中,我们可以执行任何特殊的逻辑,以便为我们的数据域转换键或值。例如,忽略 @type 键,或将值转换为原生类型(如 time.Time 类型)。

go 复制代码
if valueT, ok := value.(string); ok && reValueRFC3339.MatchString(valueT) {
	valueTime, err := time.Parse(time.RFC3339, valueT)
	if err == nil {
		value = valueTime
	}
}

完成更改后,我们以与原型类似的方式添加我们生成的 reflect.StructField

go 复制代码
fields = append(fields, reflect.StructField{
	Name: fmt.Sprintf("Field%d", len(fields)),
	Type: reflect.TypeOf(value),
	Tag:  reflect.StructTag(fmt.Sprintf(`json:"%s" gorm:"column:%s"`, key, dbkey)),
})

一旦添加了所有的 key/value,我们就可以最终返回生成的 struct

go 复制代码
return reflect.StructOf(fields), nil

构建值

接下来,我添加了一个新的函数,它负责构建类型、创建值,然后"重新封装"它 - marshal 将其重新封装回 JSON,然后 unmarshal 解封装到结构值中 - 然后返回它。

Next, I add a new function which takes care of both building the type, creating a value, and then "remarshal" it - marshal back to JSON, then unmarshal into the struct value - before returning it back.

go 复制代码
func buildRecord(recordRaw map[string]interface{}) (interface{}, error) {
	recordType, err := buildType(recordRaw)
	if err != nil {
		return nil, fmt.Errorf("building type: %s", err)
	}

	record := reflect.New(recordType).Interface()

	if err := remarshal(&record, recordRaw); err != nil {
		return nil, fmt.Errorf("updating struct: %s", err)
	}

	return record, nil
}

添加一些数据

最后,主循环使用 json.Decoder 读取 JSON Lines,然后使用前面描述的函数将记录插入到数据库中。

go 复制代码
var recordRaw map[string]interface{}

if err := jsonl.Decode(&recordRaw); err == io.EOF {
	break
} else if err != nil {
	panic(err)
}

record, err := buildRecord(recordRaw)
if err != nil {
	panic(err)
}

table := reflect.ValueOf(record).Elem().FieldByName("Table").String()

if err := db.Table(table).AutoMigrate(record); err != nil {
	panic(err)
}

if err := db.Table(table).Create(record).Error; err != nil {
	panic(err)
}

fmt.Printf("%#+v\n", record)

运行程序并通过 STDIN 管道传输相同的样本数据,它将解析它,创建表,并插入记录。

shell 复制代码
go run . <<< '{ "@type": "measurement", "@id": "a42fadde-ee76-4687-8f8f-303e083461e8", "at": "2020-10-29T23:17:21Z", "value": 79.3 }'

# &struct { Table string "json:\"@type\" gorm:\"-\""; Field1 string "json:\"@id\" gorm:\"column:_id\""; Field2 time.Time "json:\"at\" gorm:\"column:at\""; Field3 float64 "json:\"value\" gorm:\"column:value\"" }{Table:"measurement", Field1:"a42fadde-ee76-4687-8f8f-303e083461e8", Field2:time.Time{wall:0x0, ext:63739610241, loc:(*time.Location)(nil)}, Field3:79.3}

直接查看数据库,我们也可以验证结果。

bash 复制代码
sqlite3 main.sqlite \
  '.schema measurement' \
  'SELECT * FROM measurement'
# CREATE TABLE `measurement` (`_id` text,`at` datetime,`value` real);
# a42fadde-ee76-4687-8f8f-303e083461e8|2020-10-29 23:17:21+00:00|79.3

替代方案

到最后,它已经足够好用于测试,我也对 reflect 包有了更多的了解。任务完成。然而,如果你正在处理类似的场景,你可能需要考虑以下替代方案:

  • 使用 Go 以外的其他东西 - 这种动态实现(对于 Go 和 GORM)并不是它们设计的方法或用例。
  • 使用库 - 例如,dynamic-struct 包似乎为处理动态结构提供了更好的抽象,如果你真的需要它们的话。
  • 直接使用 GORM 的模式管理 - 有几个子包似乎负责管理表模式,可能可以直接使用(而不是通过 struct 定义模式)。schema, migrator
相关推荐
研究司马懿1 天前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大2 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰2 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘2 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤2 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto4 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto6 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室7 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题7 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo