4.21.1 is out, deprecates Liquibase.update()

4.21 seems to be out, but there are no release notes, yet. Could you please add them?

I am using liquibase embedded in a Kotlin application, as described in section " Embed Liquibase into your product" here: 3 Ways to Run Liquibase

My gradle update check task alerted me of the new version, and I tried to update because it warns that 4.20 has a security flaw in one the dependencies [CVE-2022-1471].

However, after the update my build fails with a deprecated warning at Liquibase.update().
At the update() method I use, there is just a @Deprecated annotation with no hint at all what to use instead.
When follow the trail in the liquibase sources to the overloaded method that all the update variants delegate to, I find @deprecated Use CommandStep

Not that helpful :frowning:

Oh, I just noticed that there are new update() overloads which are not deprecated, all taking a Writer … and no such method has any JavaDoc :frowning:

What is this Writer? Is it used for logging? If so, how would I get a Writer from log4j2 or slf4j?

Finally, could you please update the 3 Ways to Run Liquibase to use a non-deprecated update method?

Here are the release notes:

Yeah, that javadoc could be improved. “Use CommandStep” is not really even the right answer for most people. Also the versions with Writer should either be deprecated now or will be soon. We aren’t planning to actually remove the methods for a while, but still good to shift to the newer APIs when you can

The replacement for the Liquibase class in general is the liquibase.command.CommandScope class. command.CommandScope - contribute.liquibase.com gives some more information on it. We are creating that class vs. the original Liquibase facade to be flexible enough to work with everything Liquibase can do now and is looking to do in the future. A static list of methods and arguments just wasn’t cutting it anymore.

Your new version of liquibase.update() will be more like:

   try {
            new CommandScope("update")
                    .addArgumentValue("changeLogFile", "my/changelog.xml")
                    .execute();
        } catch (CommandExecutionException e) {
            System.out.println("Error running update: "+e.getMessage());
        }

Nathan

1 Like

I’m doing this (copy paste from a sample used in the integration test for my Liquibase apache karaf feature):

            applyLiquibaseChangelist(connection, "sample-db-changelog/db-changelog-1.0.0.xml");

where applyLiquibaseChangeList first creates a Liquibase object, with a JDBC connection and a resource accessor and the resource classpath to the changelog file embedded in the OSGi bundle (i.e. the jar file):

    private void applyLiquibaseChangelist(Connection connection, String changelistClasspathResource) throws LiquibaseException {
        try(Liquibase liquibase = createLiquibaseInstance(connection, changelistClasspathResource)) {
            liquibase.update("");
        }
    }

    private Liquibase createLiquibaseInstance(Connection connection, String changelistClasspathResource) throws LiquibaseException {
        DatabaseConnection databaseConnection = new JdbcConnection(connection);
        var resourceAccessor = new OSGiResourceAccessor(bundle);
        return new Liquibase(changelistClasspathResource, resourceAccessor, databaseConnection);
    }

It’s not obvious to me how this should be translated to a command scope.

I’ve looked at the linked to page and I’ve looked at the CommandScope javadocs but I didn’t find enlightnment, unfortunately… :slightly_smiling_face:

First hurdle: how do I specify that a changelog file should be fetched from a classpath resource and not from a file on disk?

(and do I need to do something OSGi specific here?)

Second hurdle: if I have a JDBC DataSource, how can I use that (instead of JDBC connection info)

Here is what I ended up with, also using an existing javax.sql.DataSource.
It’s Kotlin code but should be readable enough for a Java dev:

