Java code changeset in liquibase

Yes, there is such feature. You can create a customChange:

    <customChange class="my.java.Class">
        <param name="id" value="2" />
    </customChange>

The class must implements the liquibase.change.custom.CustomTaskChange interface.

@Override
public void execute(final Database arg0) throws CustomChangeException {
    JdbcConnection dbConn = (JdbcConnection) arg0.getConnection();
    try {
         ... do funny stuff ...
    } catch (Exception e) {
        // swallow the exception !
    }
}

A complete example will look like this

Create a class that implements CustomTaskChange or CustomSqlChange.

package com.example;

import liquibase.change.custom.CustomTaskChange;
import liquibase.database.Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.CustomChangeException;
import liquibase.exception.SetupException;
import liquibase.exception.ValidationErrors;
import liquibase.logging.LogFactory;
import liquibase.resource.ResourceAccessor;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class DataLoaderTask implements CustomTaskChange {

    //to hold the parameter value
    private String file;


    private ResourceAccessor resourceAccessor;


    public String getFile() {
        return file;
    }

    public void setFile(String file) {
        this.file = file;
    }


    @Override
    public void execute(Database database) throws CustomChangeException {
        JdbcConnection databaseConnection = (JdbcConnection) database.getConnection();
        try {

            //Opening my data file
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(resourceAccessor.getResourceAsStream(file)));

            //Ignore header
            String str = in.readLine();

            while ((str = in.readLine()) != null && !str.trim().equals("")) {
                LogFactory.getLogger().info("Processing line "+ str);
                //Do whatever is necessary
            }
            in.close();
        } catch (Exception e) {
            throw new CustomChangeException(e);
        }
    }

    @Override
    public String getConfirmationMessage() {
        return null;
    }

    @Override
    public void setUp() throws SetupException {

    }

    @Override
    public void setFileOpener(ResourceAccessor resourceAccessor) {
        this.resourceAccessor = resourceAccessor;
    }

    @Override
    public ValidationErrors validate(Database database) {
        return null;
    }

}

In the changeset xml you could use the class as below

<changeSet id="1" author="murali" runAlways="false" failOnError="true" >
        <customChange class="com.example.DataLoaderTask">
            <param name="file" value="/com/example/data/user.csv" />
        </customChange>
</changeSet>

For me the data file is in src/main/resources/com/example/data directory

Hope this helps


While both answers were very helpful to me, none of them was complete regarding my use case of manipulating data in the database via a database query. I think when you want to use java code in your changeset (instead of plain SQL code) the real point is you want to read and write something from your database (and in between manipulate it in your java code).

In my case I wanted to encrypt all mail addresses stored in our database. I don't know if the following example is perfect but it does its job pretty well:

Part one: add a changelog

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">

  <changeSet id="1" author="IssueNo321" runAlways="false" failOnError="true">
    <customChange class="class.package.name.EncryptAllMailAddresses"/>
  </changeSet>
</databaseChangeLog>

Part two: add the changelog to your changelog list db.changelog-master.yaml

databaseChangeLog:
  - includeAll:
      relativeToChangelogFile: true
      path: changelogs/IssueNo321

Part three: implement migration java code

package class.package.name;

import liquibase.change.custom.CustomTaskChange;
import liquibase.database.Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.CustomChangeException;
import liquibase.exception.SetupException;
import liquibase.exception.ValidationErrors;
import liquibase.resource.ResourceAccessor;

public class EncryptAllMailAddresses implements CustomTaskChange {

  private EncryptionService encryptionService;

  @Override
  public void execute(Database database) throws CustomChangeException {

    try {
      var dbConn = (JdbcConnection) database.getConnection(); // autocommit is false

    try {
      var selectStatement = dbConn.createStatement();
      var updateStatement = dbConn.createStatement();

      var rs = selectStatement.executeQuery("Select id, email from emails");
      while (rs.next()) {
        var id = rs.getString("id");
        var email = rs.getString("email");

        email = encryptionService.encrypt(email);
        var updateSql = String.format("update emails set email = '%s' where id = %s", email, id);
        updateStatement.execute(updateSql);
      }

      selectStatement.close();
      updateStatement.close();
    } catch (Exception e) {
        throw new CustomChangeException(e);
    }
  }

  @Override
  public String getConfirmationMessage() {
    return null;
  }

  @Override
  public void setUp() throws SetupException {}

  @Override
  public ValidationErrors validate(Database database) {
    return null;
  }

  @Override
  public void setFileOpener(ResourceAccessor resourceAccessor) {
    // do nothing since we don't need additional resources
  }
}