为什么要开发 Gendry

发布于 2021-07-03 21:56:41 字数 4147 浏览 1325 评论 0

在 Gendry 项目发布之前,公司有其它部门的同事问我:

看起来,这个项目又重复造了一个的 sql builder 的轮子,有必要吗?

这个问题我没有直接回答他,因为事实上很多问题的答案不是简单的 Yes 或者No。 从我个人而言,我是极力反对重复造轮子的。在项目中我也极力倡导:

  • 使用标准库
  • 使用 github 上高质量的 lib
  • 如果没有现成 lib 能完美解决问题(其实很多是边界 case),就基于它改,可能的话也为该项目加 feature

你可能会觉得,目前 awesome-go/database 上已经有足够多的 databaseTools 可以用了,那为什么还要选择自己重新开发一个呢?

不喜欢ORM

当然这里我不是说 ORM 不好或者什么,ORM 也有很多簇拥,像 GORM 或者 beego 都是比较成熟的ORM,使用者也很多。

但是由于 ORM 的封装,很多时候导致我们在进行开发的时候难以知道自己调用的某个API是在干什么。ORM 通过高阶的封装,通过描述式的API来帮助书写 sql,虽然非常 Object-Oriented 但会让使用方对最终会生成的sql比较困惑。

同时,随着业务的发展,数据库会逐渐成为系统的瓶颈,对 sql 进行调优可能是一项比较重要的工作。而使用ORM,手动的 sql 调优比较麻烦。因为 ORM 提供了高层次的抽象,想在 ORM 的框架中做特殊优化需要对ORM本身特别了解。

事实上,跳过对象关系手动调优sql这件事本身就破坏了 ORM 的面向对象的属性。还有一个原因是,ORM 非常详尽的文档让我比较头疼和畏惧。

sql builder

对于我们内部,如果不用 ORM,那么接下来就需要考虑使用 sql builder 来帮助构建 sql 语句。当时我们也调研了几个在 github 上质量比较高的 sql builder。所谓质量较高,其实指的就是:

  1. stars 多
  2. issue closed 多
  3. test 覆盖全

以上三项的顺序也是我自己判断一个项目质量的优先级。其实 star 多并不能说明一个项目的质量,但是大概率可以看出这个项目的使用覆盖度,越多的人用越有可能遇到 bad case 来帮助项目改进,也越有理由认为项目更加健壮。

由于当时开发本模块是2016年比较早的时候,那时很多lib都还不是很完善,star少,测试覆盖不全。最最重要的,也是Gendry和同类sql builder不一样的地方是,很多Lib都是侵入式的。

也就是说,它不仅帮你构建sql,同时帮你执行sql,也就意味着它帮你持有*sql.DB对象。不能持有DB对象意味着失去了真正对sql语句的执行权限,也就意味着项目与lib强绑定。对于一个新项目来说倒也没什么,这也算不上什么大问题,我们没有选择当时的一些Lib,主要原因是:

  • 当时的这类 lib 质量还没有说服力(star 少、issue 少、文档不像现在这么完善)
  • 大多数 star 稍微多一些的 lib 提供了太多的 API

你可能会觉得疑惑,API 太多也是问题吗?对于我个人来说,这是一个问题。过多的 API,就代表了更多的复杂性(虽然也意味着可能更多的功能),在使用的时候就需要学习更多的概念。

对于我个人而言,我更喜欢把时间花在学习标准库的用法,而不是Lib的用法。因为lib可能随着不同的项目而切换,可能会过时(这个问题做前端的朋友体会更加深刻),但是标准库永远都是一样的(对于大多数有节操的语言)。

过多的 API 也模糊了 lib 本身的定位,作为一个 sql builder,哪些事情是它真正需要做的? 还有另一个和ORM相同的问题,如果我想手动优化 sql,而sql builder 又接管了执行权,这就很不好搞了。

而对于滴滴这种请求量巨大对数据库使用经常要小心翼翼的公司来说,优化 sql 可能是家常便饭。 在众多的sql builder中,dotsql 其实是一个非常不错的 lib,抽离出完整的 sql 这种方式也很好,至少对于 DBA 查看业务sql来说非常方便。这种方式在 JAVA 和 C++ 中都有比较多的运用,但是 dotsql 也有一些问题:

  • 接管了 db 的执行权
  • 在代码中使用别名去引用写在别处的 sql 语句,在对占位符进行填充时,很难把当前值和填充对象匹配上,写着写着你就不知道当前这个变量会填到 sql 的哪个占位符上了

