数据库的持续集成和版本控制
在提出版本化数据库工作是一个必要规则这一观点之后,Scott Allen又详述了一个做好版本化数据库的方法,他给出了一个易于理解、实践性很强的方法,通过创建基线、使用变更脚本的方法来管理数据库的修订、控制程序化数据库对象(如视图、存储过程、函数和触发器),并充分利用分支和合并。
Allen在发布了关系型数据库开发的三个原则的经验总结文章之后,就开始写后续的系列文章了。这三个原则如下:
一、不要在共享数据库服务器上进行开发工作
就像软件开发中其它所谓便捷的方法一样,共享数据库的使用也是一个泥潭,它正等着冻结一个项目呢。开发人员相互覆盖彼此所做的修改,我在服务器上所做的改变让你的开发机器上的代码中断运行,这些都让远程开发速度很慢而且非常困难。避免使用共享数据库,也就免避了因使用它造成的极度时间浪费和因之而生的Bug。
二、仅保留一份权威的Schema生成源
每一个人都必须知道该从哪里获得正式的Schema,并且可以用它轻松地重新创建一个新的数据库。当我走到电脑前,可以从源码库中获得最新的版本,构建后就可以通过最简单的工具创建数据库(在更多的场景中,构建的过程甚至可以在数据库不存在时自己创建一个,所以这个构建过程应该是一步到位方式的)。
三、对你的数据库进行版本管理
这样做的原因之一就是要将变化由开发传递到测试,最终在一种可控制的、一致的环境下生产。其二就是可以重建任何时间点上的数据库,如果你正在将软件交付给客户的话,这一点就尤为重要。如果有人在你提交的应用版本build 20070612中发现了bug,你就必须能重建当时那个版本的状况——包括数据和其它所需。
Allen说明了版本化数据库的目的就是为了能保证所做的改变能保持一致性、可控性、可测试性和可重现性。许多推广者都同意这一点,并认为实现这个目标对任何一个敏捷团队的效率都很重要。
在列出了版本化数据库的重要性后,Allen又相继发布了4个贴子来描述他推荐的实现方法。
其中,第一篇贴子描述了Allen宣称的版本化数据库的起点——创建一个数据库Schema基线。从本质上来讲,这个基线是一个脚本,或者是一连串脚本,它包含所有可以从零开始生成应用数据库的SQL命令。它包括创建所有数据库所有对象(表、约束、函数、视图、索引等)的SQL命令、表查询及操作命令和插入应用所需初始数据的命令。Allen建议,一旦它完成创建并且验证无误,应立刻“将它提交到源码控制库”,此时“你可以认为已将数据库基线化了”。
对于如何创建这个基线,Allen建议使用那些能从现有数据库中导出脚本的工具(与手工编写他们的过程相反)。作为参考,他还描述了他是如何结构化那些生成的脚本文件的:
我喜欢将所有生成表、约束、缺省值和主键的SQL语句保存到同一个文件中,而那些创建视图、存储过程、函数的脚本则分开来单独存储。
如果你喜欢多文件保存的方式,那就需要一个批处理文件,shell脚本,应用程序,或其它自动化工具来自动定位并运行安装数据库需要的所有脚本文件。人工干涉这个过程是一种倒退。
Allen建议并强调,基线中需要一个表用来记录任何有关数据库结构的改变,在他后面的三个贴子中,他详细描述了该如何处理这些变化。
首先,Allen讨论了变更脚本——一种管理除视图、存储过程、函数以外的数据库对象的机制。这种方法要求任何一个改变(或一组相关的改变)必须有一个新生成的脚本文件可通过“增量”更新的方式来代表,这与Ruby Migration很相似。换句话说,当团队发现数据库需要做改变时,他们创建一个新的脚本来将数据库修改到想要的样子,通过测试后提交到源码控制库中。一旦发布后,这个脚本就永远不要再修改。
Allen这么做使视图、存储过程和函数的更新方式与其它数据库对象完全相反,每个对象都有一个“创建命令”文件,然后通过更新这一个文件来更新这些对象,对于为什么他喜欢这样做,他解释到:
原因很简单,就是为了更快速地确定问题所在。如果有人提交了一个数据库结构变化,它移除了视图所引用的一个列,那么你可以尽早地发现有错误,因为在构建版本提交到测试以前,这个问题就会被发现;同样,如果有人提交了一个视图,但却忘了发布它所需要的结构改变,几分钟后就会有人跑到他们的桌子前问他们为什么要破坏软件的运行。
另一个原因就是为了避免我遇到过的某些不太常见的错误。对于那些隐匿在视图背后的Schema的变化,某些数据库产品仍会强迫完成执行计划,而由此引发的问题很难跟踪。“扔掉所有的东西,重新开始”会避免发生这类事情。
Allen着重强调了利用自动化工具更好地实施上述策略的重要性:
当开发人员、测试人员或者安装人员从源码控制库中更新并运行本地数据升级工具时,它就像会魔术般地完成工作。它有三个步骤:
1、通过对比现有数据库结构变更脚本文件和SchemaChangeLog表中的记录,来应用最新的数据库结构;
2、删掉数据库中所有的存储过程、视图和函数;
3、运行所有的变更脚本将视图、存储过程和函数添回到数据库中去。
对于遵循这些策略的好处,尤其是使用自动化工具,Allen给出了一些示例:
由于数据结构变更脚本保存在源码控制库中,你可以在任何时候重新创建任意时间点上的数据库。如果客户报告了一个关于build 3.1.5.6723的bug,那么,你所需要做的就是获取相应版本标号或标记过的源码,然后运行这一基线和这一标记下所有的数据库变更脚本。当其运行结束后,你就已经有了一份与客户发现bug时一模一样的数据库,也就获得了一个重现这个bug的好机会。而且,当改变由开发阶段进入测试阶段时,就从根本上提供了一个一致的、有序的、可重现的产品。
Allen在这一系列文章的最后,还提及他是如何处理分支与合并的,而这是所有应用在版本服务器上建立它的第一个版本后都要面对的现实问题。Allen建议为发布而分支,这也是他偏好的分支策略,同时解释了他为什么会为每个新的发布版本重创数据库基线。他通过一个示例来描述了这个问题,并添加了这样一个场景:在较早版本中发现了缺陷,就必须对已分支的版本进行相应的数据构结构变更。在这个分支版本中,创建新的脚本来处理变更是没有问题的,问题是,如何将这个变更也应用到当前的主线版本中:
想要在主线中修复它,有两种选择。实际上可能会有无数种可能性,这取决于你想如何应用你的更新。但这里只提供两种选择:
1、将数据库变更脚本合并到当前主线版本01.00.0046的脚本中,并在基线版本2.0中进行相应的修复来处理这一变更;
2、写一个新的数据库结构变更脚本02.00.0003,其与分支版本46中的变更保持一致。
对于选项一,你必须小心处理,因为任何已经更新到v2.0版的数据库都并不会从分支上获得46号变更脚本(除非你编写的工具与我的不一样)。你只能让别人手工运行这一脚本,或者你自己查看对与现存的2.0版本数据库冲突(对于这种结果,无论如何仅限于在开发和测试机器上)。所以,除非你刚开始着手2.0的开发不久,否则这个选择并不算太好。
相比之下,选项二则要友好多了。1.0版本的数据库将会从01.00.0046中获得修复。2.0版本的数据库则在02.00.0003中得到修复。但你也要很小心地去编写02.00.0003的修改脚本,以免它履盖运行01.00.0046脚本后所做的修改。
换句话说,数据库是按照2.0版的基线脚本安装的,必须要应用02.00.0003脚本,但实际的产品数据库可能是从1.0版开始的,它将应用01.00.0046脚本来修复,所以你不能让02.00.0003去再次修改这个实际已经升级到2.0版的数据库否则,会造成错误。