实现接口功能后,需要在接口中通过数据库操作,实现 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 值问题可以使用包装类,如 IntegerBoolean 等,这也是最成熟简单的解决方案。

在 Go 中可以通过指针支持 null,也可以通过数据库包提供的 sql.NullStringsql.NullBool 等类型来实现 null 值的适配。

但是这两种 null 值适配方案实现的不是很好,有一些解决起来挺麻烦的缺陷。

有人发现,null 貌似很多时候也没啥用。于是,出现了一些人,将数据库的 not null 选上,将默认值设置得和 Go 语言的默认值一样,以此解决默认值不一致问题。

那到底要不要保留 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)"`
}

然后就是取值问题,直接使用 stringbool 类型取值时,遇到空值将抛出异常。如果是用结构体取值,则继续使用指针即可。如果只取某个字段(如上示例的 Team 字段),实测 []*string 是不能成功取出空值的,指针会被忽略。

而且在 xorm 框架中 sql.NullString 实体不会被识别,只会被当做普通结构体处理,所以这个方法也不能优雅取值。只能先通过结构体取值,后做类型转换。

sql.NullString 截图

优点:

  • 支持数据库空值;

缺点:

  • 字段需要用指针参数类型,不能使用 binding 参数校验;
  • 单独取可能空值的字段不好取值,需要额外处理。

三、不保留 null 值

不保留空值时,在 ORM 映射上就简单了许多,没有什么需要注意的地方,主要问题在于空值处理上。

xorm 框架中不会更新默认值,如果用户将一个数据更新为默认值(如删除用户简介为空字符串),那么更新将不会生效。

解决方法是通过 AllColsCols 函数将字段指定为强制更新,如此可以实现强制更新某些字段。

但是这样处理依旧不是非常好。举例如接口收到一个空字符串字段,程序无从分辨是用户主动传的空字符串,还是没有用户传值(用户不期望更新该参数)。这就需要程序在设计时约定好,比如用户不期望更新参数时也必须传原始值,造成了额外的麻烦。

优点:

  • 简单,在 JSON 序列化时也支持通过 omitempty 忽略默认值;

缺点:

  • 对于允许设置为默认值的字段,在接口设计上必须设计好空值传值问题,避免空值和默认值混淆;

四、方案选用

针对以上两种设计思路,小玖选择了第二种,不保留 null 值,简化 ORM 映射上的问题。

对于允许设置为默认值的字段,数据更新时分两种情况:

如果更新接口要求传输全量数据,则使用 AllCols 函数强制更新所有字段;

如果更新接口只传部分数据,对允许设置为空/默认值的字段,使用指针接收(避免前端传空值也被初始化为默认值),数据指针不为 nil 就会更新。

五、感想

在看一些 Go 的开源系统时,小玖也见过一些比较“秀”的操作:

有部分系统前端将需要更新的字段名称传给后端,从而指定后端更新哪些字段(也许有 SQL 注入的风险?)

有部分系统通过 map 代替结构体行数据的传输,避免默认值的情况。

这其实并不是一个非常复杂的技术难题,只是要考虑到怎么设计对开发上会更加优雅。