我正在使用一些 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