當項目每次進行版本升級的時候,如果在這次迭代中涉及表結構變更,需要將不同的生產環境下,都需要同步表結構的DDL語句,比較麻煩,而且還有可能忘記同步腳本,導致生產環境報錯....
該方案采用SpringBoot+Mybatis/MybatisPlus框架,完成在項目啟動時,自動化執行sql腳本,并且同時支持版本號【如果當前版本號高于該sql文件,則不執行】。
1、先創建一張表,專門用來記錄已經同步過的sql腳本文件名、對應的版本號。
CREATE TABLE `hd_version` (`id` varchar(64) NOT NULL,`version` varchar(64) DEFAULT NULL COMMENT '版本號',`created` datetime DEFAULT NULL COMMENT '創建時間',`remark` varchar(500) DEFAULT NULL COMMENT '備注',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='數據版本';
import java.util.Date;
import lombok.Data;
@Data
public class HdVersionEntity {/*** 主鍵id*/private String id;/*** 版本號(一般是文件名去掉文件后綴)*/private String version;/*** 文件名*/private String remark;/*** 創建時間*/private Date created;
}
import lombok.Data;@Data
public class SchemaData {/*** 版本號*/public String version;/*** 文件名*/public String fileName;public SchemaData(String version, String fileName) {this.version = version;this.fileName = fileName;}
}
?2、接著編寫dao層
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;@Mapper
public interface HdCommonDao {/*** 查詢表中是否存在當前版本號* @param version* @return*/int selectVersion(@Param("version") String version);/*** 插入版本* @param entity* @return*/int insertVersion(HdVersionEntity entity);/*** 執行sql,可以是DML、DDL* @param sql*/@Update("${sql}")void updateSql(@Param("sql") String sql);
}
3、以及對應的Mapper文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--這里文件空間命名改成自己路徑下的-->
<mapper namespace="com.xxx.DatabaseAutoFill.HdCommonDao"><select id="selectVersion" resultType="int">selecT count(1) from hd_versionwhere version = #{version}</select><select id="selectTableExist" resultType="int">select count(*) count from information_schema.TABLES where TABLE_NAME = #{tableName} and table_schema = (select database())</select><insert id="insertVersion">insert into hd_version(id,version, remark, created) values (uuid(),#{version}, #{remark}, #{created})</insert></mapper>
4、 編寫實現類
注意,這里是將整段邏輯放在ApplicationRunner接口下執行,即當Spring容器加載完之后,會立即執行該方法。
@Order(1)
@Component
@Slf4j
public class HdSchemaExecutor implements ApplicationRunner {@AutowiredHdCommonDao hdCommonDao;// 數據庫腳本文件列表private static final String PREFIX = "--v";@Override@Transactionalpublic void run(ApplicationArguments args) throws IOException {String basePath = "/dbVersion/MySQL.sql";InputStream inputStream = this.getClass().getResourceAsStream(basePath);String sqlScript = IoUtil.readUtf8(inputStream);assert inputStream != null;inputStream.close();/*** 一次至多只會執行一個版本,其實我們可以拿到所有的版本并執行最后一個版本即可*/List<String> versionList = new ArrayList<>();String[] lines = sqlScript.split("\n");for (String line : lines) {if(line.toLowerCase().contains(PREFIX)){versionList.add(line);}}// 得到版本號整串String latestVersion = versionList.get(versionList.size()-1);// 寫入數據庫的版本號前綴String version = latestVersion.substring(latestVersion.lastIndexOf("-")+1).trim().toLowerCase();int index = sqlScript.lastIndexOf(latestVersion); // 查找s2在s1中的起始位置String result = "";if (index != -1) {// 截取s2在s1中結束位置之后的部分result = sqlScript.substring(index + latestVersion.length());} else {log.info("current version exception:{}",version);LogUtil.info(version, "current version exception");}//String[] resultList = result.split("\n");String[] resultList = result.split(";");int cnt = hdCommonDao.selectVersion(version);boolean successInsert = false;// 說明不需要寫入庫if(cnt ==1 )return;for (String line : resultList) {if(!line.toLowerCase().contains("drop") && !line.toLowerCase().contains("delete") && line.length() > 25 && !line.contains("--")) {//開始執行插入操作try {hdCommonDao.updateSql(line.trim());successInsert = true;log.info("version:{},start sql script:{}",version,line.trim());LogUtil.info("version, sql script:",version,line.trim());} catch (Exception e) {log.info("version:{},sql執行異常:{}",version,line.trim());LogUtil.info("sql執行異常",line.trim());}}}if(successInsert){HdVersionEntity entity = new HdVersionEntity();entity.setVersion(version);entity.setCreated(new Date());hdCommonDao.insertVersion(entity);}log.info("auto deploying sql finished...");}
}
這里主要干三件事:
讀取指定路徑下的文件夾中的所有文件
根據這些文件的文件名去表里查,是否插入過,沒有說明需要被插入,即需要執行的sql腳本
執行sql腳本
我這里的路徑是resources下的相對路徑,因為我這個代碼是要打包放到線上環境的,用絕對路徑可能會報(FILE NOT FOUND ERROR)FNFE。?
PS
以上方法對于Spring容器加載時,沒有強依賴的表,是可以通用的?(可能有點拗口)。
即,如果Spring容器啟動時,如果需要依賴某張表,否則啟動失敗的話怎么辦,還能用我們上述方法嗎?
理論上是不行的,我這里將容器啟動時,必須強依賴的表(Quartz框架)刪去,啟動時報錯。
那對應這種情況,該怎么解決呢?
?其實這種框架,都會提供注解,如:
表明,在項目啟動的時候,會自動完成jdbc的初始化,即如果你沒有表,會先給你執行表的創建,因此不需要我們去考慮。
spring.quartz.jdbc.initialize-schema=always
Quartz也起來了。?
寫在最后
由于這個工程是臨時突加的,我也不好隨便就測試環境的庫來刪刪改改,因此我在本地windows上用docker部署了mysql,來測試的。以下是在windows上的docker部署mysql步驟:
docker pull mysql:8.0
在c盤用戶目錄下,創建conf、data、logs三個文件夾
?在conf目錄下,創建my.cnf文件,里面編寫如下內容。
[mysql]
#設置mysql客戶端默認字符集
default-character-set=UTF8MB4
[mysqld]
#設置3306端口
port=3306
#允許最大連接數
max_connections=200
#允許連接失敗的次數
max_connect_errors=10
#默認使用“mysql_native_password”插件認證
default_authentication_plugin=mysql_native_password
#服務端使用的字符集默認為8比特編碼的latin1字符集
character-set-server=UTF8MB4
#開啟查詢緩存
explicit_defaults_for_timestamp=true
#創建新表時將使用的默認存儲引擎
default-storage-engine=INNODB
#等待超時時間秒
wait_timeout=60
#交互式連接超時時間秒
interactive-timeout=600
# 對數據庫表大小寫不敏感設置,默認設置為小寫,比較也全部設置為小寫在比較
lower-case-table-names=1
# 設置默認時區
default-time_zone='+8:00'
啟動容器,注意在windows下?需要把每行后面的?`\`刪去,否在windows下會啟動失敗
?docker run --name mysql8.0 \
-v D:\docker\data\mysql8.0\config\my.cnf:/etc/mysql/my.cnf \
-v D:\docker\data\mysql8.0\data:/var/lib/mysql \
-v D:\docker\data\mysql8.0\logs:/logs -p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \-e TZ=Asia/Shanghai \
-d mysql:8.0 \
--lower-case-table-names=1
這樣,理論上就能啟動成功了。
分享幾個常用的命令:
docker exec -it 容器名稱/容器id??bash? #進入容器
docker logs 容器名稱/容器id -f -n=100 查看容器最后一百行日志