对于上面说的第二条,也就是说当执行 dot.Query(db, "sqlname", "hello","my","friend") 的时候,你经常会不知道 my 对应原 sql 中的哪个占位符。当然,这可以通过多屏或者 IDE 插件来解决,但是这是不是又引入了更多的复杂性,况且 IDE 插件。

另一个问题是很多时候带 where in 的 sql 查询业务一般都不知道 in 中需要填多少数据,通常是根据上一个 sql 查询的返回结果来填的。也考虑过给 dotsql 加 feature,比如对 in 中的占位符特殊处理,但是想到 go 并不支持以下写法 Query(db, "somesql", "a", someSlice..., "b") 而作罢。

裸写 sql

实际上我们内部进行过一次讨论,大部分人认为裸写 sql 是一个比较好的解决方案。裸写 sql 即可以让 DBA 来审核,业务方也特别方便对 sql 进行优化。

但是裸写sql可能会引入很多重复的劳动,因为每个 sql 都是提前写好的,所以对于同一个表的查询,增减一个select字段,增减一个where条件都需要单独写一个sql(意味着不同的执行函数),当然 in 查询由于其目标参数个数的不确定性,仍然需要做特殊处理和动态拼接。因此我个人认为裸写sql也不是一个很好的方案

综合来讲,我们对 sql builder 的需求是:

  • 简单,不要有太多概念,不要引入歧义,用户应该花时间去学标准库而不是 sql builder
  • 非侵入式,任何项目只要使用标准库,没有任何引入成本,想用就用,想摘除就摘除
  • 在上面两条的前提下,尽量方便使用者,减少代码量

对于我来说:

  • 一个 Lib 是否简单就是:能否不看文档只看 example 就知道怎么用
  • 非侵入式就是:尽量让用户使用标准库,lib 为执行标准库生成参数,或者至少做到给 lib 不成为一个强依赖
  • 方便使用者是说:lib 尽量满足高频次的需求,因为对一个对象进行封装势必就会减少对该对象的控制粒度。

二八法则是我们对对象进行封装的源动力,因为有80%的场景某个API的某些参数都不需要填,某些 case 都不需要处理,因此我们屏蔽掉它以便我们使用。

对于剩下20%的场景,如果Lib是非侵入式的,我们可以不用这层封装,直接使用更基础的API来解决问题。因此Lib的自我定位是非常重要的,像标准库,很多人认为net/http中用于发送http请求的client非常难用,每次去填充一个Request相当伤脑筋。

其原因就是标准库的定位是要能支持所有场景,所以它不得不这么做。如果我们大部分场景下不需要对HTTP头进行复杂的设置,不需要各种Hijacker,我们可以简单地使用net/http中的Post或者Get方法,或者根据自己的实际场景进行封装。

但是千万切忌过度复杂的封装,如果lib支持的功能太多,一个API的参数列表太长,意味着使用者需要学习更多概念。这样的话,使用方没有理由不去用标准库而使用第三方库,这样的第三方库大概率没有人会用。所以我认为开发一个功能大而全的库是无效劳动,用最简单的API解决大部分问题,才是lib该做的事情。

因此,你可以看到,最开始那个问题真的不是那么容易回答。我们一方面由于许多原因不喜欢ORM,另一方面又觉得当时的sql builder要么对自身功能定位不准确过于复杂,要么耦合性太强牵一发而动全身,考虑到sql builder本身并不复杂甚至可以说很简单(和sql parser简直不可同日而语),因此我们按照自己的审美自己造了一个符合我们理念的“轮子”。

由于在公司的多个项目中都表现稳定,同时又经过一年半的小步迭代,代码已经趋于稳定,因此考虑开源出来。Gendry是一个很小的lib,如果能够帮助到大家少写很多代码早点下班,我们就非常开心了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84959 人气
更多

推荐作者

yangzhenyu123

文章 0 评论 0

lvzun

文章 0 评论 0

执笔绘流年

文章 0 评论 0

芯好空

文章 0 评论 0

始于初秋

文章 0 评论 0

谁与争疯

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文