Skip to content

为什么要开发Gendry

caibirdme edited this page Dec 1, 2017 · 1 revision

中文|English

为什么要开发Gendry

在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,如果能够帮助到大家少写很多代码早点下班,我们就非常开心了。

Clone this wiki locally