在数据库中对数据的操作可以分为两类:查询和命令。由于命令会修改状态(数据),因此会得到开发团队很高的关注。甚至我所在的组织里,对故障的分级也是对此参照的:如果是数据显示错误,则属于轻度故障;如果是数据更新错误,则属于严重故障。但查询并不总是善类,比如在《基于Oracle的SQL优化》一书中,作者曾提到一个案例:优化一个关联了18个表的查询。在赞叹作者高超的SQL调优技术的同时,我不禁想到是否真有必要编写如此复杂的SQL?复杂查询带来可能不仅仅是性能问题,其背后隐藏的还有可能是不恰当的架构和设计。查询应该受到更多的关注。
控制条件查询的复杂度
企业应用程序的开发人员经常和数据库打交道,有时会过于依赖SQL强大的查询能力,随着需求的演化,SQL可能会越来越复杂,难以维护。
通用的条件查询
比如考虑一个酒店预订网站,客户在下单后的30分钟内需要完成支付或是提供信用卡担保,否则系统会自动将订单取消。开发团队使用定时任务来筛选出“过期”的订单:
图表 1 使用SQL条件查询
看上去也挺简单,但是订单作为一个主要的业务对象,会有许多查询的需求,一般都会实现动态条件查询。
图表 2 构造条件查询,要求orderRepository的实现能够动态拼接SQL语句
条件查询虽然好用,但测试起来可不轻松。因为在依赖数据库的自动化测试中准备、清理数据相比单元测试更麻烦。每个测试用例的上下文最好是独立的,这样不容易因为测试用例执行顺序问题导致测试结果不一致。此外,测试数据相互独立对于维护性也很重要,当测试用例达到一定规模时,“复用”测试数据以减小工作量的初衷是好的,但是很容易让测试用例的设计顾此失彼,适得其反。不必要的条件查询和其组合加剧了这个问题。另一方面,随着需求演化,有些查询条件还需要额外关联一些表,比如,现在要求VIP会员不受自动取消过期订单的约束。SQL可能变成了:
图表 3 随着需求演化,SQL开始变得复杂
于是不断有新的条件和关联表被加进来,而测试时要准备的数据也越来越多。在实际项目的iBATIS sqlmap文件中(此处的例子都经过简化),改造前拼接动态查询的篇幅超过了200行,可以根据条件动态关联对应的表,貌似十分“强大”,实际上看起来都很费劲,添加新功能经常导致部分查询的条件拼接不完整,非常痛苦。
查询 “结果”
对于按客户名称搜索订单,还是条件查询比较好用。但对于特定目的,又带有计算的查询就不是那样了。实际上,我们可以把计算的结果用来实现查询,而不是在查询中去计算。开发团队为了解决这个问题,后来改造了下单的过程,计算并保存了订单的截止时间并为其实现了一个专用查询。
图表 4 保存计算的结果,计算在下单时完成,很容易做单元测试
图表 5 查询结果
随着设计的变化,测试策略也相应调整,现在由ApplicationEvents的单元测试来验证关键的业务条件,而查询可能过期订单的测试由于和订单这个“热门”隔离开了,测试的数据准备和清理不容易发生冲突,也变得更为简单。
慎用数据库集成
在实现修改状态的功能时,开发团队往往会定义良好的接口和服务契约来降低构件间的依赖。而在处理查询功能时,开发团队有时会迫不得已使用数据库集成,但这种方式却悄悄地削弱了解耦的努力。
数据来自于两个系统
让我们回到酒店预订网站的例子,现在希望提供住客点评酒店的功能。但为了防止“灌水”,要求对每张订单只能点评一次。
图表 6 订单中心可以点评
在系统实现上,酒店点评是独立于酒店订单的一个应用程序。如果只是在查看订单详情时才显示点评的入口,那么开发团队会很自然地想到通过应用程序集成,比如由酒店点评系统提供一个查询某张订单是否有对应点评的服务。不过若在订单列表就要显示的话,开发团队觉得还是直接使用数据库集成更简单一些,而且还减少通信次数。改造一下订单列表的查询语句,在其中嵌套点评计数的语句:
图表 7 数据库集成
查询“本地”数据
但很快,开发团队发现这也许不是一个明智的主意。一方面,这个方案会增加订单查询的持久化测试用例,而且为了能够部署流水线中运行这些测试,还不得不把本属于酒店点评系统的数据库脚本复制过来。而另一方面,开发团队发现这个方案其实要求两个系统合用一个数据库,或至少使用DBLink才行。这种部署灵活性上的缺陷也给部署流水线的准备带来了不少的麻烦。
图表 8 部署限制
于是,团队调整策略,在t_hotel_res_order上增加了点评数量的冗余字段,而该数据采用消息队列来同步,这样查询就可以在“本地”实现了。
图表 9 使用MOM集成
Fail fast Learn fast
一般来说,一个功能总是可以通过多种方式来实现,除了性能之外,可维护性也是很重要的决策因素。开发团队的经验固然很重要,但尽早开展测试来评估、验证方案往往更科学。
参考实现方案对应的测试方案及成本:如果要为某个查询的测试用例准备5、6个表的数据,那么值得考虑一下是否有查询“结果”的方案。尤其是报表类的功能,非常容易演化成长篇的SQL语句。
参考实现方案对应的部署方案:尽早建立部署流水线,尽早规划部署资源和方案并演练。如果出现不必要的“限制或约束”,那么值得考虑更换集成方案。
原文出自:INFORQ