API: Persistence

Corda 为开发者提供了一种方式来将 contract state 的全部或部分暴露给一个 Object Relational Mapping(ORM) 工具来将其持久化到一个 RDBMS 中。这样做的目的是对 vault 中保存的 contract state 建立有效的索引,这样就能够在这些 states 上进行查询并且可以在 Corda 数据和拥有节点的组织机构自己本地的数据进行关联,以此来帮助 vault 开发。

ORM mapping 是通过使用 Java Persistence API(JPA) 作为 annotations 来指定的,当每次一个 state 作为 transaction 的一部分被记录到本地的 vault 中的时候,它会被节点自动地转换成数据库表中的记录。

注意:当前节点包含了一个 H2 数据库实例,但是任何支持 JDBC 的数据库都可以作为备选项并且在将来节点会支持更大范围的使用 JDBC drivers 实现的数据库。大多数节点内部的 state 也会被持久化。你可以通过 JDBC 来访问这个内部的 H2 数据库,请参考 “Node administration” 部分了解详细步骤。

Schemas

当一个 ContractState 希望被插入到节点的本地数据库并且可以通过 SQL 访问的话,它就可以实现 QueryableState 接口。

/**
 * A contract state that may be mapped to database schemas configured for this node to support querying for,
 * or filtering of, states.
 */
interface QueryableState : ContractState {
    /**
     * Enumerate the schemas this state can export representations of itself as.
     */
    fun supportedSchemas(): Iterable

    /**
     * Export a representation for the given schema.
     */
    fun generateMappedObject(schema: MappedSchema): PersistentState
}

QueryableState 接口要求 State 需要遍历它所支持的不同的关系型 schemas,比如in cases where the schema has evolved, with each one being represented by a MappedSchema object return by the supportedSchemas() method。一旦一个 schema 被选择了之后,当被 generateMappedObject() 方法请求的时候,它必须要生成对应的 representation,然后会被传入 ORM。

节点有一个内部的 SchemaService,该服务通过使用选择的 MappedSchema 来决定过了什么会被持久化,什么不会。

/**
 * A configuration and customisation point for Object Relational Mapping of contract state objects.
 */
interface SchemaService {
    /**
     * Represents any options configured on the node for a schema.
     */
    data class SchemaOptions(val databaseSchema: String? = null, val tablePrefix: String? = null)

    /**
     * Options configured for this node's schemas.  A missing entry for a schema implies all properties are null.
     */
    val schemaOptions: Map<MappedSchema, SchemaOptions>

    /**
     * Given a state, select schemas to map it to that are supported by [generateMappedObject] and that are configured
     * for this node.
     */
    fun selectSchemas(state: ContractState): Iterable

    /**
     * Map a state to a [PersistentState] for the given schema, either via direct support from the state
     * or via custom logic in this service.
     */
    fun generateMappedObject(state: ContractState, schema: MappedSchema): PersistentState
}
/**
 * A database schema that might be configured for this node.  As well as a name and version for identifying the schema,
 * also list the classes that may be used in the generated object graph in order to configure the ORM tool.
 *
 * @param schemaFamily A class to fully qualify the name of a schema family (i.e. excludes version)
 * @param version The version number of this instance within the family.
 * @param mappedTypes The JPA entity classes that the ORM layer needs to be configure with for this schema.
 */
open class MappedSchema(schemaFamily: Class<*>,
                        val version: Int,
                        val mappedTypes: Iterable<Class<*>>) {
    val name: String = schemaFamily.name
    override fun toString(): String = "${this.javaClass.simpleName}(name=$name, version=$version)"
}

SchemaService 可以由节点管理员进行配置,管理员可以选择节点使用哪些 schemas。通过这种方式,针对 ledger states 的有关系的 views 能够通过 lock-step 同内部系统或者其他的集成点而发展成更为可控的形式,并且 contract code 的每次更新变得并不重要(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)。它可以选择由 QueryableState 提供的 MappedSchema,然后会自动地更新为 schema 更新的版本,甚至提供一个非 QueryableState 提供的 MappedSchema

