起步
想在使用 GORM 时使用自定义类型必然事出有因,一般可有以下两种方式:
方法 1:
type MyString string
方法 2:
type MyString struct { string }
当需求比较简单时,可采取方法1,也就是类型别名;如果需求复杂,就不得不把数据字段嵌入自定义结构体中。字段是否匿名并不重要,主要是用来承载目的数据。
单单把数据类型定义了还不够,还需要实现两个方法,你把这理解为一种协议即可。
// 写入数据库之前,对数据做类型转换
func (s MyString) Value() (driver.Value, error) {
...
}
// 将数据库中取出的数据,赋值给目标类型
func (s *MyString) Scan(v interface{}) error {
...
}
下面将结合我在实际开发遇到的业务场景,讲解为什么需要自定义类型,以及如何去实现上述的两个方法。
方法1:类型别名
场景 1
第一个场景:我需要自定义时间的显示格式。
当我的 model 嵌入 gorm.Model 时,会多四个字段,分别是:id, created_at, updated_at, deleted_at。
type Plan struct {
gorm.Model
Name string `gorm:"column:name"`
}
我面对的需求是,把数据从数据库中取出来,并按照规定的格式显示时间,最后返回给前端(需要 JSON 处理)。当然,我比较懒,希望直接取出数据,立马返给前端,时间的格式还是我期望的那样。为简便起见,这里只用到 created_at,name 两个字段。
先定义一个返给前端的数据结构:
type MyTime time.Time
// 返回给前端的数据结构
type Resp struct {
CreatedAt MyTime `gorm:"column:created_at"`
Name string `gorm:"column:name"`
}
查询数据库代码如下。同时我用 json.Marshal 方法将结构体转换成 json 字符串,相当于模拟了将数据传递给前端的一个过程。
var resp Resp
db.Model(&Plan{}).Select("created_at, name").Limit(1).Scan(&resp)
data, _ := json.Marshal(resp)
log.Println(string(data))
然而日志输出不是我们想看到的:2020/02/16 19:21:28 {"CreatedAt":{},"Name":"早饭"}
。
这里还需要注意程序并没有报错。没报错是因为 MyTime 是 time.Time 类型的别名,两个类型之间允许相互转换。但是为什么输出是一个空值呢?
MyTime 作为 time.Time 的别名,但是并没有继承 time.Time 的方法,也就不支持 json.Marshal 转换。所以还需要为 MyTime 绑定 MarshalJSON 方法。
func (t MyTime) MarshalJSON() ([]byte, error) {
tTime := time.Time(t)
tStr := tTime.Format("2006/01/02 15:04:05") // 设置格式
// 注意 json 字符串风格要求
return []byte(fmt.Sprintf("\"%v\"", tStr)), nil
}
再运行程序就一切正常了:2020/02/16 19:31:38 {"CreatedAt":"2020/02/16 18:53:13","Name":"早饭"}
。
这里尤其需要注意 json 字符串的风格要求,不然你很有可能得不到你想要的结果。详见 两个不经意间的报错。
场景 2
第二个场景:基于自定义类型正常读写数据库。
第二个场景是基于第一个场景之上提出一些奢望。因为你不妨打开数据库看看(我用的是 Navicat Premium 可视化工具),可以看到 created_at 字段数据显示为:2020-02-16 18:53:13.8644852+08:00
。我们让时间格式打一开始就是目标格式不好吗?
Plan model 修改成下面这样:
type MyTime time.Time
type Plan struct {
CreatedAt MyTime `gorm:"column:created_at"`
Name string `gorm:"column:name"`
}
删除之前创建的数据表。一切准备就绪,我们先调用 CreateTable 创建一个新表。程序倒是没有报错的运行完毕,但是你打开表一看:没有 create_at 字段!!!
回到开篇提到的,我们还需要为自定义类型实现 Value() (driver.Value, error)
, Scan(v interface{}) error
这两个方法才行。
func (t MyTime) Value() (driver.Value, error) {
// MyTime 转换成 time.Time 类型
tTime := time.Time(t)
return tTime.Format("2006/01/02 15:04:05"), nil
}
func (t *MyTime) Scan(v interface{}) error {
switch vt := v.(type) {
case string:
// 字符串转成 time.Time 类型
tTime, _ := time.Parse("2006/01/02 15:04:05", vt)
*t = MyTime(tTime)
default:
return errors.New("类型处理错误")
}
return nil
}
可以看到,其实我们做类型处理时都借助了 time.Time 类型做中转。所以不论我们的自定义类型基于时间类型还是整型、浮点型,我们都应该先转换成 go 默认支持的类型,再进行一系列操作。
另外一个重点,关注 Value 和 Scan 的职责。Value 返回的数据是要写入数据库的,我们这里明明是时间类型,但是 return 出去的居然是字符串。同理在 Scan 方法中,参数 v 是来自数据库中的数据,MyTime 对应的字段是时间类型,但我们的处理方式明显是把 v 作为了字符串类型处理。(前提:数据库为 sqlite3)
如果不是 sqlite 数据库,如 mysql,照理说应该可以直接 return 出 time.Time 类型的数据。但我发现程序会抛出这样一个错误:Error 1265: Data truncated for column 'xxxx' at row 1
。暂时没找到解决方案,怀疑这是一个 BUG。因为数据库为 mysql 时,将时间字段放在结构体中就可以了。eg:
type MyTime struct {
Time time.Time
}
自定义类型为 struct 时如何处理,下面马上说到。
方法2:定义结构体
场景 3
第三个场景:我需要对类型限制。
我遇上了这样一个需求:要在 gender 字段中存储“男”或者“女”,且类型为字符串。类型别名就明显不适合了,因为它无法限制数据的内容。解决方案当然很多,我说说我的思考方式。
我想把存储性别这个值作为私有属性,不允许外界直接对其赋值,必须通过我提供的 New 方法,这样我就可以对传入的参数做校验。
type MyGender struct {
string
}
func NewGender(v string) (MyGender, error) {
var g MyGender
if v != "男" && v != "女" {
return g, errors.New("只支持 “男” 或者 “女”")
}
g.string = v
return g, nil
}
同理,要做到数据库驱动支持,还需要实现两个方法:
func (g MyGender) Value() (driver.Value, error) {
return g.string, nil
}
func (g *MyGender) Scan(v interface{}) error {
g.string = v.(string)
return nil
}
核心思想不变:将自定义类型转换成 go 支持的基础类型。现在 Stu model 就可以正常用来读写数据库了。
type Stu struct {
Name string `gorm:"column:name"`
Gender MyGender `gorm:"column:gender"`
}
结合源码分析
Scan 与 Value 方法从何而来?
事实上我们知道 go 提供了一些可空值的类型供开发者使用,即:sql.NullTime, sql.NullBool, sql.NullString……可以选一个看看它的源码。
// go 源码
type NullBool struct {
Bool bool
Valid bool // Valid is true if Bool is not NULL
}
// Scan implements the Scanner interface.
func (n *NullBool) Scan(value interface{}) error {
// 如果 value 为空,则认为是 false
if value == nil {
n.Bool, n.Valid = false, false
return nil
}
n.Valid = true
return convertAssign(&n.Bool, value)
}
// Value implements the driver Valuer interface.
func (n NullBool) Value() (driver.Value, error) {
// 如果无效,就返回 nil
if !n.Valid {
return nil, nil
}
return n.Bool, nil
}
当你需要自定义新类型时,可以照着源码包中的代码依葫芦画瓢。
Valuer 接口的注意事项
// go 源码
type Valuer interface {
Value() (Value, error)
}
之前说 Value() (driver.Value, error)
方法,其实就是实现 Valuer 接口。当你的程序出现下面这类错误时,你就要注意了,可能是 Value 方法没写恰当。sql: converting argument $5 type: non-Value type main.MyNum returned from Value
在官方包 database.sql.driver.types.go 中有这样一段源码:
// go 源码
func (defaultConverter) ConvertValue(v interface{}) (Value, error) {
if IsValue(v) {
return v, nil
}
switch vr := v.(type) {
case Valuer:
sv, err := callValuerValue(vr)
...
return sv, nil
...
rv := reflect.ValueOf(v)
switch rv.Kind() {
...
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int(), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
return int64(rv.Uint()), nil
case reflect.Uint64:
u64 := rv.Uint()
if u64 >= 1<<63 {
return nil, fmt.Errorf("uint64 values with high bit set are not supported")
}
return int64(u64), nil
case reflect.Float32, reflect.Float64:
return rv.Float(), nil
case reflect.Bool:
return rv.Bool(), nil
case reflect.Slice:
ek := rv.Type().Elem().Kind()
if ek == reflect.Uint8 {
return rv.Bytes(), nil
}
return nil, fmt.Errorf("unsupported type %T, a slice of %s", v, ek)
case reflect.String:
return rv.String(), nil
}
return nil, fmt.Errorf("unsupported type %T, a %s", v, rv.Kind())
}
我们随便取一例来关注:
// go 源码
...
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int(), nil
...
能够看到当数据为整型时,不论是 int 还是 int8、int16 等等,最后都去调用了 Int() 方法。再去看 Int() 的源码:
// go 源码
func (v Value) Int() int64 {
k := v.kind()
p := v.ptr
switch k {
case Int:
return int64(*(*int)(p))
case Int8:
return int64(*(*int8)(p))
case Int16:
return int64(*(*int16)(p))
case Int32:
return int64(*(*int32)(p))
case Int64:
return *(*int64)(p)
}
panic(&ValueError{"reflect.Value.Int", v.kind()})
}
也就是说,不管你是啥整型,一律转成 int64。而前面之所以会遇到异常 sql: converting argument ...
是因为我 Value 中返回了 uint8 类型。
再从下列代码中我们可以看出:当你的自定义类型实现 Valuer 接口以后,官方包就不会再给你做类型转换了。
// go 源码
case Valuer:
sv, err := callValuerValue(vr)
...
return sv, nil
因而程序报错。是不是所有数据库都这样呢,其他类型又如何?前者我说不好,后者嘛,你可以做更多的尝试或者去阅读源码。
但是取值却无法取啊
scan 方法就是用来取值的