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