Go Web 小技巧(二)GORM 使用自定义类型
本文分别介绍通过实现通用接口和 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 小技巧系列的第二篇文章,下一篇为大家介绍参数绑定当中的一些小技巧