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

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

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

一、GORM 模型定义

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.Scanner driver.Valuer 接口(interfaces)。

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

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

2.1 接口文档

// 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 实现接口

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

// 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 了

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

2.3 抽象通用工具函数

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

// 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)
}

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

// 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 中定义一个用于实际映射数据库数据的字段。

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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章