Go Web 小技巧(二)GORM 使用自定义类型

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

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

一、GORM 模型定义

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 小技巧系列的第二篇文章,下一篇为大家介绍参数绑定当中的一些小技巧

  • 本文作者: mohuishou <1@lailin.xyz>
  • 本文链接: https://lailin.xyz/post/17394.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!