3.7 数据库模式和数据的管理
3.7.1 需要对数据库模式进行管理的原因
团队开发中,如何才能有效地管理数据库模式 54 是一个比较棘手的问题 55 。在多人修改数据库的情况下,容易因漏执行 SQL 或执行顺序出错等原因造成数据库的数据一致性出现问题。
54 是对数据库构造的定义。
55 以第 2 章的问题 4 为例。
怎样才能解决这个问题呢?由于该问题的主要原因在于各个开发人员随意制作 SQL 修改数据库模式,因此应禁止开发人员修改数据库模式,并设置数据库管理员一职,由此人对所有的修改进行管理,这样是不是就能够解决问题了呢?
过去采用这种方式的开发现场比较常见,但从以下这些原因可以看出这种方式是存在问题的。
●…… 由数据库管理员负责对修改进行管理的情况
首先,在由数据库管理员负责对所有的修改进行管理的情况下,如果数据库管理员成为瓶颈的话,整体的开发速度就会受到影响 56 。
56 根据项目的规模,设置数据库管理员也是必要的,因为有数据库的调整或备份等本来应该由数据库管理员负责的业务。
●…… 修改共享数据库的模式的情况
其次,如果在团队中共享数据库,那么模式的变更就会波及到整个团队,影响开发进度。模式发生变更时,首先程序的代码也无疑会发生变化,若修改共享数据库的模式,各个开发成员的代码和数据库模式就难免会发生背离。因此可以说对数据库模式也应该进行版本管理。
3.7.2 应该如何管理数据库模式
对数据库模式进行版本管理,应该管理什么?又怎么管理呢?让我们具体地来看一下。这里假设数据库为 MySQL 或 PostgreSQL 等,也就是说使用了 RDBMS,以此为前提来继续下面的话题。但是这里的数据库并不局限于 RDBMS,文本文件、XML 文件、对象数据库以及最近使用频率逐渐增加的 MongoDB 等 NoSQL 数据库,它们的思考方法也是完全相同的。
●…… 版本管理的必要条件
对数据库模式进行版本管理的必要条件中,比较重要的是以下 3 个。
无论什么环境都能用相同的步骤来构建数据库
能够反复执行多次
文本文件
上面这些也是和 CI 相关联的思考方法。CI 相关的内容将在第 5 章进行说明。对于数据库模式,和代码一样进行版本管理,无论任何环境都能反复构建,这一点是非常重要的。另外,为了用版本管理系统方便地进行合并,以文本文件的形式管理模式也是很重要的。
例如有的开发现场使用商用的 GUI 工具来创建数据库模式,这样的工具有时反而会影响团队开发的效率。因此一定要以程序能够反复执行的文本文件形式来管理数据库模式。
●…… 什么是数据库迁移
数据库模式的 CI 称为 CDBI(Continuous DataBase Integration)。《持续集成:软件质量改进和风险降低之道》57 中也以专门的章节对其进行了说明。但是最近比起 CDBI,使用从 Ruby on Rails 的工具名(Migration)衍生而来的“数据迁移”这个叫法的人似乎更多一些。本书也以数据迁移这个用语来继续下面的解说。
57 (美)Paul M. Duvall 、Steve Matyas、Andrew Glover 著,王海鹏译,电子工业出版社,2012 年。
如今发布了很多数据迁移用的工具和框架,借助这些工具,能比较简单地实现数据迁移。
●…… 数据库迁移的功能
如前所述,作为数据库迁移工具,Ruby on Rails 的 Migration 是比较有名的。顺便说一下,笔者所在的公司是自己开发原生的工具进行数据迁移的。的确,Ruby on Rails 从诞生以来也只经过了 10 年左右的时间,还算是比较新的架构。业务持续了 10 年以上的公司中,独自开发工具进行数据迁移的情况还是比较多的。
不同的工具在实现的细节方面有细小的差异,但基本的构思几乎都是相同的。笔者所在的公司开发的工具和以 Ruby on Rails 的 Migration 为代表的工具大体上都是以下面这样的构思来制作的。
管理 SQL 执行的顺序和需要执行哪些 SQL
管理模式定义编辑的冲突
提供回滚的机制
支持数据的加载
SQL 的模式定义(DDL,Data Definition Language)和数据加载(DML,Data Manipulation Language)是有执行顺序的。如同我们在第 2 章的问题 4 中所见到的那样,执行顺序发生变化后意义也会随之改变,所以需要对执行顺序以及在各个环境中需要执行哪些 SQL 进行管理。因此大部分工具都在数据库中创建管理表,或者用专门的 XML 文件来管理执行顺序和需要执行哪些 SQL 等。
为了避免冲突,通常会为 SQL 文件确立某种命名规则。具体来说,有的以日期作为文件名,将相同日期的 SQL 全部放入同一文件;有的以连续的数字作为文件名,文件名相同的话则进行合并等。
受 Ruby on Rails 的 Migration 的影响,如今的工具基本都提供了回滚的机制。制作文件时会在同一文件中记载回滚用的 SQL。CREATE 语句对应 DROP,INSERT 语句对应 DELETE,这样执行用的 SQL 和回滚用的 SQL 就可以成对地进行管理。
数据加载方面和模式定义相同,比较多的工具采用在文件中记载 INSERT 或 UPDATE 的方法进行管理。并且这里的数据主要是系统初始化设置所必需的数据。例如税率的数据或超级用户的数据等。可以理解为和用户的使用状况无关,系统启动时所必需的数据。
请注意这里加载的数据不包括单体测试和结合测试所需要的数据。测试数据不属于初始化数据,而属于用户数据。这部分数据应该通过 Fixture58 等机制在测试程序中加载。测试部分的数据管理将在第 5 章进行说明。
58 将测试所需要的数据预先加载到数据库中的机制。为了能够反复测试,通常 Fixture 会在执行测试前加载数据,在测试结束后删除数据。像这样预先提供数据加载和删除这两个功能的情况是比较多见的。
◆◆◆
接下来将对主流的数据库迁移工具进行说明。在因为某些原因无法使用工具的情况下,可以考虑自己制作符合要求的原生工具。通过对数据库模式实行版本管理,项目的品质和速度将会有突破性的飞跃。
3.7.3 数据库迁移工具
现在已经出现了各种各样的数据库迁移工具,并且几乎所有最近的 Web 应用程序框架中都自带数据库迁移工具。Web 应用程序框架自带的工具主要有以下这些。
●…… Migration(Ruby on Rails)
现在 Web 应用程序框架的先驱 Ruby on Rails 自带的工具。其他的工具几乎都受它的影 响。为了管理 SQL,会在数据库中创建名为 system_setting 的表。
●…… south(Django)
比较有名的 Python 框架 Django 中自带的名为 south 的工具。
●…… Migrations Plugin(CakePHP)
PHP 的框架 CakePHP 中为了进行数据迁移而提供的插件。
●…… Evolution(Play Framework)
Java 和 Scala 的框架 Play Framework 中作为标准自带的名为 Evolution 的工具。
◆◆◆
也有不局限于特定的 Web 应用程序框架的通用迁移工具。这类工具的数量比较多,我们这里只介绍一小部分能够用 Java 调用的工具。
Flyway 59
Liquibase 60
dbdeploy 61
每个工具的功能都差不多,所以选择与项目相适应的,可以使用的工具就行了。
如果要在已有的应用程序中导入迁移工具,那么使用不依赖于框架的工具会比较好。还有一些 Java 的项目,只是为了使用 Migration 而特地导入 Ruby on Rails,这么做的开发现场也是有的。
3.7.4 具体用法(Evolution)
关于数据迁移工具的用法,这里以笔者作为提交者(committer)的 Play Framework 为例来进行说明。Evolution 和老大哥 Migration 比起来功能上要简单很多,但必要的功能都有了。
●…… 规定
Evolution 和其他类似的工具一样,对于文件名、记载的内容、存放的目录等都有规定。
SQL 文件以 1.sql、2.sql 这样连续的数字命名
一定要放在程序的 conf/evolutions/{ 数据库名 } 的目录下
SQL 文件中必须要有 Ups 和 Downs62 这两部分的定义
62 Ups 部分记载需要执行的 SQL 的定义,与之相对,Downs 部分记载的是将 Ups 部分的内容回滚时需要执行的 SQL。如果 Ups 部分是 Create 的话,Downs 部分就是 Drop,大致就是这个样子。详细的处理稍后进行讲解,大概机制是当版本管理系统中的 SQL 和实际的数据库状态之间的一致性出现问题时,就运行 Downs 部分的内容进行回滚。
例如像下面这样定义 1.sql 文件 63 。
63 Ruby on Rails 的情况下不是 SQL 文件,而是用 Ruby 写的单独的 DSL 文件。
# User表 # —- !Ups CREATE TABLE User ( id bigint(20) NOT NULL AUTO_INCREMENT, email varchar(255) NOT NULL, password varchar(255) NOT NULL, name varchar(255) NOT NULL, creat_at date NULL, update_at date NULL, PRIMARY KEY (id) ); # —- !Downs DROP TABLE User;●…… SQL 文件的执行
Play Framework 在存在 SQL 文件的状态下启动程序就会自动执行 Evolution 的处理 64 。处理会对比本地机器上的数据库和 Evolution 的 SQL 文件,如果发现有没有被执行的 SQL,就会在浏览器中显示如图 3.9 这样的消息,提示执行 SQL。
64 当然也可以设置为不自动执行。
图 3.9 有 SQL 没有被执行时的画面(Play Framework)
按下“Apply this script now!”就会执行 SQL 文件。关于这方面,Ruby on Rails 的 Migration 采用的是命令行操作模式,Play Framework 采用的是对话模式 65 。交互模式虽然不同,但想要处理的内容是相同的。
65 最新的 Ver2 只能使用对话模式进行处理。Ver1 也可以用命令行模式进行操作。希望 Ver2 的 Evolution 也能早日支持命令行模式。
●…… 开发者之间数据库模式的同步
假设开发人员 A 为添加表而新建了 2.sql 文件。
# Company 添加 # —- !Ups CREATE TABLE Company ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, location varchar(255) NOT NULL, description text NOT NULL, url varchar(255) NOT NULL, creat_at date NULL, update_at date NULL, PRIMARY KEY (id) ); # —- !Downs DROP TABLE Company;在 A 的数据库中执行该 SQL。
这时开发人员 B 正好想给 1.sql 中创建的 User 表增加一列。因为不知道 A 已经创建了 2.sql,所以 B 在本地机器上又创建了 2.sql。
# User 修改 # —- !Ups ALTER TABLE User ADD age INT; # —- !Downs ALTER TABLE User DROP age;开发结束后,B 向中央代码库 Push 新建的文件。之后 A 也进行 Push,但因为 B 提交的 2.sql 已经存在,所以结果产生冲突了。
<<<<<<< HEAD # Company 添加 # —- !Ups CREATE TABLE Company ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, location varchar(255) NOT NULL, description text NOT NULL, url varchar(255) NOT NULL, creat_at date NULL, update_at date NULL, PRIMARY KEY (id) ); # —- !Downs DROP TABLE Company; ======= # Update User # —- !Ups ALTER TABLE User ADD age INT; # —- !Downs ALTER TABLE User DROP age; >>>>>>> devB合并后的代码如下所示。
# Company 添加 # —- !Ups ALTER TABLE User ADD age INT; v CREATE TABLE Company ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, location varchar(255) NOT NULL, description text NOT NULL, url varchar(255) NOT NULL, creat_at date NULL, update_at date NULL, PRIMARY KEY (id) ); # —- !Downs ALTER TABLE User DROP age; DROP TABLE Company;合并后新的 2.sql 和执行了旧的 2.sql 的开发人员 A 的数据库之间已经有了差异。Evolution 会检查出该差异,回滚掉旧的数据库模式,并重新创建新的模式。回滚处理时会用到 Downs 部分,回滚到执行 2.sql 之前的状态后,再重新执行 Ups 部分。在此之后,如果 B 进行 Pull,B 的数据库也会被回滚,然后被重新创建。
这样,多人各自持有的数据库模式就都得到了同步。
●…… 一致性问题的管理
由于某些原因,执行 SQL 也会有失败的时候。例如合并 SQL 时出现问题,生成了自相矛盾的 SQL,并且没有注意就执行了该 SQL。这时 Evolution 会检测出错误,作为模式不一致的状态进行管理(图 3.10)。
图 3.10 一致性问题的管理(Play Framework)
显示上述画面时,可以通过手动执行 SQL 进行修正后再按下 Mark it resolved 按键,或者修改原有的 SQL 文件后再运行 Evolution 的方法来解决。
这样,通过对 SQL 的错误进行管理,使得管理正确的数据库模式成为了可能。
3.7.5 数据库迁移中的注意点
对数据库迁移文件(Evolution 的话是 SQL 文件,Migration 的话是 Ruby 文件)进行版本管理时,判断合并的结果是否正确是一个比较伤脑筋的问题。
RDBMS 的数据库构建可以说只要不出现错误就应该没什么问题,但无法保证合并后的 SQL 一定正确,因此需要对数据库模式的正确性进行测试。
这个和代码的合并道理相同。代码也是合并后的文件只要能通过编译就基本可以说没问题了,但从程序的角度来看,正确与否还要经过测试才知道。
只要使用数据库迁移,数据库的变更管理就万无一失,这样的说法过于草率,测试还是必须的,这一点请注意。应该编写测试程序,做到随时都能够自动进行测试。因此应该将数据库迁移作为 CI 的一个环节,纳入到 CI 之中。CI 的相关内容将在第 5 章进行说明。
