为什么要旗帜鲜明地反对 orm 和 sql builder

标签: 旗帜 orm sql | 发表时间:2022-06-05 16:47 | 作者:Xargin
出处:https://xargin.com/

这个问题在五六年前和前同事讨论过,没想到这么多年过去了,又要跟其他工程师再讨论一遍,有点恍如隔世。

因为我比较懒,所以记录一篇文章,以后碰到类似的问题就不再掺和了。

toC 场景的系统大多要面对较高的 QPS,即使是小型/中型公司使用 MySQL,没有那么高的查询量,单表数据在百万量级也属常见。

无状态的服务当前用 k8s 的 HPA 能力能够做到很好的扩容,结合基本的优化知识,大多数问题能够较好地被初/中级工程师解决。但 DB 一直是非常脆弱的一环,只要 一个工程师不慎将不带索引的查询代码带上线,就会导致线上事故。这样的事故在各家公司都不少。哦,当然,公司为了自己的形象考虑,这样的低级事故一般是不对外说的。

政府/企业对事故的管理一般有一套“事前-事中-事后”的分析框架,可以作为我们处理事故的很好的参考。在开始分析之前,要先简单了解一下这个框架:

所谓事前,就是做事情之前应该有统筹的规划,考虑可能出现的问题和困难,最好提前做实验验证下,以保证在真实现场不出问题,或者即使出现问题也有应对方案,不至于手忙脚乱、大动干戈。

所谓事中,就是做事情的过程中对事前规划的事情进行执行,以及记录过程中出现的问题;整个事中起着承前启后的作用,上对事前的规划进行验证,下对事后的总结反思进行铺垫。

所谓事后,就是做事情结束后对整个事情进行总结反思、问题分类和根因分析等。追踪并确认所有的问题有效进行了解决和应对,以确保下次不会再犯之前同样的错误。

我们就用这套框架来看看现在被人们奉为圭臬的 orm 和 sql builder 到底有啥问题。

事前

无论使用 orm 还是 sql builder,研发人员其实都是不太清楚自己的代码会产生哪些 SQL 的,比如很多工程师会参考一些开源的模板代码,将他们的数据查询层定义类似下面的接口:

  package iface

type CustomerRepository interface {
    func GetCustomer(ctx context.Context, query QueryOptions) (Customer, error)
    func ListCustomer(ctx context.Context, query QueryOptions) ([]Customer, error)
    func DeleteCustomer(...)...
    func UpdateCustomer(...)...
}

接口定义非常简单,查询条件可以随意传入,在整洁架构的核心圈中依赖这套接口可以灵活地组合出产品的业务逻辑。

而灵活和稳定在这里是矛盾的,比如在 domain 中,可能会有人根据用户传入的参数来组装查询条件:

  // 伪代码,别在意语法问题
func GetCustomers(ctx context.Context, userParam map[string]interface{})  ([]Customer, error) {
    var query QueryOptions
    if param["id"] > 0 {
    	query.ID = param["id"]
    }
    if ...
}

用户传入的条件完全不受控,在 MySQL 中,我们不可能为一个笛卡尔积的可能组合去创建索引。所以发生事故就只是运气问题。

若工程师学习过 DDD 的理论,知道基本的贫血模型和充血模型概念,按照充血模型去设计 Repository 的接口,相对来说会稍有改善,但从过往接触过的从业人员来看,懂得这些概念的人很少。

无论使用 orm 还是 sql builder,最终我们都没有办法为可能出现的线上事故做任何充分准备。这本身就是灵活的代价。你又怎么为事故来准备预案呢?

事中

简单来说,就是线上的数据库被打爆了。因为查询没有走索引,MySQL 使用的 CPU 直接飚升。若公司建立了完善的云平台,DBA 或研发可从云平台看到使 DB 出现异常的 slow query。

但问题在于 slow query 中的 SQL 很难与代码直接关联起来,我们还是来看看上面定义的 Repo 接口:

  
type CustomerRepository interface {
    func GetCustomer(ctx context.Context, query QueryOptions) (Customer, error)
    func ListCustomer(ctx context.Context, query QueryOptions) ([]Customer, error)
    func DeleteCustomer(...)...
    func UpdateCustomer(...)...
}

从 DB 平台中已经可以知道是下面这样的 SQL 有问题:

  select * from customer where id != 2

怎么知道是什么样的业务调用链路触发的呢?只能去慢慢读代码了。

在很多公司都见过类似这样的场景:

DBA:"线上 db 快挂了,你看看这条查询,紧急处理一下代码"

RD:“先容我读一下代码,找到了才能改”

DBA:“。。。”

当然,现在的 sql builder 和 orm 框架也可以在 debug 模式打印一些 slow log,但打印行为需要在查询触发时才会有。若未建索引的查询是个万分之一概率进入的分支,碰到事故再开 debug log 大概也来不及了。

事后

别想了,基本也复盘不出个所以然,无非是:

  • 下次 review 一定认真点
  • 我们给上线再加一个流程卡点
  • 下次回归 case 覆盖率要高一点

都解决不了本质问题。

比较好的方案

为了让脆弱的 DB 不要出事,最好的方案还是能够在上线前拦截出所有可能出问题的 SQL。

现在有不少研发会买一些 MySQL 相关的八股课去学习 SQL 知识,基本都是为了应付面试,实际工作中难以良好地运用。真的碰到了 MySQL 的死锁问题,比如 unique key 导致的死锁,还是要去求助 DBA,所以我不是很理解研发去卷 redo log/buffer pool 这些实现原理到底有什么用,有点扯远了。

