写了一个 MySQL 数据表和查询的 go 代码生成器

374 阅读5分钟
原文链接: huangjunwen.github.io

项目地址 github.com/huangjunwen…

动机

最近这段时间开始用 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。例如假设有两个表 useremployee 都有 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.ColumnTypego1.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 字段的正确位置文档里有一些说明。

总的而言个人觉得这些改进比较符合我的预期,不过这个方法也有一个比较大的缺点,那就是要起很多名字…