25

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

 4 years ago
source link: http://lailin.xyz/post/17394.html?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK