From b90ac87628d97dd9958cdd52346aa95cb6bf08be Mon Sep 17 00:00:00 2001 From: "Keith R. Gustafson" Date: Wed, 4 Aug 2021 11:17:09 -0700 Subject: [PATCH] Provide a Liquibase database migrator as an alternative to Flyway --- gemini/pom.xml | 4 + .../gemini/data/LiquibaseMigrator.java | 154 ++++++++++++++++++ pom.xml | 6 + 3 files changed, 164 insertions(+) create mode 100644 gemini/src/main/java/com/techempower/gemini/data/LiquibaseMigrator.java diff --git a/gemini/pom.xml b/gemini/pom.xml index bdc96d40..e2521cb8 100755 --- a/gemini/pom.xml +++ b/gemini/pom.xml @@ -108,6 +108,10 @@ org.flywaydb flyway-core + + org.liquibase + liquibase-core + org.slf4j slf4j-api diff --git a/gemini/src/main/java/com/techempower/gemini/data/LiquibaseMigrator.java b/gemini/src/main/java/com/techempower/gemini/data/LiquibaseMigrator.java new file mode 100644 index 00000000..422f42f0 --- /dev/null +++ b/gemini/src/main/java/com/techempower/gemini/data/LiquibaseMigrator.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2018, TechEmpower, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name TechEmpower, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************/ +package com.techempower.gemini.data; + +import java.sql.*; +import java.util.*; +import java.util.Map.*; + +import javax.sql.*; + +import org.slf4j.*; + +import com.techempower.gemini.*; +import com.techempower.helper.*; +import com.techempower.util.*; + +import liquibase.*; +import liquibase.changelog.*; +import liquibase.database.*; +import liquibase.database.jvm.*; +import liquibase.exception.*; +import liquibase.resource.*; + +/** + * Implementation of DatabaseMigrator that uses the Liquibase library. + */ +public class LiquibaseMigrator implements DatabaseMigrator +{ + + private Logger log = LoggerFactory.getLogger(getClass()); + private GeminiApplication app; + private String changeLogFileName = "changelog.xml"; + + /** + * Constructor + */ + public LiquibaseMigrator(GeminiApplication app) + { + this.app = app; + this.app.getConfigurator().addConfigurable(this); + } + + @Override + public void configure(EnhancedProperties props) + { + // Build a map of Liquibase-specific configuration + final String confPrefix = "liquibase."; + Map conf = new HashMap<>(); + for (String name : props.names()) { + if (StringHelper.startsWithIgnoreCase(name, confPrefix)) { + conf.put(name, props.get(name)); + } + } + + // Log configuration customizations + for (Entry e : conf.entrySet()) { + log.info("Liquibase configuration customization: {}: {}", e.getKey(), e.getValue()); + } + + // The only configuration value we actually support is the changelog file name. + String configuredChangeLog = conf.get("changelog"); + if (StringHelper.isNonEmpty(configuredChangeLog)) { + this.changeLogFileName = configuredChangeLog; + } + log.info("Liquibase changelog: {}", changeLogFileName); + } + + /** + * Private helper for consistency between migrate() and listPendingMigrations() + */ + private Contexts createContexts() + { + // This is so we can determine whether to run the "insert sample data" + // migrations or not. + if (this.app.getVersion().isProduction()) { + return new Contexts("production"); + } else if (this.app.getVersion().isTest()) { + return new Contexts("test"); + } else if (this.app.getVersion().isDevelopment()) { + return new Contexts("development"); + } + return new Contexts(); + } + + @Override + public int migrate(DataSource dataSource) + { + log.info("Starting migrate"); + try (Connection conn = dataSource.getConnection(); + Liquibase liquibase = new liquibase.Liquibase(changeLogFileName, new ClassLoaderResourceAccessor(), + DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(conn)))) { + LabelExpression labels = new LabelExpression(); + Contexts c = createContexts(); + int countUnrunChangesetsBefore = liquibase.listUnrunChangeSets(c, labels).size(); + log.info("Unrun changesets: {}", countUnrunChangesetsBefore); + if (countUnrunChangesetsBefore <= 0) { + // Nothing to do. + return 0; + } + liquibase.update(c, labels); + int countUnrunChangesetsAfter = liquibase.listUnrunChangeSets(c, labels).size(); + return countUnrunChangesetsBefore - countUnrunChangesetsAfter; + } catch (SQLException | LiquibaseException e) { + log.error("Liquibase update caught ", e); + return 0; + } + } + + @Override + public List listPendingMigrations(DataSource dataSource) + { + log.info("Starting listPendingMigrations"); + try (Connection conn = dataSource.getConnection(); + Liquibase liquibase = new liquibase.Liquibase(changeLogFileName, new ClassLoaderResourceAccessor(), + DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(conn)))) { + LabelExpression labels = new LabelExpression(); + Contexts c = createContexts(); + List unrunChangeSets = liquibase.listUnrunChangeSets(c, labels); + List toReturn = new ArrayList<>(); + for (ChangeSet changeSet : unrunChangeSets) { + toReturn.add("Unrun changeset: " + changeSet + ". Change log: " + changeSet.getChangeLog() + ". Changes: " + + changeSet.getChanges()); + } + return toReturn; + } catch (SQLException | LiquibaseException e) { + log.error("Liquibase update caught ", e); + return Collections.emptyList(); + } + } +} diff --git a/pom.xml b/pom.xml index 58f7635c..cc6da54b 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,7 @@ 3.4.5 1.8.0-beta4 7.3.1 + 4.4.2 2.13.3 1.3.0-alpha2 @@ -294,6 +295,11 @@ flyway-core ${flyway.version} + + org.liquibase + liquibase-core + ${liquibase.version} +