在 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
相关推荐
DemonAvenger1 天前
深入剖析 sync.Once:实现原理、应用场景与实战经验
分布式·架构·go
一个热爱生活的普通人2 天前
Go语言中 Mutex 的实现原理
后端·go
孔令飞2 天前
关于 LLMOPS 的一些粗浅思考
人工智能·云原生·go
小戴同学2 天前
实时系统降低延时的利器
后端·性能优化·go
Golang菜鸟3 天前
golang中的组合多态
后端·go
Serverless社区3 天前
函数计算支持热门 MCP Server 一键部署
go
Wo3Shi4七3 天前
二叉树数组表示
数据结构·后端·go
网络研究院3 天前
您需要了解的有关 Go、Rust 和 Zig 的信息
开发语言·rust·go·功能·发展·zig
27669582923 天前
拼多多 anti-token unidbg 分析
java·python·go·拼多多·pdd·pxx·anti-token
程序员爱钓鱼4 天前
Go 语言邮件发送完全指南:轻松实现邮件通知功能
后端·go·排序算法