对于 SQL、索引、死锁,还是 DBA 最为专业,这是他们赖以生存的本事。从工作流程上来讲,我们完全可以让 DBA 参与到研发流程中来,但直接让 DBA 去猜代码中会生成的 SQL 也是不现实的。个人认为 Go 社区中目前比较优秀的解决方案是 sqlc:

从 SQL 文件生成 dao 中的查询代码,将所有查询 SQL 全部显式定义在代码中,在上线流程中可以直接邀请 DBA 参与 CR 中的 SQL Review 环节。

再进一步,可以直接从 information schema 和 sys 库中读取到所有表的索引和访问信息,DB 平台可以自动对代码中的 SQL 进行分析,将不会命中索引的查询进行拦截,从而做到 100% 的事前预防。

由于 sqlc 的作者主要在 pg 环境下工作,所以对于 MySQL 查询的支持并不好,但 sqlc 是当前社区中设计思路最好的数据访问组件,未来我们会参考实现一套对 MySQL 支持更好的工具,希望几个月内能够开源出来。

相关 [旗帜 orm sql] 推荐:

为什么要旗帜鲜明地反对 orm 和 sql builder

- - No Headback
这个问题在五六年前和前同事讨论过,没想到这么多年过去了,又要跟其他工程师再讨论一遍,有点恍如隔世. 因为我比较懒,所以记录一篇文章,以后碰到类似的问题就不再掺和了. toC 场景的系统大多要面对较高的 QPS,即使是小型/中型公司使用 MySQL,没有那么高的查询量,单表数据在百万量级也属常见. 无状态的服务当前用 k8s 的 HPA 能力能够做到很好的扩容,结合基本的优化知识,大多数问题能够较好地被初/中级工程师解决.

文章: MongoDB、Java及ORM

- - InfoQ cn
目前有很多互相竞争的NoSQL产品,它们使用的方式不尽相同,但都能很好地解决大数据问题. MongoDB就是其中一款非常不错的产品. MongoDB是面向文档、无Schema的存储解决方案,它用JSON风格的文档展现、查询、修改数据. MongoDB有很丰富的文档,安装和设置都很简单,而且易于扩展.

Java 8:ORM已经过时了

- - Java译站
最近几十年来,关于ORM究竟还有没有用的争论一直不断. 很多人承认Hibernate和JPA确实很好的解决了不少实际的问题(通常是复杂对象的持久化),但有些人认为,对于面向数据的应用而言,复杂的映射关系则有点大材小用了. JPA通过在目标类型上使用硬编码的注解,来建立标准的声明式的映射规则,进而完成映射关系.

Android orm 框架xUtils简介 - lsc183

- - 博客园_首页
  数据库操作建议用ORM框架,简单高效. 这里推荐xUtils,里面包含DBUtils. github地址:https://github.com/wyouflf/xUtils.   获得数据库实例建议用单例模式.   创建一个实体类,对应数据库中的表.     private int id; //主键ID,必须.

看完不懂,你拍我——ORM框架中的对象状态

- - CSDN博客架构设计推荐文章
        不论是EJB3.0还是Hibernate,他们统称为ORM框架. 虽然每个框架对对象的状态都有自己的理解,但是都免不了有共性的东西.         在Hibernate中,对象的状态分为Transient、Persist、Detached,而在EJB中有四种状态,分别为:New、Managed、Detached、Removed.

最受欢迎的5个Android ORM框架

- - 极客521 | 极客521
在开发Android应用时,保存数据有这么几个方式,. 一个是本地保存,一个是放在后台(提供API接口),还有一个是放在开放云服务上(如 SyncAdapter 会是一个不错的选择). 对于第一种方式,即本地数据存储,如嵌入式 SQLite,你可以选择直接使用SQL语句、Content Provider 或 ORM(对象关系数据映射).

PL/SQL动态SQL(原创)

- - ITeye博客
使用动态SQL是在编写PL/SQL过程时经常使用的方法之一. 很多情况下,比如根据业务的需要,如果输入不同查询条件,则生成不同的执行SQL查询语句,对于这种情况需要使用动态SQL来完成. 再比如,对于分页的情况,对于不同的表,必定存在不同的字段,因此使用静态SQL则只能针对某几个特定的表来形成分页.

Android工具库xUtils1.9.8发布-orm性能优化,图片加载优化...

- - 开源中国社区最新新闻
感谢关注xUitls的网友最近一段时间给予的热心反馈,xUtils近期做了很多细节优化之后,功能和api已经稳定.         1.9.8主要更新内容:.         * orm模块添加列类型转换接口,支持自定义类型字段作为列映射;.         * bitmap模块优化默认参数,取消默认动画,加载更快速.

Derby SQL 分页

- - ITeye博客
    之前在网上看到有人问 Derby SQL 分页实现的问题,网上有人给出这样的解决方案,SQL 如下:. 其实,这样的分页查询,性能不理想,我试过在 300W 数据量中采用这种分页方式,需要 20~30秒之久;其实 Derby 10.6 以上版本有更好的分页支持,直接给出 SQL 实现如下:.

SQL Server--索引

- - CSDN博客推荐文章
         1,概念:  数据库索引是对数据表中一个或多个列的值进行排序的结构,就像一本书的目录一样,索引提供了在行中快速查询特定行的能力..             2.1优点:  1,大大加快搜索数据的速度,这是引入索引的主要原因..                             2,创建唯一性索引,保证数据库表中每一行数据的唯一性..