Go MySQL 数据转换 - Wed, Sep 8, 2021
Go 语言结构体与 MySQL 之间的数据转换技巧。使用 template 和 reflect 实现 CRUD,使用 reflect 将 rows 转换为结构体。
Go 语言结构体与 MySQL 之间的数据转换技巧。使用 template 和 reflect 实现 CRUD,使用 reflect 将 rows 转换为结构体。
注意: MySQL 8.0 已成为主流版本,本文档可能基于旧版本编写。
概述
Go 语言中的数据库操作通常需要处理结构体和数据库之间的转换。本文介绍如何使用反射和模板实现通用的数据转换。
核心思路:
- 使用
template和reflect实现 CRUD 操作 - 使用
reflect将rows转换为结构体 - 使用 JSON tag 作为数据库字段映射
查询
查询操作最为简单,使用模板生成 SQL 语句。
Go 模板文件
SELECT * FROM {{.table}}
{{- if .query}} WHERE {{.query}} {{end -}}
{{- if .limit -}}
{{- if ne .limit 0 -}}LIMIT {{.limit}} {{end -}}
{{- end -}}
{{- if .skip -}}
{{- if ne .skip 0 -}}OFFSET {{.skip}} {{end -}}
{{- end -}}
;
Go 代码
// SQLQuery query sql string
func SQLQuery(table string, limit, skip int64, sort, query string) (sql string) {
params := map[string]interface{}{
"table": table,
"limit": limit,
"skip": skip,
"query": query,
"sort": sort,
}
tpl, err := template.New("query").Parse(Query)
if err != nil {
return
}
buf := &bytes.Buffer{}
err = tpl.Execute(buf, params)
if err != nil {
return
}
sql = buf.String()
return
}
插入
使用反射获取 JSON tag 作为 key,取值作为值。使用 insert 语句,将 key 和 value 一一对应。
注意事项:
- 字符串需要加单引号
- 数字不需要加单引号
- 将 map 转换为字符串填充
Go 模板文件
INSERT INTO {{.table}}({{.keys}}) VALUES({{.values}});
Go 代码
// SQLInsert insert sql string
func SQLInsert(table string, in proto.Message) (sql string) {
refValue := reflect.ValueOf(in)
if refValue.Kind() == reflect.Ptr {
refValue = refValue.Elem()
}
refType := refValue.Type()
params := map[string]interface{}{
"table": table,
}
tmp := map[string]string{}
for i := 0; i < refType.NumField(); i++ {
ft := refType.Field(i)
if !ft.IsExported() {
continue
}
fv := refValue.FieldByName(ft.Name)
tag := ft.Tag.Get("json")
switch fv.Kind() {
case reflect.Map:
v, _ := json.Marshal(fv.Interface())
tmp[tag] = fmt.Sprintf("'%s'", v)
case reflect.String:
tmp[tag] = fmt.Sprintf("'%v'", fv.Interface())
case reflect.Int32:
// 注意: Sprintf 会调用 String 函数
mf := fv.MethodByName("String")
if mf.IsValid() {
tmp[tag] = fmt.Sprintf("'%v'", fv.Interface())
}
default:
// 注意: Sprintf 会调用 String 函数
mf := fv.MethodByName("String")
if mf.IsValid() {
tmp[tag] = fmt.Sprintf("'%v'", fv.Interface())
} else {
if tag == "created_at" || tag == "updated_at" {
tmp[tag] = "now()"
} else {
tmp[tag] = fmt.Sprintf("%v", fv.Interface())
}
}
}
}
var keys []string
var values []string
for k, v := range tmp {
keys = append(keys, k)
values = append(values, v)
}
params["keys"] = strings.Join(keys, ",")
params["values"] = strings.Join(values, ",")
tpl, err := template.New("insert").Parse(Insert)
if err != nil {
return
}
buf := &bytes.Buffer{}
err = tpl.Execute(buf, params)
if err != nil {
return
}
sql = buf.String()
return
}
查询结果转换为结构体
将 scan 出的列与结构体反射出的 JSON tag 一一对应,通过 fv.Addr().Interface() 提取字段指针,使用 rows.Scan 直接对反射出的指针赋值。
Go 代码
// Scan scan rows to message
func Scan(rows *sql.Rows, in proto.Message) (err error) {
refValue := reflect.ValueOf(in)
if refValue.Kind() == reflect.Ptr {
refValue = refValue.Elem()
}
cols, err := rows.Columns()
if err != nil {
return
}
refType := refValue.Type()
columns := make([]interface{}, len(cols))
for i := 0; i < refValue.NumField(); i++ {
ft := refType.Field(i)
if !ft.IsExported() {
continue
}
fv := refValue.FieldByName(ft.Name)
tag := ft.Tag.Get("json")
for j, col := range cols {
if col == tag {
columns[j] = fv.Addr().Interface()
}
}
}
err = rows.Scan(columns...)
if err != nil {
logrus.WithError(err).Errorln("rows scan error")
return
}
logrus.Infoln(in)
return
}
最佳实践
- 类型安全:使用反射时要小心类型转换错误
- 性能优化:反射操作较慢,考虑缓存反射结果
- 错误处理:确保正确处理 SQL 错误和反射错误
- 字段映射:使用明确的字段 tag,避免歧义
- SQL 注入防护:使用参数化查询,避免 SQL 注入
替代方案
考虑使用成熟的 ORM 框架:
- GORM: 功能完善的 Go ORM
- sqlx: 增强的 database/sql
- ent: Facebook 的实体框架
这些框架提供了更安全、更高效的数据转换方式。