惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
量子位
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Google DeepMind News
Google DeepMind News
博客园_首页
云风的 BLOG
云风的 BLOG
月光博客
月光博客
I
InfoQ
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Vercel News
Vercel News
美团技术团队
Microsoft Security Blog
Microsoft Security Blog
P
Proofpoint News Feed
D
Docker
F
Fortinet All Blogs
N
Netflix TechBlog - Medium
博客园 - 叶小钗
Martin Fowler
Martin Fowler
雷峰网
雷峰网
酷 壳 – CoolShell
酷 壳 – CoolShell
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog

Mohuishou

如何实现支持多集群的 Kubernetes Operator? 第三方应用如何调用我们 kubebuilder 生成的自定义资源? Kubernetes 简明教程 k8s job 为何迟迟不能结束? Go 工程化(十一) 如何优雅的写出 repo 层代码 Go 工程化(十) 如何在整洁架构中使用事务? 给博客添加章节目录 使用 Notion Database 管理静态博客文章 一个普通 Go 开发的三年 4. localhost 就一定是 localhost 么? Go可用性(七) 总结: 一张图串联可用性知识点 Go可用性(六) 熔断 10. 总结 9. kubebuilder 进阶: 源码分析 8. kubebuilder 进阶: webhook 7. kubebuilder 进阶: 测试 6. kubebuilder 实战: status & event 5. kubebuilder 实战: CRUD 4. kustomize 简明教程 3. KubeBuilder 简明教程 2. Kind: 如何快速搭建本地 K8s 开发环境? 1. Operator概述: 如何对 Kubernetes 进行扩展 Go可用性(五) 自适应限流 Go可用性(四) 漏桶算法 Go可用性(三) 令牌桶的实现 rate/limt Go可用性(二) 令牌桶原理及使用 Go可用性(一) 隔离设计 Go并发编程(十二) Singleflight Go工程化(九) 项目重构实践 Go工程化(八) 单元测试
Go Web 小技巧(二)GORM 使用自定义类型
mohuishou <1@lailin.xyz> · 2019-11-03 · via Mohuishou

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

不知道大家在使用 Gorm 的时候,是否有遇到过复杂类型 ( map, struct…) 如何映射到数据库的字段上的问题?

本文分别介绍通过实现通用接口和 Hook 的方式绑定复杂的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
gorm.Model
Name string
Age sql.NullInt64
Birthday *time.Time
Email string `gorm:"type:varchar(100);unique_index"`
Role string `gorm:"size:255"` // 设置字段大小为255
MemberNumber *string `gorm:"unique;not null"` // 设置会员号(member number)唯一并且不为空
Num int `gorm:"AUTO_INCREMENT"` // 设置 num 为自增类型
Address string `gorm:"index:addr"` // 给address字段创建名为addr的索引
IgnoreMe int `gorm:"-"` // 忽略本字段
}

这是 GORM 官方文档当中模型定义的一个例子,但是我们在实际使用过程当中往往会遇到需要复杂类型例如 map 或者是一些自定义的类型进行绑定。

我们在文档的描述当中可以看到这么一段话:

模型(Models)通常只是正常的 golang structs、基本的 go 类型或它们的指针。 同时也支持sql.Scannerdriver.Valuer 接口(interfaces)。

自已的数据类型只需要实现这两个接口就可以实现数据绑定了,文档只有一句话我们看看具体怎么做。

二、通过实现 sql.Scanner,driver.Valuer 接口实现数据绑定

2.1 接口文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// sql.Scanner
type Scanner interface {
// Scan assigns a value from a database driver.
//
// The src value will be of one of the following types:
//
// int64
// float64
// bool
// []byte
// string
// time.Time
// nil - for NULL values
//
// An error should be returned if the value cannot be stored
// without loss of information.
//
// Reference types such as []byte are only valid until the next call to Scan
// and should not be retained. Their underlying memory is owned by the driver.
// If retention is necessary, copy their values before the next call to Scan.
Scan(src interface{}) error
}

// driver.Valuer
type Valuer interface {
// Value returns a driver Value.
// Value must not panic.
Value() (Value, error)
}

我们可以发现 Valuer 用于保存数据的时候,Scaner 用于数据从数据库映射到 model 的时候

2.2 实现接口

下面我们来一个实际的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Args 参数
type Args map[string]string

// Scan Scanner
func (args Args) Scan(value interface{}) error {
if value == nil {
return nil
}

b, ok := value.([]byte)
if !ok {
return fmt.Errorf("value is not []byte, value: %v", value)
}

return json.Unmarshal(b, &args)
}

// Value Valuer
func (args Args) Value() (driver.Value, error) {
if args == nil {
return nil, nil
}

return json.Marshal(args)
}

在使用的时候我们只要再加上一个数据类型就 OK 了

1
2
3
type Data struct {
Args Args `json:"args" gorm:"type:text"`
}

2.3 抽象通用工具函数

在实际的使用中我们可能会有许多的类型的需要这样存储,所以我们直接抽象一个公用的工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// scan for scanner helper
func scan(data interface{}, value interface{}) error {
if value == nil {
return nil
}

switch value.(type) {
case []byte:
return json.Unmarshal(value.([]byte), data)
case string:
return json.Unmarshal([]byte(value.(string)), data)
default:
return fmt.Errorf("val type is valid, is %+v", value)
}
}

// for valuer helper
func value(data interface{}) (interface{}, error) {
vi := reflect.ValueOf(data)
// 判断是否为 0 值
if vi.IsZero() {
return nil, nil
}
return json.Marshal(data)
}

使用的时候只需要调用一下

1
2
3
4
5
6
7
8
9
10
11
12
// Args 参数
type Args map[string]string

// Scan Scanner
func (args Args) Scan(value interface{}) error {
return scan(&args, value)
}

// Value Valuer
func (args Args) Value() (driver.Value, error) {
return value(args)
}

三、通过 hook 实现数据绑定

除了上面的这种方法有没有其他的实现方式呢?

当然是有的,从上面的例子我们可以发现,实现方式就是保存数据的时候将数据转换为基本类型,然后在取出来绑定数据的时候再转换一下,这个过程我们也可以通过 GORM 的 Hook 实现。利用 BeforeSave 在数据保存前转换,再利用 AfterFind 在数据取出来之后转换即可。但是这种方式我们需要在 struct 中定义一个用于实际映射数据库数据的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Data Data
type Data struct {
Args map[string]interface{} `json:"args" gorm:"-"`
ArgsStr string `json:"-" gorm:"column:args"`
}

// BeforeSave 数据保存前
func (data *Data) BeforeSave() error {
if data.Args == nil {
return nil
}

b, err := json.Marshal(&data.Args)
if err != nil {
return err
}

data.ArgsStr = string(b)
return nil
}

// AfterFind 查询之后
func (data *Data) AfterFind() error {
if data.ArgsStr == "" {
return nil
}

return json.Unmarshal([]byte(data.ArgsStr), &data.Args)
}

这样同样可以达到相似的效果

总结

这篇文章介绍了两种通用数据类型在 GORM 中的绑定方式:

  • 通过实现相关的接口实现,并且抽象了一个通用的辅助函数
  • 通过 hook 实现

一般推荐使用第一种方法,只是需要单独定义数据类型,第二种方法需要多一个辅助字段,这种方式如果相关的字段过多会很不优雅。

感谢阅读,这是 Go Web 小技巧系列的第二篇文章,下一篇为大家介绍参数绑定当中的一些小技巧

关注我获取更新

猜你喜欢