数据库版本控制完全指南
在这个充斥着大数据与商业智能的新代时,唯一不变的技术就是变化,尤其是在数据库方面。出于数据统计、继续增加的对服务的需求,以及规定制度等方面的原因,几乎每天都有业务方面的变更需求,这些都会对数据库产生变更需求。当数据库变更发生时,能否从自动化中获得更大的敏捷性,以较少的资源实现较多的功能,正是那些具有高度竞争力的世界级企业在芸芸众生中脱颖而出的关键因素。
如果你的竞争对手能够更快地、并且交付质量更好的特性,那么 你必然会失去市场份额。敏捷开发方法的出现正是为了在应对不断变化的需求的情况下快速地发展,在有限的资源下也能够确保理想的质量。
重量级发布的方式已经过时了,为了每次更新或发布要等上足足六个月,这种方式无异于自掘坟墓。敏捷开发方法减少了每次发布的范围,换取的是更快地完成每个变更,并且将每个变更的影响降至最低。对于技术公司与IT部门来说,必须以敏捷性来保证对不断变化的业务需求的支持。
接下来的一个逻辑步骤是将开发与运维相结合,即采用DevOps方法。
为了在敏捷的Sprint发布中有效地应用DevOps,你需要实现部署与流程自动化,自动构建内部的开发与QA环境,以及生产环境。否则的话,你只能选择手动实现部署与发布的每个步骤与流程,这就很可能产生人为的错误,而且也无法频繁地重复这一过程。
实现自动化依赖于版本控制系统,它能够管理所有等待构建并部署到下一个环境的软件资产。
构建流程的第一个步骤是清理工作空间,并从版本控制库中换取相应的文件。这一重要的步骤避免了流程外的变更。如果开发者直接将变更保存到构建服务器的工作空间,而不是将变更签入版本控制库中,那么这种变更仍然有可能产生。这个例子听起来似乎有些可笑,因为开发者都明白,如果不把变更签入到版本控制库中,这些变更就会丢失,因为技术手段保证了流程的正确性。这一步骤同时也避免了将尚未完成的变更包含至构建过程中,只有通过正确的签入流程提交至版本控制库中的变更才会加入构建过程。版本控制库在这里成为了 唯一的信赖源。
数据库是关键的部件
如今多数的IT应用程序都包括数量众多的部件,使用了多种不同的技术:移动、ASP、PHP、应用程序服务器、Citrix及数据库等等。为了让应用程序正确运行,必须让这些组件相互配合。下面举个例子,如果在某张表中加入了一个新的列,或是在某个存储过程中加入了一个新的参数,那么为了让功能正常运行,其它所有的应用程序组件也必须与结构的变化进行同步。一旦同步过程出错,应用程序在调用存储过程时使用了错误的参数,或者是在插入数据时遗漏了新的列,应用程序就会出错。
数据库组件的独特性将它与其它组件区别开来:
- 数据库不仅仅只是SQL脚本,还包括了表结构、在存储过程中用数据库语言编写的代码、在引用表或配置表中所存放的内容,以及对象之间的依赖等等。
- 数据库是集中式的资源,多个开发者可以在同样的对象上进行工作,因此必须对他们的工作进行同步,以避免代码相互覆盖。
- 数据库变更的部署不像拷贝与替换旧版本的二进制文件那么简单,在将数据库从版本A转换至版本B的过程中,需要在保留业务数据的同时转换为新的结构。
- 数据库代码直接存在于数据库中,并且可以在任何环境中进行直接修改。这一点就与其它的组件不同,它们都是在构建服务器中的某个干净的工作空间中进行编译的。
必须满足的需求
在管理数据库变更时,需要克服一系列的困难,你必须做到以下几点:
- 确保所有的数据库代码都被涵盖(结构、代码、引用内容和授权等等)
- 确保以版本控制库作为唯一的信任源
- 确保被执行的部署脚本在运行时能够正确判断环境状态
- 确保部署脚本能够正确处理与合并冲突
- 只为相关的变更生成部署脚本
- 确保部署脚本了解数据库的依赖项
对于在开发阶段,以及内部部署(开发环境与QA环境)或部署到生产服务器时的数据库变更管理,有四种通用的管理方式。
- 建立开发阶段生成的SQL变更脚本
- 建立一个变更日志的跟踪系统
- 建立简单的比较与同步机制
- 建立一个数据库执行变更管理解决方案
建立开发阶段生成的SQL变更脚本
管理数据库变更的最基本方式,就是将所有变更命令保存在一个或一系列脚本中,并且在基于文件的版本控制系统中对它们进行管理。以此保证在一个单一的存储库中保存所有的应用程序组件资产。对于开发者来说,将数据库变更进行签入可以使用的功能类似于他们签入.NET或Java的变更时的功能,例如将变更与变更原因(变更请求、缺陷编号、用户故事等等)相关联。对于当前流行的各种基于文件的版本控制方案来说,基本上都能够在多个开发者对同一个文件进行变更的时候发出合并的警告。
但让我们来看一下,这个解决方案是否真正能够克服数据库方面的各种挑战,并且避免各种可能的风险呢:
- 确保所有的数据库代码都被涵盖 —— 由于开发者或DBA编写了脚本,因此他们能够确保所有数据库代码都被涵盖。
- 确保以版本控制库作为唯一的信任源 —— 并非如此。因为开发者和DBA能够直接登录(任何环境的)数据库,并且直接在数据库中进行变更。
手动编写的SQL脚本
对于部署脚本的变更,例如发布内容范围的变更、分支合并及重复劳动等等必须手动完成,并且需要额外的测试。
这种情况下需要维护两种类型的脚本,即对这次发布的创建脚本,以及针对某些特定变更的变更脚本。对于同样的变更需要维护两种脚本,这已是灾难开始的前兆了。
- 确保被执行的部署脚本在运行时能够正确判断环境状态 —— 这一点取决于开发者,以及脚本编写的方式。如果脚本本身只包括相关的变更命令,那么它对于执行时的环境状态就一无所知。这就意味着即使某个列已经存在,它也会试图再次新增这个列。而如果要编写能够在执行期判断环境状态的脚本,会极大地提升脚本开发工作的复杂性。
- 确保部署脚本能够正确处理与合并冲突 —— 虽然基于文件的版本管制系统提供了合并冲突的功能,但这一点对于数据库来说意义不大,因为版本管理库里的内容未必是百分之百准确的,因此也无法充当唯一的信任源。脚本或许覆盖了另一个团队所做的某个hot fix,而这种错误不会留下任何痕迹。
- 只为相关的变更生成部署脚本 —— 脚本的创建是属于开发过程的一部分。根据所布置的任务,如果要确保脚本中只包括相关的、并且经过授权的变更,必需对脚本进行改动,而这进一步提高了部署时的风险,并且也会浪费时间。
- 确保部署脚本了解数据库的依赖项 —— 开发者必须在编写脚本时留意到数据库的依赖项。如果只使用一个唯一的脚本,那么变更通常是按顺序从脚本的最后加入的,这就有可能导致对相同的对象进行多次变更。而且如果要使用多个脚本,那么脚本的执行顺序则至关重要,并且必须手动维护。
结论:这种基本方式不仅无法克服数据库的各种挑战,并且极易出错,也极耗时间,并且需要引入一个额外的系统以跟踪被执行的脚本。
建立一个变更日志的跟踪系统
另一种常见的方式是使用XML文件作为一种抽象的语言,对变更进行描述并对执行过程进行追踪。这方面最常见的开源解决方案就是Liquibase。
Liquibase使用XML文件将逻辑变更从物理变更中分离出来,并且允许开发者在不了解数据库的特定指令的情况下编写变更。在执行期间,它会将XML转化为特定的RDBMS语言以执行这些变更。所有的变更将被组合到一个变更日志中,日志可以作为一个单独的XML文件存在,也可以是由一个包含了变更顺序的主XML文件所引用的多个XML文件共同实现。
可以用现有的基于文件的版本控制系统保存这些XML文件,这一点与基本方式的好处是相同的。此外,通过Liquibase的执行跟踪能力,还能够了解到哪些变更日志是已经被部署过,不应该再次运行的,以及哪些是尚未部署而等待运行的。
那么让我们来看一下,Liquibase是否解决了这些挑战呢:
- 确保所有的数据库代码都被涵盖 ——Liquibase中的XML文件不支持对引用内容变更的管理,必须由外部的扩展功能进行处理,这就很可能导致某些变更被遗忘。
- 确保以版本控制库作为唯一的信任源 ——Liquibase本身没有任何版本控制的功能,它依赖于第三方的版本控制工具对XML文件进行管理。因此,你还是必须想办法保证基于文件的版本控制库能够正确地反映当前被测试的数据库版本。为了确保版本控制库能够作为唯一的信任源,开发者必需将变更签入,以便进行测试。这有可能导致尚未完成的变更也被部署到下一个环境中。
- 确保被执行的部署脚本在运行时能够正确判断环境状态 ——Liquibase知道哪些变更日志已经被部署,并且不会再次执行它们。但是,如果某个逻辑变更是增加一个日期类型的列,而该列已经存在,并且是varchar格式的,那么部署肯定会失败。此外,Liquibase也无法避免外部进程对数据库进行的任何变更。
- 确保部署脚本能够正确处理与合并冲突 ——在Liquibase之外对数据库进行的任何变更都可能导致冲突,而这是Liquibase无法处理的。
无法处理外部进程产生的变更
- 只为相关的变更生成部署脚本 —— 在变更日志这一级别可以忽略某些变更,但将一个变更日志分解为多个日志需要重写编写XML文件,而这也需要更多的测试。
- 确保部署脚本了解数据库的依赖项 —— 在变更日志XML文件的编写过程中,需要手动维护变更的顺序。
结论:使用能够追踪变更执行的系统并不能处理数据库开发中的所有挑战,最终也无法胜任部署的需求。
建立简单的比较与同步机制
另外一种常见的方式是通过将源(开发)环境与目标(测试、UAT、生产等等)环境进行比较,由此自动生成数据库变更脚本。这种方式节省了开发者与DBA的大量时间,因此他们无需手动地对每次发布的创建脚本或变更脚本进行手动维护了。只在需要的时候生成对应目标环境当前结构的脚本。
让我们再来看一看,这种方式是否能够应对数据库管理的挑战:
- 确保所有的数据库代码都被涵盖 —— 多数的比较与同步工具都了解如何处理不同的数据库对象,但其中只有一部分工具能够在比较与同步时处理引用数据。
- 确保以版本控制库作为唯一的信任源 —— 简单的比较与同步工具在执行比较与生成合并脚本的时候,并不会用到代码控制库。
- 确保被执行的部署脚本在运行时能够正确判断环境状态 —— 最佳实践是在准备执行的时候生成脚本,这样就能保证它引用了正确的环境状态了。
- 确保部署脚本能够正确处理与合并冲突 —— 简单的比较与同步工具将A与B(源与目标)环境进行比较,基于右方的表,该工具能够生成一份脚本,将目标环境进行“升级”,以符合源环境的内容。如果不了解某个变更的内容,那么有可能会生成错误的脚本。举例来说,在目标环境中有一个索引,是在某个不同的分支或是严重缺陷修复时创建的。如果该索引并不存在于源环境中,那么工具又该怎么做呢?删除这个索引?如果在开发环境中存在某个索引,而在生产环境中不存在,是意味着开发环境中加入了这个索引,还是说生产环境中删除了这个索引呢?使用这种工具作为解决方案,需要你对每个变更的内容有深入的了解,以保证能够正确地进行处理。
- 只为相关的变更生成部署脚本 —— 比较与同步工具会对整个数据库schema进行比较,并显示出不同之处。但它们并不了解变更背后的原因,因为这些信息是保存在软件生命周期管理工具、CMS,或是版本控制库中的,而它们对于比较与同步工具来说属于外部信息。结果是你可能会被一大堆无关的背景杂音所干扰,导致你难以判断应该做些什么。
- 确保部署脚本了解数据库的依赖项 —— 比较与同步工具能够了解数据库的依赖项,并且以正确的顺序生成相关的DDL、DCL与DML语句。但不是所有比较与同步工具都支持在生成的脚本中包含多个schema的内容。
结论:比较与同步工具能够满足这些必要需求中的一部分,但不是全部。脚本依然需要手动审查,而且在自动化过程中无法完成依赖。
建立一个数据库执行变更管理解决方案
数据库执行变更管理结合了对数据库对象强制使用版本控制的流程,并且基于版本控制库及当前环境的结构,在需要时生成部署脚本。
这种方式意味着“按需构建与部署”,意即部署脚本是在需要时才进行构建(生成)的,而不是作为开发过程的一部分。这种方式保证了有效地处理冲突、合并,以及外部进程产生的变更。
按需构建与部署
那么数据库执行变更管理解决方案又是如何应对相同的挑战的呢?
- 确保所有的数据库代码都被涵盖 —— 结构、用数据库语言编写的业务逻辑、引用内容、数据库权限等内容都被正确地管理。
- 确保以版本控制库作为唯一的信任源 —— 强制的变更策略能够阻止任何人在任何IDE(甚至是命令行)中对数据库对象进行更改,而不经过事先的签出与变更后的签入。这就保证了版本控制库在对象签入时始终于对象的定义相一致。
单一的流程强制了版本控制的实施
- 确保被执行的部署脚本在运行时能够正确判断环境状态 —— 按需(在准备执行前)构建(生成)部署脚本的方式确保了它完全了解当前的环境状态。
- 确保部署脚本能够正确处理与合并冲突 —— 在分析过程中使用基线比较,就能够了解变更的原因,并且能够简单地判断是否要将变更进行部署、或是对目标环境进行保护(即忽略该变更)、或是对冲突进行合并。
了解基线的分析
- 只为相关的变更生成部署脚本 —— 与应用程序生命周期管理(ALM)工具及变更管理系统(CMS)的结合保证你能够为每个变更分配一个原因,就如同你在基于文件的版本控制系统或任务管理系统中所做的一样。
- 确保部署脚本了解数据库的依赖项 —— 严密的分析与脚本生成算法确保了DDL、DCL和DML等语句能够根据数据库的依赖,以正确的顺序进行执行,包括了跨schema的依赖。
除了这些必需满足的需求之外,还存在着一些别的需求,例如对并行开发的支持、合并分支、与数据库IDE的集成,以及支持由数据建模工具生成的变更等等。无论你选择了哪种方式,都必须验证它是否能够处理这些需求。
结论
对数据库组件的管理有着特殊的需求,因此对自动化流程来说是个极大的挑战。在距今较远的年代里,一年中通常只有几次发布,因此花费大量时间对数据库部署脚本进行手动审查以及维护是常见的、也是情有可原的做法。现如今,随着对敏捷及更快的发布速度的需求不断增长,数据库管理必须成为自动化流程的一部分。无论是编写SQL或XML脚本,或是使用简单的比较与合并工具,一旦在自动化过程中使用,都存在着低效或是高风险的问题。最有效的方式是实现 数据库执行变更管理。