mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
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:
parent
0f214fd46e
commit
4ac701e648
@ -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,
|
||||
constructed either by the state itself or a plugin to the ``SchemaService``. This allows the ORM layer to always
|
||||
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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user