提纲:
包括索引在内的数据库模式需要进行源代码控制
诸如查询表这类用于控制业务逻辑的数据需要进行源代码控制
开发人员需要一种能够便捷地创建本地数据库的方法
共享数据库的更新只能通过构建服务器完成
健壮的 DevOps 环境需要对系统的每个组件进行持续集成。但是,数据库常常被排除在这之外,这会导致从脆弱的产品发布和低效的开发实践到让新入职的程序员工作更困难等一系列问题。
在本文中,我们将讨论在成功的持续集成环境中,关系型数据库和 NoSQL 数据库的独特的一面。
模式的源代码控制
首先需要解决的就是源代码控制和模式问题。让开发人员以一种特别的方式进行数据库变更是不合适的。这相当于在生产服务器上通过直接编辑 JavaScript 文件对其进行更改。
在为数据库规划源代码控制时,需要确保囊括了所有内容。这包括但不限于:
表或者集合
约束
索引
视图
存储过程、函数以及触发器
数据库配置
您可能会想,“我使用的是无模式数据库,所以我不需要源代码控制”。即便如此,您仍需要考虑索引和数据库的整体配置。如果在 QA 和生产数据库中索引计划不同,那么执行性能测试将毫无意义。
数据库的源代码控制有两种基本类型,我们将其称为“全模式”和“变更脚本”。
全模式源码控制
“全模式”源码控制指的是源代码控制与你所希望的数据库的外观看起来十分相似。在使用此模式时,可以看到所有的表和视图都按照预期的样子排列,这让你无需部署数据库就能够更容易理解它。
SQL Server 的 SQL Server Data Tools(SSDT)就是全模式源码控制的一个例子。这个工具可以通过 CREATE 脚本的形式表示所有数据库对象。当想要用 SQL 创建一个新的对象,只需将最终脚本直接粘贴到处于源代码控制下的数据库项目中即可,这就很方便了。
全模式源码控制的另一个例子是实体框架迁移(Entity Framework Migrations)。在这个案例中,数据库主要通过 C#/VB 类的方式而非 SQL 脚本的方式表现。但同样,可以通过浏览源代码获得对数据库的整体认识。
在使用全模式源码控制时,通常不需要直接编写迁移脚本。部署工具通过将数据库的当前状态与处于源代码控制中的理想版本进行比较来确定需要进行哪些更改。这可以让你快速完成数据库变更并看到结果。在使用这种类型的工具时,我很少直接更改数据库,而是使用工具完成大部分工作。
有些情况下,只有工具是不够的,即使包含部署前和部署后脚本也是如此。在这种情况下,生成的迁移脚本必须由数据库开发人员或 DBA 手工修改,这可能会破坏持续部署计划。这种情况通常发生在对表结构进行重大变更时,因为在这些情况下生成的迁移脚本效率可能较低。
全模式源码控制的另一个优点是它支持代码分析。例如,如果列的名称被更改,但在视图中未做出相应更新,SSDT 将返回一个编译错误。就像应用程序设计语言中的静态类型一样,可以捕获大量错误,并且能够对部署有明显错误的脚本提前防范。
变更脚本源码控制方式
另一种源代码控制方式就是变更脚本源码控制。这种方式不存储数据库对象本身,而是存储创建数据库对象所需的步骤列表。我曾经成功使用的是 Liquibase 数据库,总的来说这类工具的工作机制都差不多。
变更脚本工具的主要优点是可以完全控制最终迁移脚本的样貌。这使得复杂变更的执行更加容易,例如表的拆分或合并。
不幸的是,这种类型的源代码控制也存在一些缺陷。首先是需要编写变更脚本。虽然它的控制力度更强,但同样也更费时。相比于手写 ALTER TABLE 脚本,为 C#类添加一个属性要容易得多。
当使用 SchemaBinding 之类的功能时,会让问题更加严重。对于不熟悉这个术语的人,可以将其理解为,通过锁定表的模式启用 SQL Server 中的一些高级特性。如果要变更表或视图的设计,必须首先将 SchemaBinding 从任何与该表或视图有关的视图中移除。如果这些视图被其他视图模式绑定(schema-bound),那么这些视图同样也需要临时解除绑定。虽然对于全模式源代码控制工具来说,生成所有的样板很容易,但是想要用手工的方式正确地生成它们还是一项相当困难的工作。
变更脚本工具的另一个缺点是它们往往难以理解。例如,如果你希望在不部署表的情况下了解某个表中列的信息,则需要查阅所有涉及该表的变更脚本。这就很容易错过某些信息。
该如何选择源代码控制模型?
从头开始一个新的数据库项目时,我会选择全模式源代码控制。这种模式会让开发人员工作更加高效并且只要有一名人员完成设置工作,之后只需要很少的知识就可以正常使用。
对于现存的数据库,特别是那些已经存续多年的生产环境数据库,通常选择变更脚本源代码控制模式更加合适。全模式工具会对数据库设计做出某些特定的假设,而更改脚本工具则是通用的。此外,全模式工具构建难度更大,对于某些特殊的数据库来说,可能根本不可用。
数据管理
根据表中所含数据的性质,表可以被广泛地分类为“管理表”、“用户表”或“混合表”。根据表所属的类别不同,处理这些表的方式也是不同的。
管理表
将数据库置于源代码控制之下的一个常见错误是遗忘数据。总有一些“查询表”保存着用户不打算修改的数据。例如,其中可能包含表驱动的业务规则逻辑、状态机的各种状态码,或者仅仅是与应用程序代码中的枚举类相匹配的键-值对列表。
这类表中的数据应该被视为源代码一样对待。对这类数据的变更需要经过与其他代码变更相同的评审和 QA 过程。特别重要的是,为确保这些变更不会被遗漏,这些变更的部署应该与其他应用程序和数据库部署一并自动完成。
在 SQL Server 数据工具中,我使用部署后脚本处理此问题。在这个脚本中,我将期望数据填充到一张临时表中,然后使用 MERGE 语句更新实际表。
如果源代码控制工具无法很好地处理这个问题,还可以通过构建独立工具来执行数据更新。重要的不是如何做,而是这个过程是否易用并且可靠。
用户表
用户表指的是用户可以添加或修改数据的表。因此,这包括可以直接修改的表(例如名称和地址)和通过操作间接修改的表(例如货单收据、日志)。
真实的用户数据基本不会被直接加入源代码控制中,不过,为开发和测试提供逼真的样本数据也是一种最佳实践。这些数据可以直接存储在数据库项目中,处于源代码控制之下的其他地方,如果特别大,也可以保存在独立的共享文件中。
混合表
混合表指的是即存储管理数据也存储用户数据的表。有两种方法可以对其进行分区。
列分区是指用户可以修改某些列,但不能修改其他列。在这种场景下,可以将该表视为有额外限制的普通管理表,用户控制的列永远不会被更新。
行分区指的是某些记录用户无法修改的情况。我曾经遇到的常见的场景是需要在用户表中对某些值进行硬编码。在较大型的系统中,对于每个可以独立于任何真实用户进行更改的微服务,可能都有一个独立的用户 ID。例如,可能是一个被称为“银行数据导入器”的用户。
在我看来,管理行分区混合表的最佳方法是通过保留键的方式。当定义 identity/auto-number 列时,将初始值设置为 1,000,通过源代码控制对编号从 1 到 999 的用户 ID 进行管理。这需要数据库允许手动设置 identity 列中的值。在 SQL Server 中,是通过 SET_IDENTITY_INSERT 命令完成的。
处理此场景的另一个选择是使用名为“SystemControlled”的列或者能够达到类似效果的方法。当设置为 1/true 时,表示应用程序不可直接修改。如果设置为 0/false,则部署脚本会将其忽略。
个人开发数据库
将模式和数据置于源代码控制之下,就可以进行下一步并着手设置个人开发数据库。正如每个开发人员都应该能够运行自己的 web 服务器实例一样,有可能对数据库设计做出修改的每个开发人员都需要能够运行自己的数据库副本。
这一规则经常会被打破,这对开发团队是相当不利的。在共享环境上做变更的开发人员一定会相互干扰。他们甚至可能会陷入“部署之争”,每个开发人员都试图将更改部署到数据库。当使用全模式工具执行此操作时,开发人员将交替地恢复其他开发人员的变更,甚至都不会意识到这一点。在使用变更脚本工具的情况下,数据库则可能处于不确定状态,迁移脚本可能无法正常使用,需要从备份中重新恢复。
另一个问题是模式漂移。这时,开发数据库与处于源代码控制下的内容不再匹配。随着时间的推移,开发数据库会逐渐积累越来越多的非生产表、测试脚本、临时视图和其他垃圾信息需要清除。当每个开发人员都有自己的数据库时,这样做要容易得多,因为他们可以随时重置数据库。
最后也是最重要的问题是,服务开发人员和 UI 开发人员需要稳定的平台来编写代码。如果共享数据库不断变化,他们就无法有效地工作。在我曾经工作过的一家公司,很少看到开发人员大喊“服务又宕机了!”,然后玩一个小时视频游戏,等待共享数据库重新组装起来。
共享的开发和集成数据库
对于共享的开发人员数据库或集成数据库来说,首要原则就是不能对数据库直接进行修改。更新共享数据库的唯一方法就是通过构建服务器以及持续集成/部署流程完成。这不仅可以防止模式漂移,还可以通过有计划的更新减少中断的次数。
根据经验,我会在相关分支进行代码检入时同步更新共享开发人员数据库。这可能会造成中断,但通常是处于可控范围的。在进行集成之前,您确实需要先验证部署脚本的正确性。
对于集成数据库而言,我更倾向于每日安排一次部署,与服务的部署次数相同。这样能够为 UI 开发人员提供一个相对稳定的工作平台。对于 UI 开发人员来说,没有什么比不知道突然开始失败的代码是他们的错误还是服务/数据库中的问题更令人沮丧的了。
数据库安全和源代码控制
在数据库管理中,安全性是一个经常被忽略的方面。具体来说,就是哪些用户和角色可以访问哪些表、视图和存储过程。在最糟糕的情况下,应用程序可以获得对数据库的完全访问权限。它可以对每个表的每一列进行读写操作,甚至是那些与其没有业务关联的列。数据泄漏经常是由于实用小程序的访问权限远远超过其所需的访问权限而造成的。
锁定数据库的主要反对意见是,“我们无法预知什么会崩溃”。因为以前从未被锁定过,所以开发人员根本不知道应用程序实际需要的权限是什么。
解决这一问题的办法是从第一天起就将权限控制放入源代码控制中。这样,当开发人员测试应用程序时,如果权限不正确,一开始就会失败。这反过来又意味着,当到达 QA 阶段时,所有权限问题都已解决,不存在权限缺失的猜测或风险。
容器化
根据项目的性质不同,数据库的容器化是一个可选步骤。我将通过两个案例说明个中原因。
对于第一个案例,这个项目有一个非常简单的分支结构:有一个“dev”分支,它会导入 QA 分支,QA 分支又会导入阶段化分支和最终的生产分支。这可以通过四个共享数据库实现,管道中的每个阶段,各使用一个数据库。
在第二个案例中,我们有一组重要的特性分支。每个重要的特性分支被进一步细分为开发和 QA 分支。每个特性必须通过 QA 检验,才能够成为合并到主分支的候选特性,因此每个重要特性都需要有各自的测试环境。
在第一个案例中,即使 web 服务确实需要容器,容器化也可能是浪费时间。对于第二个用例,容器化某种程度上则是至关重要的。如果没有真正的容器(例如 Docker),那么在创建新的重要特性分支时,至少可以根据需要生成新环境的部署脚本(例如 AWS 或 Azure)。
关于作者
Jonathan Allen 在 90 年代后期开始为一家医疗诊所开发 MIS 项目,逐步将该项目从 Access 和 Excel 升级成企业级的解决方案。在花了五年时间为金融业编写自动化交易系统之后,他成为了多个项目的顾问,其中包括自动化仓库的 UI、癌症研究软件的中间层,以及一家大型房地产保险公司的大数据需求。在空闲时间,他喜欢研究和记录源于 16 世纪的武术。
查看英文原文: How to Source Control Your Databases for DevOps
评论