CorDapp Database Upgrade/ Migration-Production perspective

January 08, 2020
Database admin manually running DB scripts generated by database-management-tool

We have seen how to set up and connect production Corda node to a PostgreSQL database in the previous blog. We also saw how to perform a CorDapp database upgrade in the previous post. In this post, let’s see how we can upgrade CorDapp tables by using Liquibase scripts in production mode.

Let’s make use of the database management tool to generate the migration scripts, and the database admin executes them onto the database.

Broadly there are 6 steps required for the CorDapp database upgrade.

  1. Define the Liquibase script.
  2. Define the custom Schema.
  3. Define IOUState by extending with QueryableState and override IOUState’s supportedSchemas, generateMappedSchema, migrationResource.
  4. Setup users, schema permissions, update node.conf, start the node as described briefly in the previous blog.
  5. Database schema creation with Corda Database Management Tool, Start the node.
  6. Stop the node. Create a new Liquibase script with the new column, and update master-changelogto include this.
  7. Similar to Step 5.

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 the 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 location 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 add the script in this file.

Step 2: Define custom Schema

  1. To define a schema, extend the class via MappedSchema, 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. We extend the JPA entity via PersistentIOU, which has a stateRef pointer to the ledger state. This pointer helps to join ledger states and this table and query depending on the requirements.
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, start the node as described briefly in Step 1 in previous blog.

Step 5: Database schema creation with Corda Database Management Tool as described in Step 2 in previous blog.

Step 2.3 from the above step tells you to generate database scripts by running the dry-run command of database-management-tool. This script will now also include creating the specified IOU table changeset.

Once the scripts have been applied to the database by the database admin, start the node. If all the changesets are applied, the node should start successfully.

Once the scripts have been applied to the database by the database admin, start the node. If all the changesets have been applied the node should start successfully.

Step 6: 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. Update IOUSchemaV1by 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 new column and correct the 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 project database-migration-tutorial with this new jar.

Step 7: Similar to Step 5

Use the database-management-tool to generate the new schemas and manually apply them onto the database.

Step 8: Start the node

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-Production perspective was originally published in Corda on Medium, where people are continuing the conversation by highlighting and responding to this story.

Share: