|
@@ -0,0 +1,192 @@
|
|
|
|
|
+package com.fs.framework.liquibase;
|
|
|
|
|
+
|
|
|
|
|
+import liquibase.Liquibase;
|
|
|
|
|
+import liquibase.changelog.ChangeSet;
|
|
|
|
|
+import liquibase.database.Database;
|
|
|
|
|
+import liquibase.database.DatabaseFactory;
|
|
|
|
|
+import liquibase.database.jvm.JdbcConnection;
|
|
|
|
|
+import liquibase.resource.ClassLoaderResourceAccessor;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+
|
|
|
|
|
+import javax.sql.DataSource;
|
|
|
|
|
+import java.sql.Connection;
|
|
|
|
|
+import java.sql.ResultSet;
|
|
|
|
|
+import java.sql.SQLException;
|
|
|
|
|
+import java.sql.Statement;
|
|
|
|
|
+import java.util.LinkedHashSet;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 执行 Liquibase 迁移并统计 pending changeset。
|
|
|
|
|
+ */
|
|
|
|
|
+public class LiquibaseMigrationRunner {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(LiquibaseMigrationRunner.class);
|
|
|
|
|
+
|
|
|
|
|
+ private final LiquibaseProperties properties;
|
|
|
|
|
+
|
|
|
|
|
+ public LiquibaseMigrationRunner(LiquibaseProperties properties) {
|
|
|
|
|
+ this.properties = properties;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public MigrationAudit audit(DataSource dataSource) {
|
|
|
|
|
+ configureLockWait();
|
|
|
|
|
+ try (Connection connection = dataSource.getConnection()) {
|
|
|
|
|
+ Database database = createDatabase(connection);
|
|
|
|
|
+ Liquibase liquibase = createLiquibase(database);
|
|
|
|
|
+ List<ChangeSet> all = liquibase.getDatabaseChangeLog().getChangeSets();
|
|
|
|
|
+ List<ChangeSet> pending = liquibase.listUnrunChangeSets(null);
|
|
|
|
|
+ Set<String> parsedKeys = toChangeSetKeys(all);
|
|
|
|
|
+ Set<String> pendingKeys = toChangeSetKeys(pending);
|
|
|
|
|
+ Set<String> executedKeys = loadExecutedChangeSetKeys(dataSource);
|
|
|
|
|
+ Set<String> missingInDb = new LinkedHashSet<>(parsedKeys);
|
|
|
|
|
+ missingInDb.removeAll(executedKeys);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("Liquibase 审计: 已解析={}, 待执行={}, DATABASECHANGELOG 已记录={}, 缺失记录={}",
|
|
|
|
|
+ parsedKeys.size(), pendingKeys.size(), executedKeys.size(), missingInDb.size());
|
|
|
|
|
+ for (ChangeSet changeSet : pending) {
|
|
|
|
|
+ log.info("待执行 changeset -> id={}, author={}, file={}",
|
|
|
|
|
+ changeSet.getId(), changeSet.getAuthor(), changeSet.getFilePath());
|
|
|
|
|
+ }
|
|
|
|
|
+ for (String missing : missingInDb) {
|
|
|
|
|
+ log.warn("DATABASECHANGELOG 缺失 changeset -> {}", missing);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new MigrationAudit(parsedKeys.size(), pendingKeys.size(), parsedKeys, pendingKeys, missingInDb);
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ throw new IllegalStateException("Liquibase migration 审计失败", ex);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public int countPendingChangeSets(DataSource dataSource) {
|
|
|
|
|
+ return audit(dataSource).getPendingTotal();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void update(DataSource dataSource) {
|
|
|
|
|
+ configureLockWait();
|
|
|
|
|
+ try (Connection connection = dataSource.getConnection()) {
|
|
|
|
|
+ connection.setAutoCommit(false);
|
|
|
|
|
+ Database database = createDatabase(connection);
|
|
|
|
|
+ Liquibase liquibase = createLiquibase(database);
|
|
|
|
|
+ try {
|
|
|
|
|
+ int totalChangeSets = liquibase.getDatabaseChangeLog().getChangeSets().size();
|
|
|
|
|
+ log.info("开始执行 Liquibase update, changelog={}, 已解析 changeset 总数={}",
|
|
|
|
|
+ properties.getChangelog(), totalChangeSets);
|
|
|
|
|
+ if (totalChangeSets == 0) {
|
|
|
|
|
+ throw new IllegalStateException(
|
|
|
|
|
+ "Liquibase 未解析到任何 changeset,请检查 master changelog 是否正确(SQL master 不支持 --include,需使用 XML)");
|
|
|
|
|
+ }
|
|
|
|
|
+ liquibase.update((String) null);
|
|
|
|
|
+ connection.commit();
|
|
|
|
|
+ log.info("Liquibase update 执行成功");
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ connection.rollback();
|
|
|
|
|
+ } catch (Exception rollbackEx) {
|
|
|
|
|
+ log.warn("Liquibase update 回滚失败", rollbackEx);
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new IllegalStateException("Liquibase 迁移失败,已回滚,禁止启动", ex);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (IllegalStateException ex) {
|
|
|
|
|
+ throw ex;
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ throw new IllegalStateException("Liquibase 迁移失败,已回滚,禁止启动", ex);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Set<String> loadExecutedChangeSetKeys(DataSource dataSource) {
|
|
|
|
|
+ Set<String> keys = new LinkedHashSet<>();
|
|
|
|
|
+ String sql = "SELECT ID, AUTHOR FROM DATABASECHANGELOG";
|
|
|
|
|
+ try (Connection connection = dataSource.getConnection();
|
|
|
|
|
+ Statement statement = connection.createStatement();
|
|
|
|
|
+ ResultSet rs = statement.executeQuery(sql)) {
|
|
|
|
|
+ while (rs.next()) {
|
|
|
|
|
+ keys.add(rs.getString("AUTHOR") + ":" + rs.getString("ID"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return keys;
|
|
|
|
|
+ } catch (SQLException ex) {
|
|
|
|
|
+ if (isTableNotExists(ex)) {
|
|
|
|
|
+ return keys;
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new IllegalStateException("读取 DATABASECHANGELOG 失败", ex);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Set<String> toChangeSetKeys(List<ChangeSet> changeSets) {
|
|
|
|
|
+ Set<String> keys = new LinkedHashSet<>();
|
|
|
|
|
+ for (ChangeSet changeSet : changeSets) {
|
|
|
|
|
+ keys.add(changeSet.getAuthor() + ":" + changeSet.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ return keys;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Database createDatabase(Connection connection) throws Exception {
|
|
|
|
|
+ return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Liquibase createLiquibase(Database database) throws Exception {
|
|
|
|
|
+ ClassLoader classLoader = getClass().getClassLoader();
|
|
|
|
|
+ if (classLoader.getResource(properties.getChangelog()) == null) {
|
|
|
|
|
+ throw new IllegalStateException("未找到 master changelog: " + properties.getChangelog());
|
|
|
|
|
+ }
|
|
|
|
|
+ Liquibase liquibase = new Liquibase(properties.getChangelog(), new ClassLoaderResourceAccessor(), database);
|
|
|
|
|
+ int parsed = liquibase.getDatabaseChangeLog().getChangeSets().size();
|
|
|
|
|
+ if (parsed == 0) {
|
|
|
|
|
+ throw new IllegalStateException(
|
|
|
|
|
+ "Liquibase 未解析到 changeset,请检查 SQL 文件是否包含 "
|
|
|
|
|
+ + "'--liquibase formatted sql' 及 '--changeset author:id' 格式");
|
|
|
|
|
+ }
|
|
|
|
|
+ return liquibase;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void configureLockWait() {
|
|
|
|
|
+ System.setProperty("liquibase.changelogLockWaitTimeInMinutes",
|
|
|
|
|
+ String.valueOf(properties.getLockWaitTimeoutMinutes()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isTableNotExists(SQLException ex) {
|
|
|
|
|
+ String message = ex.getMessage();
|
|
|
|
|
+ return message != null && (message.contains("doesn't exist") || message.contains("does not exist"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static final class MigrationAudit {
|
|
|
|
|
+ private final int parsedTotal;
|
|
|
|
|
+ private final int pendingTotal;
|
|
|
|
|
+ private final Set<String> parsedKeys;
|
|
|
|
|
+ private final Set<String> pendingKeys;
|
|
|
|
|
+ private final Set<String> missingInDatabase;
|
|
|
|
|
+
|
|
|
|
|
+ public MigrationAudit(int parsedTotal,
|
|
|
|
|
+ int pendingTotal,
|
|
|
|
|
+ Set<String> parsedKeys,
|
|
|
|
|
+ Set<String> pendingKeys,
|
|
|
|
|
+ Set<String> missingInDatabase) {
|
|
|
|
|
+ this.parsedTotal = parsedTotal;
|
|
|
|
|
+ this.pendingTotal = pendingTotal;
|
|
|
|
|
+ this.parsedKeys = parsedKeys;
|
|
|
|
|
+ this.pendingKeys = pendingKeys;
|
|
|
|
|
+ this.missingInDatabase = missingInDatabase;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public int getParsedTotal() {
|
|
|
|
|
+ return parsedTotal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public int getPendingTotal() {
|
|
|
|
|
+ return pendingTotal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Set<String> getParsedKeys() {
|
|
|
|
|
+ return parsedKeys;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Set<String> getPendingKeys() {
|
|
|
|
|
+ return pendingKeys;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Set<String> getMissingInDatabase() {
|
|
|
|
|
+ return missingInDatabase;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|