Corda 5 Developer Preview 2 is now available.

net.corda.v5.application.persistence

Corda 5 supports CRUD (Create, Read, Update, Delete) operations for user-defined types. This is achieved using JPA-annotated entities and, to manage database migrations, Liquibase.

Defining Custom Tables Using Liquibase Migrations

CorDapps store data in a relational database. When Corda creates a virtual node for a CorDapp (as part of a CPI), it requires associated tables, indexes, foreign-keys, etc. To create these, you must embed Liquibase files into the CorDapp CPK.

Liquibase manages DB changes in a “Change Log” which references one or more change sets. You must specify the top level databaseChangeLog in a resource file in the CPK called migration/db.changelog-master.xml. This file can reference one or more files including changeSet.

You should organise these change sets with future changes in mind. For example, we recommend a single include per version of the table. Once a changeSet is deployed, it cannot be changed and any change must be provided as a changeSet with a new id. We suggest adding a version in the id; for example, <table-name>-v1.

Example of src/resources/migration/db.changelog-master.xml:

<?xml version="1.1" 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-4.3.xsd">
    <include file="migration/dogs-migration-v1.0.xml"/>
</databaseChangeLog>

The referenced include file should also be a resource file in src/resources/migration and define the table itself. For example, src/resources/migration/dogs-migration-v1.0.xml:

<?xml version="1.1" 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-4.3.xsd">

    <changeSet author="R3.Corda" id="dogs-migrations-v1.0">
        <createTable tableName="dog">
            <column name="id" type="uuid">
                <constraints nullable="false"/>
            </column>
            <column name="name" type="VARCHAR(255)"/>
            <column name="birthdate" type="DATETIME"/>
            <column name="owner" type="VARCHAR(255)"/>
        </createTable>
        <addPrimaryKey columnNames="id" constraintName="dog_id" tableName="dog"/>
    </changeSet>
</databaseChangeLog>

Running the Migrations

To run the migrations:

  1. Upload the CPI.
  2. Create a virtual node using the Corda 5 REST API.

The migrations run when the virtual node is created and logging shows the migrations executing. If you have direct database access, you should see the tables being created. If you are using Postgres, make sure to look under the correct schema, since each virtual node creates a new schema, unless an external VNode database was provided during VNode creation.

Mapping Your New Tables to JPA Entities

CorDapps should use JPA annotated POJOs for data access objects. Each class requires @CordaSerializable and @Entity annotations. The following is an example that also defines some named JPQL queries:

@CordaSerializable
@Entity(name = "dog")
@NamedQueries(
    NamedQuery(name = "Dog.summon", query = "SELECT d FROM Dog d WHERE d.name = :name"),    
    NamedQuery(name = "Dog.independent", query = "SELECT d FROM Dog d WHERE d.owner IS NULL"),    
    NamedQuery(name = "Dog.summonLike", query = "SELECT d FROM Dog d WHERE d.name LIKE :name ORDER BY d.name"),    
    NamedQuery(name = "Dog.all", query = "SELECT d FROM Dog d ORDER BY d.name"),    
    NamedQuery(name = "Dog.release", query = "UPDATE Dog SET owner=null")
)
class Dog(
    @Id    
    @Column(name = "id", nullable = false)    
    val id: UUID,    
    @Column(name = "name")    
    val name: String,    
    @Column(name = "birthdate")    
    val birthdate: Instant,
    @Column(name = "owner")
    val owner: String?
) {
    constructor() : this(id = UUID.randomUUID(), name = "", birthdate = Instant.now(), owner = "")

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Dog

        if (id != other.id) return false
        if (name != other.name) return false
        if (birthdate != other.birthdate) return false
        if (owner != other.owner) return false

        return true
    }

    override fun hashCode(): Int {
        var result = id.hashCode()
        result = 31 * result + name.hashCode()
        result = 31 * result + birthdate.hashCode()
        result = 31 * result + owner.hashCode()
        return result
    }
}

Using the Persistence API From a CorDapp Flow

  1. Define a reference to the persistence service. This should be supplied via the Corda dependency injection system:
  import net.corda.v5.application.flows.CordaInject
  import net.corda.v5.application.flows.RPCStartableFlow
  import net.corda.v5.application.persistence.PersistenceService

  class MyExampleFlow : RPCStartableFlow {
      @CordaInject
      lateinit var persistenceService: PersistenceService

      // your code goes here
  1. To create a Dog entity that writes a row to the database, use the following code:
  val dog = Dog(dogId, "dog", Instant.now(), "none")
  persistenceService.persist(dog)
  1. To load a row from the database by ID, use the following code:
  val dog = persistenceService.find(Dog::class.java, dogId)
  return if (dog == null) {
      "no dog found"
  } else {
      "found dog id='${dog.id}' name='${dog.name}"
  }

Alternatively, to load all entities and create Dog instances for every record in the Dog table in the database, use the following code:

val dogs = persistenceService.findAll(Dog::class.java).execute()
  1. To update a record, use the merge operation. For example, to change the name of a Dog and set the owner to null:
val newDogName = input.getValue("name")
persistenceService.merge(Dog(dogId, newDogName, Instant.now(), "none"))

All of the operations available are defined in the public interface: PersistenceService.

Was this page helpful?

Thanks for your feedback!

Chat with us

Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.

Propose documentation improvements directly

Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.

We're sorry this page wasn't helpful. Let us know how we can make it better!

Chat with us

Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.

Create an issue

Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.

Propose documentation improvements directly

Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.