go 语言操作数据库 CRUD

go 语言标准库已经提供数据库访问通用接口,不同数据库需搭配相应连接 Driver。标准库里面 database/sql 实现基本数据类型 Scan, 基本的 Transaction 以及 sql 参数化。用这些东西去操作数据库完全够用,只是随着项目代码增长,sql string 会蔓延到项目的各个角落。若要修改数据库表结构,这些 sql string 就是噩梦一般存在。流行做法是使用 ORM(Object Relational Mapping) 去解决这个问题。ORM 封装好一些 CRUD 基本操作,可以避免大量手写 sql string.

go 编译型语言,无法做到像 Ruby 里面的 Method Missing 这样动态特性,可以为一个复合类型的 struct 随时添加一个 field. go 社区使用的 ORM 还是需要事先手动添加好 Field. 这样做需要对数据库和 struct 的成员做很多约定,写好对应的访问 tag。这些仍然无法避免修改数据库表时要一齐修改 go 代码。

我们可以仔细想想 ORM 真的是唯一的选择吗?ORM 实际上无法完美操作数据库,它做的事情无非就是这些事情:

  1. 参数化 sql string。
  2. 封装一些简单的 CRUD 操作。
  3. 序列化 sql 查询结果。

对于一些复杂的数据库查询语句,任何强大的 ORM 库,灵活的动态语言特性都无法解决。况且 ORM 也未必是好东西,它简化了操作,却间接隐藏了数据库的一些功能。如果不脱离 ORM 依赖我们无法去更好的操作数据库,去自己优化查询语句。以 go 语言目前的特性来看,这个社区永远也不可能会做出能像动态语言那样灵活的 ORM。关于 ORM 与 go 语言更详细的吐槽请见这里

如果不用 ORM 我们碰到的无非是上面提到的 3 个问题而已。把这个三个问题掰开处理,第一个问题是不存在的。参数化 sql string 这件事情我们只需要小心的处理就能防止 sql 注入这样的安全问题。剩下的就是 2 和 3 的问题,而第二个问题我们暂时也不需要解决,我们一开始就去写一些重复代码好了。而问题 3 我们可以手动自己去序列化,也可以用社区的一些库去解决这个问题。社区已经有了不错的解决方案,我用的就是 sqlx 去解决的这个问题。

当初项目开始时我考察不少 ORM 库后,果断抛弃使用 ORM。项目经过一段时间迭代后,不用 ORM 没有特别明显的不便。唯一的问题就是我需要手动为每一张表做好 table 到 go struct 之间的映射。需要写非常多重复性的 CRUD 操作代码。这些代码很难使用统一方法减少重复,只好任其膨胀。直到有一天我看到这篇文章,原来 go 语言需要写重复代码这个问题在其他问题领域也存在,而社区老司机们解决这个问题的方式是使用机器生成代码。社区大范围这么做的原因是 go 语言的语法非常简单,很容易用 go 生成 go 代码。

写 CRUD 生成器思路很简单,只需要去数据库里查询 table 的详细信息,为不同的 column 数据类型映射 go 数据类型。最后用 go 的 template 实现一些简单的 CRUD 方法。对于一般的 select 查询语句我这里没有去做实现,因为不同的表查询条件是不一样的,这种差异用函数传参解决会更好。生成器做 column 映射和拼接查询语句是极好的。

最后在项目里面把生成器生成的代码和手写的数据库相关代码分离开来,以方便数据库表结构变动后可以让生成器重新生成代码。这里是我写的这个简单的生成器,参照了一部分 xo 项目的代码,由于我只支持 Postgresql, 没有去支持 join 和 has_many 这样的操作,代码相对他们要简单很多。

事情到这里并没有完,我们还需要序列化后的查询结果做一些反序列化的工作,go 语言的 struct tag 对这方面有很好的支持。可是我们用机器生成的代码是无法控制外界需要的各种反序列化的字段。下面以序列化 json 为例,我们并不想把数据库的 ID 字段暴露给外界。如果用生成器去控制这些配置,生成器需要做更多的配置,数据库表字段需要更多约定,这无疑会增加生成器的难度。我们必须要为每一张表手动单独做一个 Export 的结构体与方法,以满足各种其他序列化的要求。

type UserExport struct {
	Name      string `json:"name"`
	Email     string `json:"email"`
  	UpdatedAt string `json:"updated_at"`
}

func (u User) Export() UserExport {
	return UserExport{
		Name:      u.Name,
		Email:     u.Email,
        UpdatedAt: u.UpdatedAt,
	}
}

func (u User) MarshalJSON() ([]byte, error) {
	return json.Marshal(u.Export())
}

如上所示,我们可以为 User 表配置不同 field 和 tag。可以单独为不同的序列化数据类型写一个 Marshal 方法,这些都是手动可以控制。为了灵活我们还可以把 Export 这个结构体直接嵌入到其他结构体中,不用担心 Marshal 会继承式覆盖后面 Marshal 方法。