
本文详细说明了如何解决与部署过程中数据库兼容性相关的问题。 如果您在没有任何准备的情况下尝试执行部署,我们将告诉您产品上的应用程序会发生什么。 然后,我们将经历应用程序生命周期的各个阶段,这些阶段对于实现零停机时间( 大约Transl .:进一步-零停机时间 )是必不可少的。 我们操作的结果将是以向后兼容的方式应用向后不兼容的数据库更改。
如果您想了解本文中的代码示例,可以在GitHub上找到它们。
引言
零停机时间部署
什么是零停机神秘部署 ? 您可以在部署应用程序时这样说,以便您可以成功引入新版本的应用程序进行生产,而用户不会注意到其不可访问性。 从用户和公司的角度来看,这是最佳的部署方案,因为这样您就可以引入新功能并消除错误而不会中断。
如何实现呢? 有几种方法,这是其中一种:
- 扩展服务的版本1
- 迁移数据库
- 与版本1并行部署服务的版本2
- 一旦看到版本号2正常运行,请删除版本号1
- 完成了!
容易吧? 不幸的是,这并不是那么简单,我们将在后面详细讨论。 现在,让我们看看另一个相当普遍的部署过程-蓝绿色部署。
您听说过蓝绿色部署吗? 使用Cloud Foundry,这非常容易做到。 只需看一下本文 ,我们将对其进行更详细的描述。 简要总结一下,我们回顾了如何进行蓝绿色部署:
- 确保操作两个生产代码副本(“蓝色”和“绿色”);
- 将所有流量定向到蓝色环境,即 以便将生产URL指向此处;
- 在绿色环境中部署和测试所有应用程序更改
- 将网址从蓝色环境切换到绿色环境
蓝绿色部署是一种使您可以轻松引入新功能而不必担心生产会中断的方法。 这是由于这样的事实,即使发生了某些事情,您也可以通过简单地“单击开关”轻松回滚到以前的环境。
阅读完以上所有内容后,您可以提出以下问题:蓝绿色部署与零停机时间有什么关系?
嗯,它们有很多共同点,因为支持同一环境的两个副本需要付出双重努力来维护它们。 这就是为什么马丁·福勒 ( Martin Fowler )认为某些团队坚持采用这种方法的原因:
另一个选择是使用相同的数据库,为Web和域层创建蓝绿色开关。 在这种方法中,数据库通常是个问题,尤其是当您需要更改其架构以支持该软件的新版本时。
在这里,我们讨论本文的主要问题。 数据库 。 让我们再来看一下这句话。
迁移数据库。
现在您必须问自己一个问题-如果更改数据库向后不兼容怎么办? 我的应用程序的第一个版本不会中断吗? 实际上,这正是将要发生的事情...
因此,尽管零停机时间/蓝绿色部署带来了巨大的好处,但公司仍倾向于为其应用遵循以下更安全的部署过程:
- 使用新版本的应用程序准备软件包
- 关闭正在运行的应用程序
- 运行脚本进行数据库迁移
- 部署并启动新版本的应用程序
在本文中,我们将详细描述如何使用数据库和代码来利用零停机时间部署。
数据库问题
如果您有一个无状态应用程序,该应用程序未在数据库中存储任何数据,则可以立即获得零停机时间部署。 不幸的是,大多数软件都需要将数据存储在某个地方。 这就是为什么在更改电路之前应该三思而后行的原因。 在深入研究如何更改方案的细节,以便可以在不停机的情况下进行部署之前,让我们首先关注版本控制方案。
版本控制方案
在本文中,我们将使用Flyway作为版本控制工具( 大约Transl。:我们正在谈论数据库迁移 )。 自然,我们还将编写一个具有内置Flyway支持的Spring Boot应用程序,并在设置应用程序上下文时迁移电路。 使用Flyway时,可以将迁移脚本存储在项目文件夹中(默认情况下,在classpath:db/migration
)。 在这里,您可以看到此类迁移文件的示例。
└── db └── migration ├── V1__init.sql ├── V2__Add_surname.sql ├── V3__Final_migration.sql └── V4__Remove_lastname.sql
在此示例中,我们看到了4种迁移方案,如果它们没有更早执行,则将在应用程序启动时一个接一个地执行。 让我们以其中一个文件( V1__init.sql
)为例。
CREATE TABLE PERSON ( id BIGINT GENERATED BY DEFAULT AS IDENTITY, first_name varchar(255) not null, last_name varchar(255) not null ); insert into PERSON (first_name, last_name) values ('Dave', 'Syer');
一切都完美地说明了一切:您可以使用SQL来确定应如何修改数据库。 有关Spring Boot和Flyway的更多信息,请查看Spring Boot Docs 。
将版本控制与Spring Boot结合使用可为您带来2大好处:
- 您将数据库更改与代码更改分开
- 数据库迁移与应用程序的推出同时发生,即 您的部署过程得以简化
解决数据库问题
在本文的下一部分中,我们将重点考虑两种数据库更改方法。
前者将被视为警告,如果没有进行初步准备,则不应执行零停机时间部署。第二者提供了有关如何在不停机的情况下执行部署并同时保持向后兼容性的解决方案。
我们将进行的项目将是一个简单的Spring Boot Flyway应用程序,其中数据库中存在一个具有first_name
和last_name
的Person
( 大约per .: Person
是一个表,而irst_name
和last_name
是其中的字段 )。 我们想将last_name
重命名为surname
。
假设条件
在深入研究细节之前,我们需要概述一些关于应用程序的假设。 我们要实现的主要结果将是一个相当简单的过程。
注意事项 商业专业提示。 简化流程可以为您节省大量支持费用(在公司工作的人越多,您可以节省的钱就更多)!
无需回滚数据库
这简化了部署过程(几乎不可能进行数据库的某些回滚,例如回滚删除)。 我们只喜欢回滚应用程序。 这样,即使您具有不同的数据库(例如SQL和NoSQL),您的部署管道也将看起来相同。
必须始终可以将应用程序回滚一个版本(不再)
仅在必要时才进行回滚。 如果当前版本中存在一个不容易修复的错误,我们应该能够返回最新的工作版本。 我们假定此最新的工作版本为先前的版本。 维持多个发行版的代码和数据库兼容性非常困难且成本很高。
注意事项 为了提高可读性,在本文的框架内,我们将更改应用程序的主要版本。
步骤1:初始状态
应用版本: 1.0.0
数据库版本: v1
评注
这将是应用程序的初始状态。
数据库更改
该数据库包含last_name.
CREATE TABLE PERSON ( id BIGINT GENERATED BY DEFAULT AS IDENTITY, first_name varchar(255) not null, last_name varchar(255) not null ); insert into PERSON (first_name, last_name) values ('Dave', 'Syer');
代码变更
该应用程序将Person数据保存到last_name
:
/* * Copyright 2012-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package sample.flyway; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Person { @Id @GeneratedValue private Long id; private String firstName; private String lastName; public String getFirstName() { return this.firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return this.lastName; } public void setLastName(String lastname) { this.lastName = lastname; } @Override public String toString() { return "Person [firstName=" + this.firstName + ", lastName=" + this.lastName + "]"; } }
不兼容的列重命名
让我们看一下如何更改列名的示例:
注意事项 以下示例将有意中断。 为了说明数据库兼容性问题,我们展示了这一点。
应用程序版本: 2.0.0.BAD
数据库版本: v2bad
评注
当前的更改不允许我们同时运行两个实例(旧实例和新实例)。 因此,将很难实现零停机时间部署(考虑到假设,这实际上是不可能的)。
A / B测试
当前的情况是,我们有一个在prod中部署的应用程序版本1.0.0,
和一个数据库v1
。 我们必须部署应用程序的第二个实例2.0.0.BAD
版本,并将数据库升级到v2bad
。
步骤:
- 部署了应用程序版本
2.0.0.BAD
的新实例,该实例将数据库更新为v2bad
v2bad
数据库中的last_name
列不再存在-更改为surname
- 数据库和应用程序更新成功,并且某些实例在
1.0.0
工作,其他实例在2.0.0.BAD
。 所有与v2bad
相关的 - 版本
1.0.0
所有实例将开始引发错误,因为它们将尝试将数据插入到last_name
列中,该列不再 - 版本
2.0.0.BAD
所有实例2.0.0.BAD
正常使用
如您所见,如果我们对数据库和应用程序进行向后不兼容的更改,则无法进行A / B测试。
回滚应用
假设在尝试执行A / B部署( 大约Transl。::作者可能是在这里指的是A / B测试 )之后,我们决定需要将应用程序回滚到1.0.0.
版1.0.0.
假设我们不想回滚数据库。
步骤:
- 我们停止了应用程序实例版本
2.0.0.BAD
- 数据库仍然是
v2bad
- 由于
1.0.0
版不了解什么是surname
,我们将看到错误 - 地狱挣脱了我们不能再回来
如您所见,如果我们对数据库和应用程序进行向后不兼容的更改,则无法回滚到以前的版本。
脚本执行日志
Backward incompatible scenario: 01) Run 1.0.0 02) Wait for the app (1.0.0) to boot 03) Generate a person by calling POST localhost:9991/person to version 1.0.0 04) Run 2.0.0.BAD 05) Wait for the app (2.0.0.BAD) to boot 06) Generate a person by calling POST localhost:9991/person to version 1.0.0 <-- this should fail 07) Generate a person by calling POST localhost:9992/person to version 2.0.0.BAD <-- this should pass Starting app in version 1.0.0 Generate a person in version 1.0.0 Sending a post to 127.0.0.1:9991/person. This is the response: {"firstName":"b73f639f-e176-4463-bf26-1135aace2f57","lastName":"b73f639f-e176-4463-bf26-1135aace2f57"} Starting app in version 2.0.0.BAD Generate a person in version 1.0.0 Sending a post to 127.0.0.1:9991/person. This is the response: curl: (22) The requested URL returned error: 500 Internal Server Error Generate a person in version 2.0.0.BAD Sending a post to 127.0.0.1:9995/person. This is the response: {"firstName":"e156be2e-06b6-4730-9c43-6e14cfcda125","surname":"e156be2e-06b6-4730-9c43-6e14cfcda125"}
数据库更改
将last_name
重命名为surname
迁移脚本
源Flyway脚本:
CREATE TABLE PERSON ( id BIGINT GENERATED BY DEFAULT AS IDENTITY, first_name varchar(255) not null, last_name varchar(255) not null ); insert into PERSON (first_name, last_name) values ('Dave', 'Syer');
重命名last_name
的脚本。
-- This change is backward incompatible - you can't do A/B testing ALTER TABLE PERSON CHANGE last_name surname VARCHAR;
代码变更
我们将lastName
字段名称更改为surname
。
以向后兼容的方式重命名列
这是我们可能遇到的最常见的情况。 我们需要进行向后不兼容的更改。 我们已经证明,对于没有停机的部署,我们不应该只是在没有其他操作的情况下应用数据库迁移。 在本文的这一部分中,我们将对应用程序进行3个部署以及数据库迁移,以实现所需的结果并同时保持向后兼容性。
注意事项 回想一下,我们有一个数据库版本v1
。 它包含列first_name
和last_name
。 我们必须将last_name
更改为surname
。 我们还有一个版本1.0.0,
的应用程序,尚未使用surname
。
步骤2:添加姓
应用版本: 2.0.0
数据库版本: v2
评注
通过添加新列并复制其内容,我们创建了向后兼容的数据库更改。 同时,如果我们回滚JAR或拥有可用的旧JAR,则它在运行时不会中断。
我们推出新版本
步骤:
- 迁移数据库以创建新的
surname
列。 现在您的数据库版本v2
- 将数据从
last_name
复制到surname
。 请注意 ,如果您有大量此类数据,则应考虑批量迁移! - 编写同时使用新列和旧列的代码。 现在您的应用程序版本为
2.0.0
- 如果该值不为
null
,则从surname
列中读取该值;如果未指定surname
,则从l ast_name
。 您可以从代码中删除getLastName()
,因为当您将应用程序从3.0.0
回滚到2.0.0
时,它将返回null
。
如果您使用的是Spring Boot Flyway,则将在启动应用程序2.0.0
版时执行这两个步骤。 如果手动运行数据库版本控制工具,则必须为此执行两项不同的操作(首先手动更新数据库版本,然后部署新的应用程序)。
这很重要。 请记住,新创建的列一定 不能为NOT NULL 。 如果回滚,旧应用程序将不知道新列,并且在Insert.
过程中将不会安装新列Insert.
但是,如果添加此限制,并且数据库将是v2
,则需要设置新列的值。 这将导致违反限制。
这很重要。 您应该删除getLastName()
方法,因为版本3.0.0
在代码getLastName()
没有last_name
列的概念。 这意味着将在此处设置null。 您可以保留该方法并添加null
检查,但是更好的解决方案是确保在getSurname()
逻辑中选择了正确的非零值。
A / B测试
当前的情况是我们在prod上部署了应用程序版本1.0.0
,在v1
部署了数据库。 我们需要部署应用程序版本2.0.0
的第二个实例,该实例会将数据库升级到v2
。
步骤:
- 部署了应用程序版本
2.0.0
的新实例,该实例将数据库更新为v2
- 同时,一些请求由版本
1.0.0
的实例处理 - 更新成功,并且您拥有应用程序版本
1.0.0
和其余版本2.0.0.
多个工作实例2.0.0.
每个人都在v2
与数据库通信 1.0.0
版不使用数据库中的姓列,但2.0.0
版使用。 它们不会互相干扰,并且应该没有错误。2.0.0
版将数据存储在旧列和新列中,这提供了向后兼容性
这很重要。 如果您有任何查询根据旧/新列中的值对项目进行计数,则应记住,现在您有重复的值(很可能它们仍在迁移)。 例如,如果您要计算姓氏(无论列名如何)以字母A
开头A
,则在数据迁移( old
→ new
列)完成之前,如果在新列上执行查询,则数据可能不一致。
回滚应用
现在,我们有一个应用程序版本2.0.0
和一个v2
的数据库。
步骤:
- 将您的应用程序回滚到
1.0.0
版本。 1.0.0
版不使用数据库中的surname
列,因此回滚必须成功
数据库更改
该数据库包含一个名为last_name
的列。
源Flyway脚本:
CREATE TABLE PERSON ( id BIGINT GENERATED BY DEFAULT AS IDENTITY, first_name varchar(255) not null, last_name varchar(255) not null ); insert into PERSON (first_name, last_name) values ('Dave', 'Syer');
用于添加surname
的脚本。
注意事项 请记住,您不能将任何NOT NULL约束添加到添加的列。 如果回滚JAR,则旧版本对添加的列一无所知,并将自动将其设置为NULL。 如果有这样的限制,旧的应用程序将被破坏。
-- NOTE: This field can't have the NOT NULL constraint cause if you rollback, the old version won't know about this field -- and will always set it to NULL ALTER TABLE PERSON ADD surname varchar(255); -- WE'RE ASSUMING THAT IT'S A FAST MIGRATION - OTHERWISE WE WOULD HAVE TO MIGRATE IN BATCHES UPDATE PERSON SET PERSON.surname = PERSON.last_name
代码变更
我们将数据存储在last_name
和surname
。 同时,我们从last_name
读取内容,因为此列最相关。 在部署过程中,某些请求可能已由尚未更新的应用程序实例处理。
/* * Copyright 2012-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package sample.flyway; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Person { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String surname; public String getFirstName() { return this.firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } /** * Reading from the new column if it's set. If not the from the old one. * * When migrating from version 1.0.0 -> 2.0.0 this can lead to a possibility that some data in * the surname column is not up to date (during the migration process lastName could have been updated). * In this case one can run yet another migration script after all applications have been deployed in the * new version to ensure that the surname field is updated. * * However it makes sense since when looking at the migration from 2.0.0 -> 3.0.0. In 3.0.0 we no longer * have a notion of lastName at all - so we don't update that column. If we rollback from 3.0.0 -> 2.0.0 if we * would be reading from lastName, then we would have very old data (since not a single datum was inserted * to lastName in version 3.0.0). */ public String getSurname() { return this.surname != null ? this.surname : this.lastName; } /** * Storing both FIRST_NAME and SURNAME entries */ public void setSurname(String surname) { this.lastName = surname; this.surname = surname; } @Override public String toString() { return "Person [firstName=" + this.firstName + ", lastName=" + this.lastName + ", surname=" + this.surname + "]"; } }
步骤3:从代码中删除last_name
应用版本: 3.0.0
数据库版本: v3
评注
注意事项 trans .:显然,作者错误地复制了原始文章中第2步中的该块的文本,在此步骤中,应对应用程序代码进行更改,以删除使用last_name
列的功能。
通过添加新列并复制其内容,我们创建了向后兼容的数据库更改。 另外,如果我们回滚JAR或有可用的旧JAR,则它在运行时不会中断。
回滚应用
我们目前有3.0.0
版的应用程序和v3
数据库。 版本3.0.0
不会将数据保存在last_name
。 这意味着surname
将存储最新信息。
步骤:
- 将您的应用程序回滚到
2.0.0
版本。 2.0.0
版同时使用last_name
和surname
。- 如果版本
2.0.0
不为null,则它将使用surname
,否则为last_name
数据库更改
数据库中没有结构上的更改。 执行以下脚本,该脚本执行旧数据的最终迁移:
-- WE'RE ASSUMING THAT IT'S A FAST MIGRATION - OTHERWISE WE WOULD HAVE TO MIGRATE IN BATCHES -- ALSO WE'RE NOT CHECKING IF WE'RE NOT OVERRIDING EXISTING ENTRIES. WE WOULD HAVE TO COMPARE -- ENTRY VERSIONS TO ENSURE THAT IF THERE IS ALREADY AN ENTRY WITH A HIGHER VERSION NUMBER -- WE WILL NOT OVERRIDE IT. UPDATE PERSON SET PERSON.surname = PERSON.last_name; -- DROPPING THE NOT NULL CONSTRAINT; OTHERWISE YOU WILL TRY TO INSERT NULL VALUE OF THE LAST_NAME -- WITH A NOT_NULL CONSTRAINT. ALTER TABLE PERSON MODIFY COLUMN last_name varchar(255) NULL DEFAULT NULL;
代码变更
注意事项 trans .:作者在第2步中也错误地复制了此块的描述。根据文章故事的逻辑,此步骤中的代码更改应旨在从中删除与last_name
列一起使用的元素。
我们将数据存储在last_name
和surname.
另外,我们从列last_name
读取内容,因为它最相关。 在部署过程中,某些请求可能由尚未更新的实例处理。
/* * Copyright 2012-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package sample.flyway; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Person { @Id @GeneratedValue private Long id; private String firstName; private String surname; public String getFirstName() { return this.firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getSurname() { return this.surname; } public void setSurname(String lastname) { this.surname = lastname; } @Override public String toString() { return "Person [firstName=" + this.firstName + ", surname=" + this.surname + "]"; } }
步骤4:从数据库中删除last_name
应用版本: 4.0.0
数据库版本: v4
评注
由于版本3.0.0
代码未使用last_name
列,因此,如果在从数据库中删除该列后回滚到3.0.0
,则在执行过程中不会发生任何不良情况。
脚本执行日志
We will do it in the following way: 01) Run 1.0.0 02) Wait for the app (1.0.0) to boot 03) Generate a person by calling POST localhost:9991/person to version 1.0.0 04) Run 2.0.0 05) Wait for the app (2.0.0) to boot 06) Generate a person by calling POST localhost:9991/person to version 1.0.0 07) Generate a person by calling POST localhost:9992/person to version 2.0.0 08) Kill app (1.0.0) 09) Run 3.0.0 10) Wait for the app (3.0.0) to boot 11) Generate a person by calling POST localhost:9992/person to version 2.0.0 12) Generate a person by calling POST localhost:9993/person to version 3.0.0 13) Kill app (3.0.0) 14) Run 4.0.0 15) Wait for the app (4.0.0) to boot 16) Generate a person by calling POST localhost:9993/person to version 3.0.0 17) Generate a person by calling POST localhost:9994/person to version 4.0.0 Starting app in version 1.0.0 Generate a person in version 1.0.0 Sending a post to 127.0.0.1:9991/person. This is the response: {"firstName":"52b6e125-4a5c-429b-a47a-ef18bbc639d2","lastName":"52b6e125-4a5c-429b-a47a-ef18bbc639d2"} Starting app in version 2.0.0 Generate a person in version 1.0.0 Sending a post to 127.0.0.1:9991/person. This is the response: {"firstName":"e41ee756-4fa7-4737-b832-e28827a00deb","lastName":"e41ee756-4fa7-4737-b832-e28827a00deb"} Generate a person in version 2.0.0 Sending a post to 127.0.0.1:9992/person. This is the response: {"firstName":"0c1240f5-649a-4bc5-8aa9-cff855f3927f","lastName":"0c1240f5-649a-4bc5-8aa9-cff855f3927f","surname":"0c1240f5-649a-4bc5-8aa9-cff855f3927f"} Killing app 1.0.0 Starting app in version 3.0.0 Generate a person in version 2.0.0 Sending a post to 127.0.0.1:9992/person. This is the response: {"firstName":"74d84a9e-5f44-43b8-907c-148c6d26a71b","lastName":"74d84a9e-5f44-43b8-907c-148c6d26a71b","surname":"74d84a9e-5f44-43b8-907c-148c6d26a71b"} Generate a person in version 3.0.0 Sending a post to 127.0.0.1:9993/person. This is the response: {"firstName":"c6564dbe-9ab5-40ae-9077-8ae6668d5862","surname":"c6564dbe-9ab5-40ae-9077-8ae6668d5862"} Killing app 2.0.0 Starting app in version 4.0.0 Generate a person in version 3.0.0 Sending a post to 127.0.0.1:9993/person. This is the response: {"firstName":"cbe942fc-832e-45e9-a838-0fae25c10a51","surname":"cbe942fc-832e-45e9-a838-0fae25c10a51"} Generate a person in version 4.0.0 Sending a post to 127.0.0.1:9994/person. This is the response: {"firstName":"ff6857ce-9c41-413a-863e-358e2719bf88","surname":"ff6857ce-9c41-413a-863e-358e2719bf88"}
数据库更改
对于v3
我们只需删除last_name
列并添加缺少的约束。
-- REMOVE THE COLUMN ALTER TABLE PERSON DROP last_name; -- ADD CONSTRAINTS UPDATE PERSON SET surname='' WHERE surname IS NULL; ALTER TABLE PERSON ALTER COLUMN surname VARCHAR NOT NULL;
代码变更
代码没有变化。
结论
我们通过执行几个向后兼容的部署成功地应用了向后不兼容的列名更改。 以下是所采取步骤的摘要:
- 具有
v1
数据库架构的应用程序部署版本1.0.0
(列名称= last_name
) - 部署应用程序版本
2.0.0,
该版本将数据保存在last_name
和surname
。 应用程序从last_name
读取。 该数据库的版本为v2
,其中包含last_name
和surname. surname
的列surname. surname
surname. surname
是l ast_name
的副本。 (: not null) 3.0.0
, surname
surname. , last_name
surname
. NOT NULL last_name
. v3
4.0.0
— . v4
, last_name
. .
, , / .
代号
, , Github . .
, .
├── boot-flyway-v1 - 1.0.0 version of the app with v1 of the schema ├── boot-flyway-v2 - 2.0.0 version of the app with v2 of the schema (backward-compatible - app can be rolled back) ├── boot-flyway-v2-bad - 2.0.0.BAD version of the app with v2bad of the schema (backward-incompatible - app cannot be rolled back) ├── boot-flyway-v3 - 3.0.0 version of the app with v3 of the schema (app can be rolled back) └── boot-flyway-v4 - 4.0.0 version of the app with v4 of the schema (app can be rolled back)
, , .
, :
./scripts/scenario_backward_compatible.sh
, :
./scripts/scenario_backward_incompatible.sh
Spring Boot Sample Flyway
Spring Boot Sample Flyway.
http://localhost:8080/flyway
, .
H2 ( http://localhost:8080/h2-console
), (URL jdbc — jdbc:h2:mem:testdb
).
选配
另请阅读我们博客上的其他文章: