Corda API:持久化

原文地址:https://docs.corda.net/api-persistence.html

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 也会被持久化。

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<MappedSchema>

    /**
     * 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<MappedSchema>

    /**
     * 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 的 cordappPackages 参数来注册包(packages)
  • 使用 MockServices 的测试必须要显式地使用 MockServices makeTestDatabaseAndMockServices() helper 方法的 cordappPackages 参数注册包(packages)

注意:使用 DriverDSL 的测试,如果他们在相同的项目结构下的话,当 driver 在调用的时候,他们会被自动注册你的自定义的 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.finance.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 net.corda.core.utilities.MAX_HASH_HEX_SIZE
import net.corda.core.contracts.MAX_ISSUER_REF_SIZE
import org.hibernate.annotations.Type
import javax.persistence.*

/**
 * 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.
 */
@CordaSerializable
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(
            /** X500Name of owner party **/
            @Column(name = "owner_name")
            var owner: AbstractParty,

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

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

            @Column(name = "issuer_key_hash", length = MAX_HASH_HEX_SIZE)
            var issuerPartyHash: String,

            @Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE)
            @Type(type = "corda-wrapper-binary")
            var issuerRef: ByteArray
    ) : PersistentState()
}

Identity mapping

由 identity 类型定义的 schema entity 属性(AbstractPartyPartyAnonymousParty)会被自动处理来确保当一个 identity 是 well known 的时候,只有 X500Name 的 identity 会被持久化,否则一个 null 值会被存储到相关的 column 中。为了保持隐私性,identity keys 从来不会被持久化。开发者应该使用 IdentityService 来从 well know X500 identity names 中找到 keys。

JDBC session

Apps 也有可能会使用一个像 Java SQL Connection API 中描述的那种标准的 JDBC 连接(session)来直接地跟节点底层的数据库进行交互。

使用 ServiceHub jdbcSession 方法想下边这样获得一个 JDBC 连接:

JDBC session 可以在 Flows 和 Service Plugins 中被使用。

下边的例子展示了使用一个 jdbcSession 来创建一个自定义的 corda service:

object CustomVaultQuery {

    @CordaService
    class Service(val services: AppServiceHub) : SingletonSerializeAsToken() {
        private companion object {
            private val log = contextLogger()
        }

        fun rebalanceCurrencyReserves(): List<Amount<Currency>> {
            val nativeQuery = """
                select
                    cashschema.ccy_code,
                    sum(cashschema.pennies)
                from
                    vault_states vaultschema
                join
                    contract_cash_states cashschema
                where
                    vaultschema.output_index=cashschema.output_index
                    and vaultschema.transaction_id=cashschema.transaction_id
                    and vaultschema.state_status=0
                group by
                    cashschema.ccy_code
                order by
                    sum(cashschema.pennies) desc
            """
            log.info("SQL to execute: $nativeQuery")
            val session = services.jdbcSession()
            return session.prepareStatement(nativeQuery).use { prepStatement ->
                prepStatement.executeQuery().use { rs ->
                    val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
                    while (rs.next()) {
                        val currencyStr = rs.getString(1)
                        val amount = rs.getLong(2)
                        log.info("$currencyStr : $amount")
                        topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
                    }
                    topUpLimits
                }
            }
        }
    }
}

它会像下边这样在一个自定义的 flow 里被引用:

        @Suspendable
        @Throws(CashException::class)
        override fun call(): List<SignedTransaction> {
            progressTracker.currentStep = AWAITING_REQUEST
            val topupRequest = otherPartySession.receive<TopupRequest>().unwrap {
                it
            }

            val customVaultQueryService = serviceHub.cordaService(CustomVaultQuery.Service::class.java)
            val reserveLimits = customVaultQueryService.rebalanceCurrencyReserves()

            val txns: List<SignedTransaction> = reserveLimits.map { amount ->
                // request asset issue
                logger.info("Requesting currency issue $amount")
                val txn = issueCashTo(amount, topupRequest.issueToParty, topupRequest.issuerPartyRef)
                progressTracker.currentStep = SENDING_TOP_UP_ISSUE_REQUEST
                return@map txn.stx
            }

            otherPartySession.send(txns)
            return txns
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值