Spring Boot版本:2.3.4.RELEASE
场景
在开发过程中,难免会因为数据库设计不合理或者没有考虑周全,而需要对表做一些修改,比如增加字段、修改字段类型等。在独立开发项目的时候顺手就改了,但是在团队协同开发的时候就不行,每个人的数据库都需要完全同步,所以需要做好数据库的版本管理。
方案一:hibernate自动更新
hibernate更新表在独立开发的时候挺有用,而且使用方便,只要修改实体类代码即可,但是缺点也明显,没有修改记录,不能删除字段。
实现方式
导入依赖:
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<!-- Spring-data-jpa依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
复制代码
在Spring Boot的项目中,需要在application.yml配置文件中添加:
spring:
# 连接数据库
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://myserverhost:3306/mytest?createDatabaseIfNotExist=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
# 自动更新
jpa:
hibernate:
ddl-auto: update
show-sql: true
复制代码
示例代码如下(省去了不必要的内容):
@Entity
@Table(name = "tb_user")
public class User implements Serializable {
@Id
@Column(name = "id", columnDefinition = "BIGINT(20) UNSIGNED NOT NULL auto_increment COMMENT '主键id'")
private Long id;
@Column(name = "username", columnDefinition = "VARCHAR(64) UNIQUE NOT NULL COMMENT '用户名,唯一'")
private String username;
@Column(name = "password", columnDefinition = "VARCHAR(64) NOT NULL COMMENT '密码d'")
private String password;
}
复制代码
这样在项目启动的时候,就会检测到实体类代码,并自动进行表的创建,修改了属性后也会自动更新。
好处
- 够直观,不用执行sql语句,通过Java实体类就能实现变化
- 团队内同步方便,只需要将修改后的实体类代码提交到仓库,成员同步更新即可
缺陷
- 没有数据库的更新记录,只能通过代码仓库历史查看
- 无法实现彻底的表字段修改,该方案会保留原有字段,添加新字段,如果更新表字段的操作多了,会导致数据库表废弃字段太多
方案二:flyway框架
flyway框架的使用稍微麻烦些,但是好处是明显的,有清晰的历史修改记录,因为是基于sql语句所以定制性很高,并且对协同开发特别友好。
实现方式
<!--h2内存数据库-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!--flyway数据库迁移-->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>6.4.4</version>
</dependency>
复制代码
在SpringBoot项目中,flyway的数据库管理的sql文件默认放在resources-db-migration文件夹下:
- resources
- db
- migration
V1__Create_Table_User.sql
复制代码
sql文件命名格式说明:
V1__Create_Table_User.sql
V:表示文件只会被执行一次,其他的还有U和R,这里只讲V
1:版本号,可以用递增的整型或者点组合,如V1,V1.2,V2.3.4
__:分割符,必须是双下划线
Create_Table_User:描述,单下划线作为分割,这里的内容可以随意,会在更新历史中显示,如:Create Table User,这样就可以看出是创建一个新的User表。
sql文件内容
V1.0.0__Create_Table_User.sql
CREATE TABLE IF NOT EXISTS `tb_user` (
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT COMMENT '主键id',
`username` VARCHAR(64) NOT NULL UNIQUE COMMENT '用户名,唯一',
`password` VARCHAR(64) NOT NULL COMMENT '密码d',
PRIMARY KEY(`id`)
) ENGINE=InnoDB COMMENT='用户表';
复制代码
V1.0.1__Add_Column_User.sql
ALTER TABLE `tb_user` ADD `email` VARCHAR(100);
复制代码
V1.0.2__Edit_Column_User.sql
ALTER TABLE `tb_user` CHANGE `email` `phone` VARCHAR(11);
复制代码
V1.0.3__Delete_Column_User.sql
ALTER TABLE `tb_user` DROP COLUMN `phone`;
复制代码
V1.0.4__Create_Table_Product.sql
CREATE TABLE IF NOT EXISTS `tb_product` (
`id` BIGINT(20) UNSIGNED AUTO_INCREMENT COMMENT '主键id',
`name` VARCHAR(64) NOT NULL UNIQUE COMMENT '用户名,唯一',
`price` DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '价格',
PRIMARY KEY(`id`)
) ENGINE=InnoDB COMMENT='产品表';
复制代码
借助flyway框架我们就可以通过原生sql语句的方式对表进行更新,和hibernate一样,在项目启动的时候就会自动执行变化。
预防失误
使用sql语句做表修改的时候,一定要进行测试,不然本来只想删除一个字段,写错成删除全部字段就麻烦了,所以在应用前,可以改成test环境,使用h2内存数据库查看最终效果,确认无误后再引用到开发/生产环境。
让我们来添加测试环境:
- resources
application.yml
application-dev.yml
application-test.yml
复制代码
application.yml如下:
server:
port: 8888
spring:
profiles:
# active: dev
active: test
复制代码
application-dev.yml如下:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://myserverhost:3306/mytest?createDatabaseIfNotExist=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: admin
复制代码
application-test.yml如下:
spring:
datasource:
driver-class-name: org.h2.Driver
# 内存数据库
url: jdbc:h2:mem:DBName;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_LOWER=TRUE
h2:
console:
enabled: true # 开启console访问
settings:
trace: true # trace方便调试
web-allow-others: true # 允许web访问
path: /h2 # web访问路径
复制代码
flyway需要重新创建表,所以我们把刚刚方案一里创建的tb_user表删掉,然后启动程序
启动成功后访问:http://localhost:8888/h2
直接点击connect即可跳转到表结构页面,查看表结构是否是想要的效果,因为每次启动程序都会刷新,所以通过这种办法可以不断调式,最后切换回测试或生产环境。
然后只要提交代码到Git,团队成员拉取代码后重启程序即可自动同步。
好处
- 有清晰的数据库表更新历史记录
- 使用原生sql语句,定制性更高
缺陷
- 修改表的时候需要调试后再应用,没有方案一那么直观
数据库表映射到Java实体类
数据库表创建成功之后,往往需要将其转成Java实体类,手撸的话工作量大,还容易出现细节错误,借助映射工具可以很好的完成这个工作。
一开始用的是MyBatis Generator,但是没有研究出来怎么显示字段属性信息,像这样:
// 没有字段属性信息
@Id
@Column(name = "id")
private Long id;
// 有字段属性信息
@Id
@Column(name = "id", columnDefinition = "BIGINT(20) UNSIGNED NOT NULL auto_increment COMMENT '主键id'")
private Long id;
复制代码
虽然@Column注解有unique、nullable等属性,但是总不如columnDefinition显示的这么全面,所以为了满足我的需求,我花了些时间手撸了一个映射工具类DatabaseConvertUtil,支持实体类映射、Swagger注释、实体类注解,附带tk.mybatis的数据交互层自动生成,这个不必要,但是通过这个功能可以体现出自己手撸的工具能有极高的定制性。
使用说明
要测试全部功能,提前安装swagger和tkmybatis框架,当然这不是必要的,后面会有说明:
<!--swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
<!--Mybatis通用mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
复制代码
DatabaseConvertUtil,主要看数据库连接和main函数即可:
// 连接数据库
private static final String DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String URL = "jdbc:mysql://myserverhost:3306/mytest?characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC";
private static final String USERNAME = "root";
private static final String PASSWORD = "root";
public static void main(String[] args) {
doEntity = false; // 是否添加@Id,@Column等注解
doSwagger = true; // 是否添加Swagger注解
doDao = true; // 是否生成dao类,这个不是所有人都需要,但是可以体现出高定制性
entityPackageName = "com.cc.model.entity"; // 实体类所在的包
daoPackageName = "com.cc.dao"; // dao所在的包
entitySavePath = "E:\\chenc\\mygit\\mymall-demo\\flywayDemo\\src\\main\\java\\com\\cc\\model\\entity"; // 实体类包所在的绝对路径
daoSavePath = "E:\\chenc\\mygit\\mymall-demo\\flywayDemo\\src\\main\\java\\com\\cc\\dao"; // dao包所在的绝对路径
// 映射表明和实体类名
alias.put("tb_user", "TbUser");
alias.put("tb_product", "TbProduct");
System.out.println("开始进行映射...");
generatorEntityClass();
System.out.println("数据库表映射到实体类完成");
}
复制代码
-
doEntity:是否需要@Entity、@Table等实体类注解
-
doSwagger:是否需要swagger注解
-
alias:映射表名和实体类名
-
savePath:实体类存放路径
-
packageName:实体类存放的包名(因为没做好自动检测,所以目前还需要手写)
-
doDao:是否需要生成dao文件(目前针对tk.mybatis)
-
daoPackageName:dao文件包名
-
daoSavePath:dao存放路径
有些默认的可以不用设置,执行main函数即可,如果出错看下是否路径没有写对。
关于属性类型
看DatabaseConvertUtil类源码importMap和typeMap,如果有缺少的可以自行添加。
flyway其他配置
spring:
flyway:
enabled: true # 启用
validate-on-migrate: false # 不校验迁移文件
table: my_schema_history # 在微服务公用一个数据库时,为每个微服务指定一个flyway记录表名
baseline-on-migrate: true # 指定了表名后要加上这个
baseline-version: 0 # 指定了表名后要加上这个
复制代码