实现接口功能后,需要在接口中通过数据库操作,实现 Web 系统的业务功能。而 Go 因为其一些特性,数据类型和数据库的适配存在一些问题,需要在数据库开发设计时提前考虑。
想起一件事,以前有个外包的朋友告诉我,数据库所有字段必须全部
not null
,这是公司规范!
以前觉得这个规范很离谱,使用了 Go 之后发觉设置为 not null
确实可以避免很多麻烦。
一、默认值问题 {#一-默认值问题}
Go 的默认值对于其他语言的开发来说可能有些不适,举例几个常见数值的默认值。
String 默认值:
- GO:空字符串
- Java:null
- MySQL:null
int 默认值:
- Go:0
- Java:0
- MySQL:null
boolean 默认值:
- Go:false
- Java:false
- MySQL:null
通过以上默认值对比,可以发现问题,MySQL 所有数据类型都可以支持 null 值,而编程语言上却不支持。
这就导致一个问题,编程语言上的数据值如何和数据库的值对应?
在 Java
开发中,针对 null 值问题可以使用包装类,如 Integer
、Boolean
等,这也是最成熟简单的解决方案。
在 Go 中可以通过指针支持 null,也可以通过数据库包提供的 sql.NullString
、sql.NullBool
等类型来实现 null 值的适配。
但是这两种 null 值适配方案实现的不是很好,有一些解决起来挺麻烦的缺陷。
有人发现,null 貌似很多时候也没啥用。于是,出现了一些人,将数据库的 not null
选上,将默认值设置得和 Go 语言的默认值一样,以此解决默认值不一致问题。
那到底要不要保留 null 值呢?
二、保留 null 值 {#二-保留-null-值}
数据库保留 null 值时面临的第一个问题就是,怎么把 null 值传给数据库,这就需要用到指针,因为指针是支持 nil 值的。
如下示例代码,Name
属性使用的 string
类型,默认值为空字符串,不支持 null。
Team
属性使用的 *string
类型,默认值为 nil,支持空值。但是无法使用 binding
做参数校验,在 nil 值时 binding
校验会报错。
type UpdateAttachmentParam struct {
Id int64 `json:"id"`
Name string `json:"name" xorm:"VARCHAR(255) notnull" binding:"lte=255"`
Team *string `json:"team" xorm:"VARCHAR(255)"`
}
然后就是取值问题,直接使用 string
、bool
类型取值时,遇到空值将抛出异常。如果是用结构体取值,则继续使用指针即可。如果只取某个字段(如上示例的 Team
字段),实测 []*string
是不能成功取出空值的,指针会被忽略。
而且在 xorm
框架中 sql.NullString
实体不会被识别,只会被当做普通结构体处理,所以这个方法也不能优雅取值。只能先通过结构体取值,后做类型转换。
优点:
- 支持数据库空值;
缺点:
- 字段需要用指针参数类型,不能使用
binding
参数校验; - 单独取可能空值的字段不好取值,需要额外处理。
三、不保留 null 值 {#三-不保留-null-值}
不保留空值时,在 ORM 映射上就简单了许多,没有什么需要注意的地方,主要问题在于空值处理上。
xorm
框架中不会更新默认值,如果用户将一个数据更新为默认值(如删除用户简介为空字符串),那么更新将不会生效。
解决方法是通过 AllCols
或 Cols
函数将字段指定为强制更新,如此可以实现强制更新某些字段。
但是这样处理依旧不是非常好。举例如接口收到一个空字符串字段,程序无从分辨是用户主动传的空字符串,还是没有用户传值(用户不期望更新该参数)。这就需要程序在设计时约定好,比如用户不期望更新参数时也必须传原始值,造成了额外的麻烦。
优点:
- 简单,在 JSON 序列化时也支持通过
omitempty
忽略默认值;
缺点:
- 对于允许设置为默认值的字段,在接口设计上必须设计好空值传值问题,避免空值和默认值混淆;
四、方案选用 {#四-方案选用}
针对以上两种设计思路,小玖选择了第二种,不保留 null 值,简化 ORM 映射上的问题。
对于允许设置为默认值的字段,数据更新时分两种情况:
如果更新接口要求传输全量数据,则使用 AllCols
函数强制更新所有字段;
如果更新接口只传部分数据,对允许设置为空/默认值的字段,使用指针接收(避免前端传空值也被初始化为默认值),数据指针不为 nil 就会更新。
五、感想 {#五-感想}
在看一些 Go 的开源系统时,小玖也见过一些比较"秀"的操作:
有部分系统前端将需要更新的字段名称传给后端,从而指定后端更新哪些字段(也许有 SQL 注入的风险?)
有部分系统通过 map 代替结构体行数据的传输,避免默认值的情况。
这其实并不是一个非常复杂的技术难题,只是要考虑到怎么设计对开发上会更加优雅。