多个不同的 contract state 实现可能会提供跟一些常规 schema 的映射,这个是被期望出现的。例如一个 Interest Rate Swap contract 和一个 Equity OTC Option contract 可能都提供一个跟常见的 Derivative Schema 的映射。这个 schema 通常不应该是 contract 的一部分,并且应该是独立存在的,这样会鼓励针对于特定的业务领域或者 CorDapp 可以对常用部分进行重用。

MappedSchema 提供了一个 family name,该 family name 通过使用 Java package 样式的 name-spacing 来去除模糊, 这个 name-spacing 来自于一个在不同版本中永远都是统一的一个 schema family 类,这就允许了 SchemaService 可以选择一个喜欢的 schema 的版本。

SchemaService 同样也要负责提供一个 SchemaOptions,这对于一个指定的 MappedSchema 是可以配置的,这就允许了对于一个数据库的 schema 或者表名前缀进行配置,以此来避免同其他的 MappedSchema 的任何冲突。

注意:我们希望这里有一些对 SchemaService 的 plugin 的支持,来提供版本的更新并提供其他的 schemas 来作为 CorDapp 的一部分,并且这些 active schemas 是可配置的。但是当前的实现并没有提供这些,造成了 QueryableState 所支持的所有 schemas 的所有版本都是持久化的。这会在将来被改变。类似的,当前也不支持配置 SchemaOptions 但是将来是会支持的。

注册自定义的 schema

自定义的合约 schemas 会在 CorDapps 启动的时候被自动注册。节点的启动过程会扫描你的 CorDapp jar 里的 plugin configuration 路径下的 schemas(任何的扩展 MappedSchema 接口的类)。
为了测试的目的,像下边这样手动地注册自定义的 schemas 是有必要的:

  • 使用 MockNetworkMockNode 的测试必须要使用 MockNode 的 registerCustomSchemas()  方法明确注册自定义的 schemas
  • 使用 MockServices 的测试必须要使用 MockServices 的 customSchemas 属性来明确地注册 schemas

Object Relational Mapping

对于一个 QueryableState 的持久化的展示应该是一个 PersistentState 的实例,可以通过 state 本身或者 SchemaService 的 plugin 来构建。这就允许了 ORM 层永远会将一个 ContractState 的持久化表现和一个 StateRef 相关联,并且允许把 vault 中的不同的 unconsumed states 进行 joining。

PersistentState subclass 应该被定义为一个 JPA 2.1 Entity,这个 entity 应该有一个定义 table name 并且还应该有一些属性(Kotlin 中是属性,Java 中是 getters/setters)来映射成为合适的列和 SQL 类型。其他的 entities 可以被包含来形成一些复杂的属性,比如集合,所以这个映射可以不是平的(flat)。MappedSchema 必须要为这个 schema 提供所有 JPA entity classes 的列表,以此来初始化这个 ORM 层。

基础代码中提供了一些 entities 和映射的例子,包括 Cash.StateCommercialPaper.State。例如,下边是 cash schema 的第一个版本。

package net.corda.schemas

import net.corda.core.identity.AbstractParty
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Index
import javax.persistence.Table

/**
 * An object used to fully qualify the [CashSchema] family name (i.e. independent of version).
 */
object CashSchema

/**
 * First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood
 * at the time of writing.
 */
object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) {
    @Entity
    @Table(name = "contract_cash_states",
           indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"),
                             Index(name = "pennies_idx", columnList = "pennies")))
    class PersistentCashState(
            @Column(name = "owner_key")
            var owner: String,

            @Column(name = "pennies")
            var pennies: Long,

            @Column(name = "ccy_code", length = 3)
            var currency: String,

            @Column(name = "issuer_key")
            var issuerParty: String,

            @Column(name = "issuer_ref")
            var issuerRef: ByteArray
    ) : PersistentState()
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注