“禁止用 select * 作为查询字段列表”落地指南

一、背景

《阿里巴巴 Java 开发手册》 MySQL 数据库部分,ORM 映射部分,谈到:

【强制】 在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
说明:
1)增加查询分析器解析成本。
2)增减字段容易与 resultMap 配置不一致。
3)无用字段增加网络消耗,尤其是 text 类型的字段。

甚至有些公司还会对代码进行扫描,当发现代码或者 MyBatis 配置中出现 select * 时会给出告警要求修改。
“禁止用 select * 作为查询字段列表”落地指南

规范中将这么规定的原因给出了解释,但是落地时又会遇到一些抉择。

二、问题

先看一个正例和一个反例。

反例:

 UserDO getEmailById(Long id);

对应 xml 语句

<select id="getEmailById" parameterType="java.lang.Long" resultMap="resultMap">
  SELECT * FROM user WHERE id = #{id}
</select>

正例:

 String getEmailById(Long id);

对应 xml 语句:

<select id="getEmailById" parameterType="java.lang.Long" resultType="java.lang.String">
  SELECT email FROM user WHERE id = #{id}
</select>

正如手册上所说的,这种写法带来的好处是:

1)增加查询分析器解析成本。
2)增减字段容易与 resultMap 配置不一致。
3)无用字段增加网络消耗,尤其是 text 类型的字段。

很奇怪的是,很多文章最多提到这里就结束了。

居然就这样结束了???


那么如果查询部分字段怎么办?是继续使用 UserDO 还是定义新的 DO 类?

【1】继续使用 UserDO 作为方法返回值:

<<优点>>:
省事,减少对象定义
<<缺点>>:
无法根据函数名或返回值明确知道哪些属性被赋值哪些属性没有被赋值。

【2】定义新的 DO 对象

<<优点>>:
1)可以根据方法名和返回值,明确感知当前业务获取的字段
2)专用查询和通用查询很好地作区分
<<缺点>>:
当场景较多时,需要定义的 DO 对象过多

如 user 表中有 20 个字段,A 业务需要查询其中 18个字段,B 业务需要其中 8 个字段,C 业务需要所有字段,D 业务需要其中 5个字段,E 业务需要其中7 个字段等等,并且这些场景都是根据 ID 进行查询。

三、抉择

3.1 大逻辑

1)一般情况下多查几个字段,性能差异并不大
2)很多场景下,性能不是我们做决定的最重要因素,代码的可读性、可维护性非常重要
3)编码时要坚持做正确的事,而不是怎么省事怎么来
4)代码要符合设计模式的一些原则,要高内聚弱耦合

3.2 类比

【1】如果你是接口的调用方,服务方给你提供了一个接口,返回的 DTO 里面有 10个字段,你只需要其中的 2 个字段,你就要求对方提供新的接口,只返回这2个字段?虽然这样做性能更好,但实际工作中通常不会这么做。

如果你需要 2 个字段,他需要3 个字段,另外一个人也需要 3 个字段但是字段还不一样,都定义新的接口,服务提供方要崩溃了。

再如领域驱动设计中,领域对象(如 User )不会因为上游防腐层需要几个属性,而返回不同的专有领域对象。

如<<你去市场去买菜>>这个场景,菜农不可能因为你这次只需要 2 个鸡蛋,就摊位上就只能摆 2 个鸡蛋。他通常会把所有的摆放出来,你根据需要自己去挑选。
如 <<你去互联网平台上买菜>> 骑手送菜的场景,此时对于当前订单而言,只应该送给你订单对应数量的蔬菜,而不是把超市所有菜都带来,送到你家门口时,再全部摆出来,让你现场自己数。

“禁止用 select * 作为查询字段列表”落地指南

【2】如果你依赖的二方服务给你返回一个全的 DTO,让你根据调用的方法名去“猜测” 里面哪些属性会被赋值(不看他的源码,你咋知道哪些被赋值哪些没有被赋值),是不是很可怕?

如果你将一个全的 DTO 或者通用的 VO 给前端,不保证所有属性都被赋值,让他根据调用的方法去“猜测”当前场景哪些属性被赋值过,是不是很可怕?

可能有些同学可能会说,给一个文档约定下也可以啊。
可是,有什么能比参数和返回值来约定更合适呢?
后面任何改动都要去增删文档?
人员变动之后代码如何维护?

通常两个选择:
(1)提供一个大而全的,保证有的字段都赋值,上游按需获取;
(2)提供一个专用的对象,被赋值的字段都在这个对象的属性中。

3.3 结论

【推荐】如果业务上明确只需要部分字段时,可以使用通用接口获取所有字段,然后上层只取用需要的字段即可。

[1] 如果查询条件走索引,查询的字段里不含大字段,查询单个字段和查询多个字段的性能差异微乎其微几乎可以忽略不计。
[2] 传统的三层架构,防腐层调用服务层、服务层调用数据访问层,某种程度上是为了复用。使用通用查询接口(通过id 获取整个DO 对象),可以更大程度上实现代码复用。

[2.1] 如上面所说上面不同业务需要不同数量的字段,定义六七个对象比较繁琐,业务需要应该在 DTO 或者 VO 层面控制字段,DO 层面可以复用。
[2.2] 如果你的业务VO 需要下游服务的 3 个字段,那么你要求下游服务单独给你提供只返回这3个字段的 DTO/ BO ?? 显然不合理吧?
[2.3] 不应该让每个查询场景都影响到 DAO 层,如果是这样,那么分层的意义何在?

【推荐】如果需要定制化查询,函数名不能有歧义,要体现出业务含义;不允许使用通用 DO 对象,需使用包装类型或者定义专有 DO 。
反例:
UserDO getUserDetailById(Long id)

这里的方法名是对 “用户详情页面需要字段”的业务描述,还是“用户全部字段”的描述?

UserDO getUserEmailById(Long id)

如果调用方代码较长,后续使用 UserDO 时“要时刻牢记” 这个 UserDO 只有 email 这个属性有值。

正例:
String getEmailById(Long id)
UserSimpleDO getSimpleById(Long id)

[1] 如果使用容易歧义的类通用化的函数名称,返回值是通用的DO,使用方很容易误用。
[2] 创建 DO 工作量并不大,对象的转换也可以通过工具类加以转化。
[3] 符合接口隔离原则,“使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口” 转换下 “不应该依赖不需要的字段”
[4] 符合迪米特法则 Talk only to your immediate friends and not to strangers 当前业务所需的字段才是 immediate friends,其他字段是 strangers ,符合高内聚、弱耦合的软件设计原则

设想一下 如果 UserDO getSimpleById(Long id) 这么定义,你不看 mybatis 的 xml 你知道有多少个属性有被值?
调用方更应该用哪个方法,关注参数和返回值,不应该“*”去了解底层实现。

四、总结

我们在做出抉择时,应该牢记软件设计的一些典型原则,如高内聚、弱耦合;设计模式的几大原则:单一职责、高内聚弱耦合、里氏替换、接口隔离、迪米特法则;降低复杂度等等。

坚持做正确的事,而不是怎么省事怎么来。

不能因为性能而牺牲可读性,可维护性。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
“禁止用 select * 作为查询字段列表”落地指南
上一篇:Web Service 与WebAPI 的区别


下一篇:深入理解 Lambda 表达式