CorDapp Database Upgrade/ Migration-Development Perspective

January 08, 2020
Node applies the schemas automatically onto the database

We have seen how to set up and connect to a PostgreSQL database in the previous blog post. In this blog, we will see how we can upgrade custom CorDapp database tables using Liquibase scripts in development mode.

Both the internals of the Corda and external CorDapps use Liquibase for database schema versioning. This makes Corda database agnostic. CorDapp custom tables are created or upgraded automatically using Liquibase. Liquibase supports writing ddl/dml statements in many formats (XML, JSON, SQL, YAML).

In development mode, we will connect the node to the database using admin access. When the node is started, the database agnostic scripts are automatically converted to database specific scripts and are applied automatically onto the database.

Broadly there are 6 steps to perform a CorDapp database upgrade/migration.

  1. Define the Liquibase script.
  2. Define a custom schema.
  3. Define IOUState by extending with QueryableState and override IOUState’s supported Schemas, generateMappedSchema, migrationResource.
  4. Setup users, schema permissions, update node.conf and start the node as described in the previous blog. Database tables are created automatically.
  5. Stop the node. Update the schema by adding a new column. Create a new Liquibase script and update the master-changelog to include the script. Replace old jars with new jars.
  6. Start the node. The new script automatically applies to the database.

Step 1: Define the Liquibase script.

  1. Add the Liquibase script to create the IOU table.
  2. Line 6 defines a new changeset. Line 7 defines a create table command by specifying table name. We specify all the columns of the table with datatype, default values on the remaining lines.
  3. Create a migration folder in workflow-java/src/main/resources/migration.
  4. Add this script to the migration folder. This is the default place where Corda tries to find the Liquibase scripts.
  5. Create a parent iou.changelog-master containing reference to iou.changelog-v1.xml as below. It’s a good practice to define a master changelog, which includes all the versions of a particular schema. To update the schema, create a new Liquibase script and include the script in this file.

Step 2: Define a custom schema

  1. To define a schema, extend the class via MappedSchema and define a JPA entity within this class. By default, if you do not override the migrationResource then a Hibernate entity gets created. To use Liquibase, override the migrationResource specifying the Liquibase script name.
  2. Extend the JPA entity via PersistentIOU, which has the stateRef pointer to the ledger state. This pointer joins the table to the ledger states table, which allows you to query the table using a vault query.
public class IOUSchemaV1 extends MappedSchema {
    public IOUSchemaV1() {
        super(IOUSchema.class, 1, ImmutableList.of(PersistentIOU.class));
    }

    @Nullable
    @Override
    public String getMigrationResource() {
        return "iou.changelog-master";
    }

    @Entity
    @Table(name = "iou_states")
    public static class PersistentIOU extends PersistentState {
        @Column(name = "lender") private final String lender;
        @Column(name = "borrower") private final String borrower;
        @Column(name = "value") private final int value;
        @Column(name = "linear_id") private final UUID linearId;


        public PersistentIOU(String lender, String borrower, int value, UUID linearId) {
            this.lender = lender;
            this.borrower = borrower;
            this.value = value;
            this.linearId = linearId;
        }

        // Default constructor required by hibernate.
        public PersistentIOU() {
            this.lender = null;
            this.borrower = null;
            this.value = 0;
            this.linearId = null;
        }

        public String getLender() {
            return lender;
        }

        public String getBorrower() {
            return borrower;
        }

        public int getValue() {
            return value;
        }

        public UUID getId() {
            return linearId;
        }
    }
}

Step 3: Define IOUState by extending via QueryableState

  1. Every contract state should implement QueryableState if they need to be inserted as a custom table. Doing so will add two new functions to the class that must be implemented. supportedSchemas which should list the supported schemas and generateMappedObject to provide a mapping of the ledger state to the custom state.
@Override public PersistentState generateMappedObject(MappedSchema schema) {
    if (schema instanceof IOUSchemaV1) {
        return new IOUSchemaV1.PersistentIOU(
                this.lender.getName().toString(),
                this.borrower.getName().toString(),
                this.value,
                this.linearId.getId());
    } else {
        throw new IllegalArgumentException("Unrecognised schema $schema");
    }
}
@Override public Iterable<MappedSchema> supportedSchemas() {
    return ImmutableList.of(new IOUSchemaV1());
}

Step 4 : Setup users, schema permissions, update node.conf and start the node as described briefly in previous blog.