private fun migrateDb(dataSource: DataSource) {
    logger.info("Running liquibase update")

    dataSource.connection.use { connection ->
        val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(connection))
        val changeLogFile = "/liquibase-changelog.xml"

        val scopeObjects = mapOf(
            Scope.Attr.database.name to database,
            Scope.Attr.resourceAccessor.name to ClassLoaderResourceAccessor()
        )

        Scope.child(scopeObjects) {
            val updateCommand = CommandScope(*UpdateCommandStep.COMMAND_NAME).apply {
                addArgumentValue(DbUrlConnectionCommandStep.DATABASE_ARG, database)
                addArgumentValue(UpdateCommandStep.CHANGELOG_FILE_ARG, changeLogFile)
                addArgumentValue(UpdateCommandStep.CONTEXTS_ARG, Contexts().toString())
                addArgumentValue(UpdateCommandStep.LABEL_FILTER_ARG, LabelExpression().originalString)
                addArgumentValue(ChangeExecListenerCommandStep.CHANGE_EXEC_LISTENER_ARG, null as ChangeExecListener?)
                addArgumentValue(DatabaseChangelogCommandStep.CHANGELOG_PARAMETERS, ChangeLogParameters(database))

                setOutput(OutputStream.nullOutputStream()) // Suppress output to stdout (logging will still occur)
            }
            updateCommand.execute()
        }
    }
}

Ok, I’ve tried to translate this into Java code:

    private void applyLiquibaseChangelist(Connection connection, String changelistClasspathResource) throws Exception {
        var database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
        Map<String, Object> scopeObjects = Map.of(
            Scope.Attr.database.name(), database,
            Scope.Attr.resourceAccessor.name(), new OSGiResourceAccessor(bundle));

        Scope.child(scopeObjects, (ScopedRunner) () -> new CommandScope("update")
                    .addArgumentValue(DbUrlConnectionCommandStep.DATABASE_ARG, database)
                    .addArgumentValue(UpdateCommandStep.CHANGELOG_FILE_ARG, changelistClasspathResource)
                    .addArgumentValue(DatabaseChangelogCommandStep.CHANGELOG_PARAMETERS, new ChangeLogParameters(database))
                    .execute());
    }

But it fails with the following error (fails in execute()):

liquibase.exception.CommandExecutionException: java.lang.RuntimeException: Cannot end scope xlveporhmt when currently at scope bsmuuxncug
	at liquibase.command.CommandScope.execute(CommandScope.java:235) ~[?:?]
	at no.priv.bang.karaf.sample.db.liquibase.test.SampleDbLiquibaseRunner.lambda$applyLiquibaseChangelist$1(SampleDbLiquibaseRunner.java:100) ~[?:?]
	at liquibase.Scope.lambda$child$0(Scope.java:194) ~[?:?]
	at liquibase.Scope.child(Scope.java:203) ~[?:?]
	at liquibase.Scope.child(Scope.java:193) ~[?:?]
	at liquibase.Scope.child(Scope.java:172) ~[?:?]
	at no.priv.bang.karaf.sample.db.liquibase.test.SampleDbLiquibaseRunner.applyLiquibaseChangelist(SampleDbLiquibaseRunner.java:96) ~[?:?]
	at no.priv.bang.karaf.sample.db.liquibase.test.SampleDbLiquibaseRunner.prepare(SampleDbLiquibaseRunner.java:65) ~[?:?]
	at org.ops4j.pax.jdbc.config.impl.DataSourceRegistration.<init>(DataSourceRegistration.java:88) ~[?:?]
	at org.ops4j.pax.jdbc.config.impl.DataSourceConfigManager.lambda$null$4(DataSourceConfigManager.java:95) ~[?:?]
	at org.ops4j.pax.jdbc.config.impl.ServiceTrackerHelper$1.addingService(ServiceTrackerHelper.java:132) ~[?:?]
	at org.osgi.util.tracker.ServiceTracker$Tracked.customizerAdding(ServiceTracker.java:943) ~[osgi.core-8.0.0.jar:?]
	at org.osgi.util.tracker.ServiceTracker$Tracked.customizerAdding(ServiceTracker.java:871) ~[osgi.core-8.0.0.jar:?]
	at org.osgi.util.tracker.AbstractTracked.trackAdding(AbstractTracked.java:256) ~[osgi.core-8.0.0.jar:?]
	at org.osgi.util.tracker.AbstractTracked.track(AbstractTracked.java:229) ~[osgi.core-8.0.0.jar:?]
	at org.osgi.util.tracker.ServiceTracker$Tracked.serviceChanged(ServiceTracker.java:903) ~[osgi.core-8.0.0.jar:?]
	at org.apache.felix.framework.EventDispatcher.invokeServiceListenerCallback(EventDispatcher.java:990) ~[?:?]
	at org.apache.felix.framework.EventDispatcher.fireEventImmediately(EventDispatcher.java:838) ~[?:?]
	at org.apache.felix.framework.EventDispatcher.fireServiceEvent(EventDispatcher.java:545) ~[?:?]
	at org.apache.felix.framework.Felix.fireServiceEvent(Felix.java:4863) ~[?:?]
	at org.apache.felix.framework.Felix.registerService(Felix.java:3834) ~[?:?]
	at org.apache.felix.framework.BundleContextImpl.registerService(BundleContextImpl.java:328) ~[?:?]
	at org.apache.felix.framework.BundleContextImpl.registerService(BundleContextImpl.java:302) ~[?:?]
	at org.ops4j.pax.jdbc.derby.impl.Activator.start(Activator.java:34) ~[?:?]
	at org.apache.felix.framework.util.SecureAction.startActivator(SecureAction.java:849) ~[?:?]
	at org.apache.felix.framework.Felix.activateBundle(Felix.java:2429) ~[?:?]
	at org.apache.felix.framework.Felix.startBundle(Felix.java:2335) ~[?:?]
	at org.apache.felix.framework.BundleImpl.start(BundleImpl.java:1006) ~[?:?]
	at org.apache.felix.framework.BundleImpl.start(BundleImpl.java:992) ~[?:?]
	at org.apache.karaf.features.internal.service.BundleInstallSupportImpl.startBundle(BundleInstallSupportImpl.java:165) ~[?:?]
	at org.apache.karaf.features.internal.service.FeaturesServiceImpl.startBundle(FeaturesServiceImpl.java:1160) ~[?:?]
	at org.apache.karaf.features.internal.service.Deployer.deploy(Deployer.java:1041) ~[?:?]
	at org.apache.karaf.features.internal.service.FeaturesServiceImpl.doProvision(FeaturesServiceImpl.java:1069) ~[?:?]
	at org.apache.karaf.features.internal.service.FeaturesServiceImpl.lambda$doProvisionInThread$13(FeaturesServiceImpl.java:1004) ~[?:?]
	at java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[?:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[?:?]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[?:?]
	at java.lang.Thread.run(Thread.java:829) ~[?:?]
	Suppressed: java.sql.SQLException: Cannot close a connection while a transaction is still active.
		at org.apache.derby.impl.jdbc.SQLExceptionFactory.getSQLException(Unknown Source) ~[?:?]
		at org.apache.derby.impl.jdbc.SQLExceptionFactory.getSQLException(Unknown Source) ~[?:?]
		at org.apache.derby.impl.jdbc.Util.generateCsSQLException(Unknown Source) ~[?:?]
		at org.apache.derby.impl.jdbc.Util.generateCsSQLException(Unknown Source) ~[?:?]
		at org.apache.derby.impl.jdbc.EmbedConnection.newSQLException(Unknown Source) ~[?:?]
		at org.apache.derby.impl.jdbc.EmbedConnection.checkForTransactionInProgress(Unknown Source) ~[?:?]
		at org.apache.derby.impl.jdbc.EmbedConnection.close(Unknown Source) ~[?:?]
		at no.priv.bang.karaf.sample.db.liquibase.test.SampleDbLiquibaseRunner.prepare(SampleDbLiquibaseRunner.java:64) ~[?:?]
		at org.ops4j.pax.jdbc.config.impl.DataSourceRegistration.<init>(DataSourceRegistration.java:88) ~[?:?]
		at org.ops4j.pax.jdbc.config.impl.DataSourceConfigManager.lambda$null$4(DataSourceConfigManager.java:95) ~[?:?]
		at org.ops4j.pax.jdbc.config.impl.ServiceTrackerHelper$1.addingService(ServiceTrackerHelper.java:132) ~[?:?]
		at org.osgi.util.tracker.ServiceTracker$Tracked.customizerAdding(ServiceTracker.java:943) ~[osgi.core-8.0.0.jar:?]
		at org.osgi.util.tracker.ServiceTracker$Tracked.customizerAdding(ServiceTracker.java:871) ~[osgi.core-8.0.0.jar:?]
		at org.osgi.util.tracker.AbstractTracked.trackAdding(AbstractTracked.java:256) ~[osgi.core-8.0.0.jar:?]
		at org.osgi.util.tracker.AbstractTracked.track(AbstractTracked.java:229) ~[osgi.core-8.0.0.jar:?]
		at org.osgi.util.tracker.ServiceTracker$Tracked.serviceChanged(ServiceTracker.java:903) ~[osgi.core-8.0.0.jar:?]
		at org.apache.felix.framework.EventDispatcher.invokeServiceListenerCallback(EventDispatcher.java:990) ~[?:?]
		at org.apache.felix.framework.EventDispatcher.fireEventImmediately(EventDispatcher.java:838) ~[?:?]
		at org.apache.felix.framework.EventDispatcher.fireServiceEvent(EventDispatcher.java:545) ~[?:?]
		at org.apache.felix.framework.Felix.fireServiceEvent(Felix.java:4863) ~[?:?]
		at org.apache.felix.framework.Felix.registerService(Felix.java:3834) ~[?:?]
		at org.apache.felix.framework.BundleContextImpl.registerService(BundleContextImpl.java:328) ~[?:?]
		at org.apache.felix.framework.BundleContextImpl.registerService(BundleContextImpl.java:302) ~[?:?]
		at org.ops4j.pax.jdbc.derby.impl.Activator.start(Activator.java:34) ~[?:?]
		at org.apache.felix.framework.util.SecureAction.startActivator(SecureAction.java:849) ~[?:?]
		at org.apache.felix.framework.Felix.activateBundle(Felix.java:2429) ~[?:?]
		at org.apache.felix.framework.Felix.startBundle(Felix.java:2335) ~[?:?]
		at org.apache.felix.framework.BundleImpl.start(BundleImpl.java:1006) ~[?:?]
		at org.apache.felix.framework.BundleImpl.start(BundleImpl.java:992) ~[?:?]
		at org.apache.karaf.features.internal.service.BundleInstallSupportImpl.startBundle(BundleInstallSupportImpl.java:165) ~[?:?]
		at org.apache.karaf.features.internal.service.FeaturesServiceImpl.startBundle(FeaturesServiceImpl.java:1160) ~[?:?]
		at org.apache.karaf.features.internal.service.Deployer.deploy(Deployer.java:1041) ~[?:?]
		at org.apache.karaf.features.internal.service.FeaturesServiceImpl.doProvision(FeaturesServiceImpl.java:1069) ~[?:?]
		at org.apache.karaf.features.internal.service.FeaturesServiceImpl.lambda$doProvisionInThread$13(FeaturesServiceImpl.java:1004) ~[?:?]
		at java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[?:?]
		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[?:?]
		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[?:?]
		at java.lang.Thread.run(Thread.java:829) ~[?:?]
	Caused by: org.apache.derby.iapi.error.StandardException: Cannot close a connection while a transaction is still active.
		at org.apache.derby.iapi.error.StandardException.newException(Unknown Source) ~[?:?]
		at org.apache.derby.impl.jdbc.SQLExceptionFactory.wrapArgsForTransportAcrossDRDA(Unknown Source) ~[?:?]
		... 38 more
Caused by: java.lang.RuntimeException: Cannot end scope xlveporhmt when currently at scope bsmuuxncug
	at liquibase.Scope.exit(Scope.java:241) ~[?:?]
	at liquibase.Scope.child(Scope.java:205) ~[?:?]
	at liquibase.Scope.child(Scope.java:193) ~[?:?]
	at liquibase.Scope.child(Scope.java:189) ~[?:?]
	at liquibase.command.CommandScope.addOutputFileToMdc(CommandScope.java:254) ~[?:?]
	at liquibase.command.CommandScope.execute(CommandScope.java:207) ~[?:?]
	... 37 more