动机
最近这段时间开始用 go 写网站,遇到各种新老问题。其中一个老问题是如何访问 MySQL 数据库。
使用 ORM (Object Relationship Mapping)
大致浏览了一些热门的实现,感觉不是很合眼缘,跟动态语言的实现(例如 SQLAlchemy )差不少。
ORM 的特点在于使用程序员熟悉的对象概念抽象数据库关系,使之成为可编程的对象,提供使用上的便利:
- 构造查询的便利,用户一般只需要指定需要查询的对象(一般对应数据库表),或者关系(一般对应
JOIN
),还可以按需要增添查询条件,ORM 即可推导出需要执行的语句:
sess.query(User) # SELECT user.id, ... FROM user
sess.query(Employee.User) # SELECT employee.id, ... FROM employee JOIN user ON ...
sess.query(User).filter(User.name==xxx) # SELECT user.id, ... FROM user WHERE name=xxx
sess.query(User).options(joinedload(Employee)).order_by(...) # ...
- 查询后的便利,结果集会被重新组装成对象,后续仍只需继续在这些对象上操作即可。
这样理想状态下涉及数据库的操作就被完全封装到一个闭环里头。然而数据库关系其实并不简单,建立一个表达力十足的映射模型即是使用 python 这种表达力很强的动态语言都很复杂(看看这个函数),何况 go 这种既缺乏元编程能力也缺乏语法糖的语言呢。
因此我所见的实现往往只能退而求其次,只实现一些基本常用的功能。
直接使用 SQL
除了使用 ORM 另外一种方法是直接面对关系数据库/SQL 本身,例如有很多人(声称)直接使用 sqlx,但我感觉全部手写会不会也挺麻烦重复的呢?
所以有一些工具能帮忙生成代码,例如 xo,连接上数据库直接导出 schema 就能生成基本的 CRUD 访问代码,同时也可以根据它的 SQL DSL 生成 SQL 的 wrapper code。整个过程一目了然,没有层层封装的不透明感,生成代码的效率也高,虽然需要手写 SQL,但代码生成过程是经过实际数据库验证的,这就相当可靠了。
因此我认为这种方法在 go 中是最合适的,不过实际使用了一下这个工具后,还是有一些不满意的地方,主要集中在 SQL wrapper code 生成这一部分:
-
有些 SQL 语句需要改写才行,这主要是因为它的实现基于
CREATE VIEW ...
,但不是所有合法的SELECT
语句都可以用来创建 view。例如假设有两个表user
和employee
都有id
字段(这很常见),则SELECT user.*, employee.* FROM user JOIN employee ...
虽然是合法语句,却因为字段重名而不能用来创建 view;而要添加 alias 改写成SELECT user.id AS user_id, ...., employee.id AS employee_id, ...
这样才行。 -
一些 MySQL 相关的问题,例如:#123
-
生成的代码虽然省去了手动拼接手动 scan 的麻烦,但仍然不如 ORM 中操作对象那么方便,例如上边的例子生成的代码大致长这样:
type JoinResult struct {
UserID int32
// ...
EmployeeID int32
// ...
}
只是将结果平铺在一个 struct 中,而不是(我)理想中的:
type JoinResult struct {
User *User
Employee *Employee
}
这样后续我如果需要继续在结果集的 User
上继续操作的话,我需要重新手动装配一次。
改良
因为上述原因,所以决定基于 xo 的方法重新造一个轮子看看能不能改善一点。
首先,直接以 sql.ColumnType(go1.8 添加)而不是从 information_schema
里提取 schema 和 query 的元数据,这样所有合法的查询都能支持。不过这种方法有个缺点,能获得多少元信息取决于这个
RDBMS 的协议返回什么样的信息,例如,postgres 似乎是无法获得 Nullable
信息的;而 MySQL 的协议返回的信息相对比较完整,这也是为什么只支持 MySQL 的缘故啦 :-)
另一方面是增强 SQL DSL,在 sqlw-mysql 中我选择使用 XML 来描述一个查询,例如(quick start 的例子之一):
<stmt name="SubordinatesBySuperiors">
<a name="id" type="...int" />
<v in_query="1" />
SELECT
<wc table="employee" as="superior" />, <!-- 上司 -->
<wc table="employee" as="subordinate" /> <!-- 下级 -->
FROM
employee AS superior LEFT JOIN employee AS subordinate
ON subordinate.superior_id=superior.id
WHERE
superior.id IN (<r by=":id">1</r>)
</stmt>
之所以使用 XML 是因为需要在 SQL 语句片段中间插入一些特殊的指令(元素),这些指令有些有特殊的标记作用;有些则会展开成 SQL 片段来减少一些重复劳动,例子里的 <wc table="employee" as="superior">
(wildcard)指令实际上是会展开成字段列表如
superior.id, superior.employee_sn, superior.user_id, superior.superior_id
上述例子演示的是 one2many
的关系(一个上司对应 0+ 个下级),最终生成的代码节选如下:
// SubordinatesBySuperiorsResult is the result of `SubordinatesBySuperiors`.
type SubordinatesBySuperiorsResult struct {
Superior *Employee
Subordinate *Employee
// ...
}
// SubordinatesBySuperiorsResultSlice is slice of SubordinatesBySuperiorsResult.
type SubordinatesBySuperiorsResultSlice []*SubordinatesBySuperiorsResult
// ...
func SubordinatesBySuperiors(ctx context.Context, q Queryer, id ...int) (SubordinatesBySuperiorsResultSlice, error) {
// ...
}
// DistinctSuperior returns distinct (by primary value) Superior in the slice.
func (slice *SubordinatesBySuperiorsResultSlice) DistinctSuperior() []*Employee {
// ...
}
// GroupBySuperior groups by Superior and returns distinct (by primary value) Superior with
// their associated sub group of slices.
func (slice *SubordinatesBySuperiorsResultSlice) GroupBySuperior() ([]*Employee, []SubordinatesBySuperiorsResultSlice) {
// ...
}
// DistinctSubordinate returns distinct (by primary value) Subordinate in the slice.
func (slice *SubordinatesBySuperiorsResultSlice) DistinctSubordinate() []*Employee {
// ...
}
// GroupBySubordinate groups by Subordinate and returns distinct (by primary value) Subordinate with
// their associated sub group of slices.
func (slice *SubordinatesBySuperiorsResultSlice) GroupBySubordinate() ([]*Employee, []SubordinatesBySuperiorsResultSlice) {
// ...
}
可以看到,除了生成出基本的 wrapper function (SubordinatesBySuperiors
)外,结果集也附带一些辅佐函数 GroupByXXX/DistinctXXX
以便操作,例如可以这样使用:
// 查询 id 为 1~7 号员工的下级
slice, err := models.SubordinatesBySuperiors(ctx, tx, 1, 2, 3, 4, 5, 6, 7)
if err != nil {
log.Fatal(err)
}
// 结果集按 superior 分组,superiors[i] 对应 groups[i]
superiors, groups := slice.GroupBySuperior()
// 迭代每一个上司
for i, superior := range superiors {
// 获得与之对应的组别里全部下级
subordinates := groups[i].DistinctSubordinate()
// 打印
if len(subordinates) == 0 {
log.Printf("Employee %+q has no subordinate.\n", superior.EmployeeSn)
} else {
log.Printf("Employee %+q has the following subordinates:\n", superior.EmployeeSn)
for _, subordinate := range subordinates {
log.Printf("\t%+q\n", subordinate.EmployeeSn)
}
}
}
是不是也还挺方便的呢?
注:关于 <wc>
指令如何确定 Superior
/Subordinate
字段的正确位置文档里有一些说明。
总的而言个人觉得这些改进比较符合我的预期,不过这个方法也有一个比较大的缺点,那就是要起很多名字…