在 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 小时前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
zaim19 小时前
计算机的错误计算(一百一十四)
java·c++·python·rust·go·c·多项式
百里守约学编程1 天前
70. 爬楼梯
算法·leetcode·go
a_ran2 天前
一些 Go Web 开发笔记
后端·golang·go·编程·web·网站
影灵衣丶2 天前
go进阶编程:设计模式之适配器模式
后端·go
StevenZeng学堂2 天前
【云原生安全篇】Cosign助力Harbor验证镜像实践
网络·安全·云原生·容器·kubernetes·云计算·go
qq_172805593 天前
GO Message Bus
开发语言·后端·golang·go
一丝晨光3 天前
void类型
java·开发语言·javascript·c++·c#·go·c
IT杨秀才4 天前
自己动手写了一个协程池
后端·程序员·go
狼爷4 天前
解开 Golang‘for range’的神秘面纱:易错点剖析与解读
go