Persistence API - Updates (#4303)

* Updating with latest changes to persistence documentation

* Minor updates to api-persistence, submitting initial pull request.

* Updated single '-' to ensure proper formatting

* Minor spelling + grammar updates for final commit before pull request.

* Initial updates based on Joel's feedback on Git.

* Committing with latest changes request on pull request.

Update still required for how to customize schema service behaviour.

* Removed passage describing unimplemented features of schema service

* Added inline commenting to example code for readability.

* Additional spelling + grammar updates.
This commit is contained in:
Nick Rogers 2019-01-03 04:51:04 -05:00 committed by Joel Dudley
parent 0f214fd46e
commit 4ac701e648

View File

@ -9,24 +9,26 @@ API: Persistence
.. contents::
Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping*
(ORM) tool to be persisted in a RDBMS. The purpose of this is to assist *vault* development by effectively indexing
persisted contract states held in the vault for the purpose of running queries over them and to allow relational joins
between Corda data and private data local to the organisation owning a node.
Corda offers developers the option to expose all or some parts of a contract state to an *Object Relational Mapping*
(ORM) tool to be persisted in a *Relational Database Management System* (RDBMS).
The ORM mapping is specified using the `Java Persistence API <https://en.wikipedia.org/wiki/Java_Persistence_API>`_
(JPA) as annotations and is converted to database table rows by the node automatically every time a state is recorded
in the node's local vault as part of a transaction.
The purpose of this, is to assist `vault <https://docs.corda.net/vault.html>`_
development and allow for the persistence of state data to a custom database table. Persisted states held in the
vault are indexed for the purposes of executing queries. This also allows for relational joins between Corda tables
and the organization's existing data.
.. note:: Presently the node includes an instance of the H2 database but any database that supports JDBC is a
candidate and the node will in the future support a range of database implementations via their JDBC drivers. Much
of the node internal state is also persisted there. You can access the internal H2 database via JDBC, please see the
info in ":doc:`node-administration`" for details.
The Object Relational Mapping is specified using `Java Persistence API <https://en.wikipedia.org/wiki/Java_Persistence_API>`_
(JPA) annotations. This mapping is persisted to the database as a table row (a single, implicitly structured data item) by the node
automatically every time a state is recorded in the node's local vault as part of a transaction.
.. note:: By default, nodes use an H2 database which is accessed using *Java Database Connectivity* JDBC. Any database
with a JDBC driver is a candidate and several integrations have been contributed to by the community.
Please see the info in ":doc:`node-database`" for details.
Schemas
-------
Every ``ContractState`` can implement the ``QueryableState`` interface if it wishes to be inserted into the node's local
database and accessible using SQL.
Every ``ContractState`` may implement the ``QueryableState`` interface if it wishes to be inserted into a custom table in the node's
database and made accessible using SQL.
.. literalinclude:: ../../core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt
:language: kotlin
@ -34,12 +36,12 @@ database and accessible using SQL.
:end-before: DOCEND QueryableState
The ``QueryableState`` interface requires the state to enumerate the different relational schemas it supports, for
instance in cases where the schema has evolved, with each one being represented by a ``MappedSchema`` object return
by the ``supportedSchemas()`` method. Once a schema is selected it must generate that representation when requested
via the ``generateMappedObject()`` method which is then passed to the ORM.
instance in situations where the schema has evolved. Each relational schema is represented as a ``MappedSchema``
object returned by the state's ``supportedSchemas`` method.
Nodes have an internal ``SchemaService`` which decides what to persist and what not by selecting the ``MappedSchema``
to use.
Nodes have an internal ``SchemaService`` which decides what data to persist by selecting the ``MappedSchema`` to use.
Once a ``MappedSchema`` is selected, the ``SchemaService`` will delegate to the ``QueryableState`` to generate a corresponding
representation (mapped object) via the ``generateMappedObject`` method, the output of which is then passed to the *ORM*.
.. literalinclude:: ../../node/src/main/kotlin/net/corda/node/services/api/SchemaService.kt
:language: kotlin
@ -51,13 +53,10 @@ to use.
:start-after: DOCSTART MappedSchema
:end-before: DOCEND MappedSchema
The ``SchemaService`` can be configured by a node administrator to select the schemas used by each app. In this way the
relational view of ledger states can evolve in a controlled fashion in lock-step with internal systems or other
integration points and not necessarily with every upgrade to the contract code. It can select from the
``MappedSchema`` offered by a ``QueryableState``, automatically upgrade to a later version of a schema or even
provide a ``MappedSchema`` not originally offered by the ``QueryableState``.
With this framework, the relational view of ledger states can evolve in a controlled fashion in lock-step with internal systems or other
integration points and is not dependant on changes to the contract code.
It is expected that multiple different contract state implementations might provide mappings within a single schema.
It is expected that multiple contract state implementations might provide mappings within a single schema.
For example an Interest Rate Swap contract and an Equity OTC Option contract might both provide a mapping to
a Derivative contract within the same schema. The schemas should typically not be part of the contract itself and should exist independently
to encourage re-use of a common set within a particular business area or Cordapp.
@ -71,19 +70,19 @@ class name of a *schema family* class that is constant across versions, allowing
preferred version of a schema.
The ``SchemaService`` is also responsible for the ``SchemaOptions`` that can be configured for a particular
``MappedSchema`` which allow the configuration of a database schema or table name prefixes to avoid any clash with
``MappedSchema``. These allow the configuration of database schemas or table name prefixes to avoid clashes with
other ``MappedSchema``.
.. note:: It is intended that there should be plugin support for the ``SchemaService`` to offer the version upgrading
and additional schemas as part of Cordapps, and that the active schemas be configurable. However the present
implementation offers none of this and simply results in all versions of all schemas supported by a
``QueryableState`` being persisted. This will change in due course. Similarly, it does not currently support
.. note:: It is intended that there should be plugin support for the ``SchemaService`` to offer version upgrading, implementation
of additional schemas, and enable active schemas as being configurable. The present implementation does not include these features
and simply results in all versions of all schemas supported by a ``QueryableState`` being persisted.
This will change in due course. Similarly, the service does not currently support
configuring ``SchemaOptions`` but will do so in the future.
Custom schema registration
--------------------------
Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan
for schemas (any class that extends the ``MappedSchema`` interface) in the `plugins` configuration directory in your CorDapp jar.
Custom contract schemas are automatically registered at startup time for CorDapps. The node bootstrap process will scan for states that implement
the Queryable state interface. Tables are then created as specified by the ``MappedSchema``s identified by each states ``supportedSchemas`` method.
For testing purposes it is necessary to manually register the packages containing custom schemas as follows:
@ -94,16 +93,16 @@ For testing purposes it is necessary to manually register the packages containin
Object relational mapping
-------------------------
The persisted representation of a ``QueryableState`` should be an instance of a ``PersistentState`` subclass,
To facilitate the ORM, the persisted representation of a ``QueryableState`` should be an instance of a ``PersistentState`` subclass,
constructed either by the state itself or a plugin to the ``SchemaService``. This allows the ORM layer to always
associate a ``StateRef`` with a persisted representation of a ``ContractState`` and allows joining with the set of
unconsumed states in the vault.
The ``PersistentState`` subclass should be marked up as a JPA 2.1 *Entity* with a defined table name and having
properties (in Kotlin, getters/setters in Java) annotated to map to the appropriate columns and SQL types. Additional
entities can be included to model these properties where they are more complex, for example collections, so the mapping
does not have to be *flat*. The ``MappedSchema`` must provide a list of all of the JPA entity classes for that schema
in order to initialise the ORM layer.
entities can be included to model these properties where they are more complex, for example collections (:ref:`Persisting Hierarchical Data<persisting-hierarchical-data>`), so
the mapping does not have to be *flat*. The ``MappedSchema`` constructor accepts a list of all JPA entity classes for that schema in
the ``MappedTypes`` parameter. It must provide this list in order to initialise the ORM layer.
Several examples of entities and mappings are provided in the codebase, including ``Cash.State`` and
``CommercialPaper.State``. For example, here's the first version of the cash schema.
@ -116,6 +115,218 @@ Several examples of entities and mappings are provided in the codebase, includin
Ensure that table and column names are compatible with the naming convention of the database vendors for which the Cordapp will be deployed,
e.g. for Oracle database, prior to version 12.2 the maximum length of table/column name is 30 bytes (the exact number of characters depends on the database encoding).
Persisting Hierarchical Data
----------------------------
You may wish to persist hierarchical relationships within states using multiple database tables
You may wish to persist hierarchical relationships within state data using multiple database tables. In order to facillitate this, multiple ``PersistentState``
subclasses may be implemented. The relationship between these classes is defined using JPA annotations. It is important to note that the ``MappedSchema``
constructor requires a list of *all* of these subclasses.
An example Schema implementing hierarchical relationships with JPA annotations has been implemented below. This Schema will cause ``parent_data`` and ``child_data` tables to be
created.
.. container:: codeset
.. sourcecode:: java
@CordaSerializable
public class SchemaV1 extends MappedSchema {
/**
* This class must extend the MappedSchema class. Its name is based on the SchemaFamily name and the associated version number abbreviation (V1, V2... Vn).
* In the constructor, use the super keyword to call the constructor of MappedSchema with the following arguments: a class literal representing the schema family,
* a version number and a collection of mappedTypes (class literals) which represent JPA entity classes that the ORM layer needs to be configured with for this schema.
*/
public SchemaV1() {
super(Schema.class, 1, ImmutableList.of(PersistentParentToken.class, PersistentChildToken.class));
}
/**
* The @entity annotation signifies that the specified POJO class' non-transient fields should be persisted to a relational database using the services
* of an entity manager. The @table annotation specifies properties of the table that will be created to contain the persisted data, in this case we have
* specified a name argument which will be used the table's title.
*/
@Entity
@Table(name = "parent_data")
public static class PersistentParentToken extends PersistentState {
/**
* The @Column annotations specify the columns that will comprise the inserted table and specify the shape of the fields and associated
* data types of each database entry.
*/
@Column(name = "owner") private final String owner;
@Column(name = "issuer") private final String issuer;
@Column(name = "amount") private final int amount;
@Column(name = "linear_id") public final UUID linearId;
/**
* The @OneToMany annotation specifies a one-to-many relationship between this class and a collection included as a field.
* The @JoinColumn and @JoinColumns annotations specify on which columns these tables will be joined on.
*/
@OneToMany(cascade = CascadeType.PERSIST)
@JoinColumns({
@JoinColumn(name = "output_index", referencedColumnName = "output_index"),
@JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"),
})
private final List<PersistentChildToken> listOfPersistentChildTokens;
public PersistentParentToken(String owner, String issuer, int amount, UUID linearId, List<PersistentChildToken> listOfPersistentChildTokens) {
this.owner = owner;
this.issuer = issuer;
this.amount = amount;
this.linearId = linearId;
this.listOfPersistentChildTokens = listOfPersistentChildTokens;
}
// Default constructor required by hibernate.
public PersistentParentToken() {
this.owner = "";
this.issuer = "";
this.amount = 0;
this.linearId = UUID.randomUUID();
this.listOfPersistentChildTokens = null;
}
public String getOwner() {
return owner;
}
public String getIssuer() {
return issuer;
}
public int getAmount() {
return amount;
}
public UUID getLinearId() {
return linearId;
}
public List<PersistentChildToken> getChildTokens() { return listOfPersistentChildTokens; }
}
@Entity
@CordaSerializable
@Table(name = "child_data")
public static class PersistentChildToken {
// The @Id annotation marks this field as the primary key of the persisted entity.
@Id
private final UUID Id;
@Column(name = "owner")
private final String owner;
@Column(name = "issuer")
private final String issuer;
@Column(name = "amount")
private final int amount;
/**
* The @ManyToOne annotation specifies that this class will be present as a member of a collection on a parent class and that it should
* be persisted with the joining columns specified in the parent class. It is important to note the targetEntity parameter which should correspond
* to a class literal of the parent class.
*/
@ManyToOne(targetEntity = PersistentParentToken.class)
private final TokenState persistentParentToken;
public PersistentChildToken(String owner, String issuer, int amount) {
this.Id = UUID.randomUUID();
this.owner = owner;
this.issuer = issuer;
this.amount = amount;
this.persistentParentToken = null;
}
// Default constructor required by hibernate.
public PersistentChildToken() {
this.Id = UUID.randomUUID();
this.owner = "";
this.issuer = "";
this.amount = 0;
this.persistentParentToken = null;
}
public UUID getId() {
return Id;
}
public String getOwner() {
return owner;
}
public String getIssuer() {
return issuer;
}
public int getAmount() {
return amount;
}
public TokenState getPersistentToken() {
return persistentToken;
}
}
}
.. sourcecode:: kotlin
@CordaSerializable
object SchemaV1 : MappedSchema(schemaFamily = Schema::class.java, version = 1, mappedTypes = listOf(PersistentParentToken::class.java, PersistentChildToken::class.java)) {
@Entity
@Table(name = "parent_data")
class PersistentParentToken(
@Column(name = "owner")
var owner: String,
@Column(name = "issuer")
var issuer: String,
@Column(name = "amount")
var currency: Int,
@Column(name = "linear_id")
var linear_id: UUID,
@JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index"))
var listOfPersistentChildTokens: MutableList<PersistentChildToken>
) : PersistentState()
@Entity
@CordaSerializable
@Table(name = "child_data")
class PersistentChildToken(
@Id
var Id: UUID = UUID.randomUUID(),
@Column(name = "owner")
var owner: String,
@Column(name = "issuer")
var issuer: String,
@Column(name = "amount")
var currency: Int,
@Column(name = "linear_id")
var linear_id: UUID,
@ManyToOne(targetEntity = PersistentParentToken::class)
var persistentParentToken: TokenState
) : PersistentState()
Identity mapping
----------------
Schema entity attributes defined by identity types (``AbstractParty``, ``Party``, ``AnonymousParty``) are automatically
@ -161,8 +372,8 @@ In addition to ``jdbcSession``, ``ServiceHub`` also exposes the Java Persistence
method. This method can be used to persist and query entities which inherit from ``MappedSchema``. This is particularly
useful if off-ledger data must be maintained in conjunction with on-ledger state data.
.. note:: Your entity must be included as a mappedType in as part of a MappedSchema for it to be added to Hibernate
as a custom schema. See Samples below.
.. note:: Your entity must be included as a mappedType as part of a ``MappedSchema`` for it to be added to Hibernate
as a custom schema. If it's not included as a mappedType, a corresponding table will not be created. See Samples below.
The code snippet below defines a ``PersistentFoo`` type inside ``FooSchemaV1``. Note that ``PersistentFoo`` is added to
a list of mapped types which is passed to ``MappedSchema``. This is exactly how state schemas are defined, except that
@ -201,7 +412,7 @@ the entity in this case should not subclass ``PersistentState`` (as it is not a
class PersistentFoo(@Id @Column(name = "foo_id") var fooId: String, @Column(name = "foo_data") var fooData: String) : Serializable
}
Instances of ``PersistentFoo`` can be persisted inside a flow as follows:
Instances of ``PersistentFoo`` can be manually persisted inside a flow as follows:
.. container:: codeset