Step 5: Stop the node. Update the schema by adding a new column. Create a new Liquibase script and update the master-changelog to include this.

  1. Stop the node, and then update IOUSchemaV1 by adding a new column.
public class IOUSchemaV1 extends MappedSchema {
    public IOUSchemaV1() {
        super(IOUSchema.class, 1, ImmutableList.of(PersistentIOU.class));
    }

    @Nullable
    @Override
    public String getMigrationResource() {
        return "iou.changelog-master";
    }

    @Entity
    @Table(name = "iou_states")
    public static class PersistentIOU extends PersistentState {
        @Column(name = "lender") private final String lender;
        @Column(name = "borrower") private final String borrower;
        @Column(name = "value") private final int value;
        @Column(name = "linear_id") private final UUID linearId;
        @Column(name = "constraint_type") private final Integer constraint_type;


        public PersistentIOU(String lender, String borrower, int value, UUID linearId, Integer constraint_type) {
            this.lender = lender;
            this.borrower = borrower;
            this.value = value;
            this.linearId = linearId;
            this.constraint_type = constraint_type;
        }

        // Default constructor required by hibernate.
        public PersistentIOU() {
            this.lender = null;
            this.borrower = null;
            this.value = 0;
            this.linearId = null;
            constraint_type = 0;
        }

        public String getLender() {
            return lender;
        }

        public String getBorrower() {
            return borrower;
        }

        public int getValue() {
            return value;
        }

        public UUID getId() {
            return linearId;
        }

        public Integer getConstraint_type() {
            return constraint_type;
        }
    }
}

2. Update IOUState by adding an extra column, updating mapping in generateMappedObject.

@BelongsToContract(IOUContract.class)
public class IOUState implements LinearState, QueryableState {
    private final Integer value;
    private final Party lender;
    private final Party borrower;
    private final UniqueIdentifier linearId;
    private final Integer constraint_type;

    /**
     * @param value the value of the IOU.
     * @param lender the party issuing the IOU.
     * @param borrower the party receiving and approving the IOU.
     */
    public IOUState(Integer value,
                    Party lender,
                    Party borrower,
                    UniqueIdentifier linearId,
                    Integer constraint_type)
    {
        this.value = value;
        this.lender = lender;
        this.borrower = borrower;
        this.linearId = linearId;
        this.constraint_type = constraint_type;
    }

    public Integer getValue() { return value; }
    public Party getLender() { return lender; }
    public Party getBorrower() { return borrower; }
    @Override public UniqueIdentifier getLinearId() { return linearId; }
    @Override public List<AbstractParty> getParticipants() {
        return Arrays.asList(lender, borrower);
    }

    @Override public PersistentState generateMappedObject(MappedSchema schema) {
        if (schema instanceof IOUSchemaV1) {
            return new IOUSchemaV1.PersistentIOU(
                    this.lender.getName().toString(),
                    this.borrower.getName().toString(),
                    this.value,
                    this.linearId.getId(),
                    this.constraint_type);
        } else {
            throw new IllegalArgumentException("Unrecognised schema $schema");
        }
    }

    @Override public Iterable<MappedSchema> supportedSchemas() {
        return ImmutableList.of(new IOUSchemaV1());
    }

    @Override
    public String toString() {
        return String.format("IOUState(value=%s, lender=%s, borrower=%s, linearId=%s)", value, lender, borrower, linearId);
    }
}

3. Create a new Liquibase script as below for this new column.

4. Add a reference to this script to iou-changelog-master.xml.

4. Run the gradle jar task.

5. Replace the old jar in the project with this new jar.

Step 6: Start the node.

When the node starts, Liquibase looks at the databasechangelog table. As of now, there is only an iou-changelog-v1.xml entry in the table. Hence it knows that only iou-changelog-v1.xml has been executed. Hence it now picks up the new script, which is iou-changelog-v2.xml, converts it to database-specific scripts, and executes it on the database. An entry of iou-changelog-v2.xml is added to the databasechangelog table.

To download the above used IOU application, you can clone below URL, switch to the database-migration-tutorial branch, and use database-migration-tutorial project.

git clone https://github.com/corda/samples.git

Thanks to Szymon and The Corda Team

Thanks for reading — Sneha Damle, Developer Evangelist (R3).


CorDapp Database Upgrade/ Migration-Development Perspective was originally published in Corda on Medium, where people are continuing the conversation by highlighting and responding to this story.

Share: