Vault query criteria default attribute handling. (#1322)

* Performance fix: prevent self joins on VaultStates table (was occurring when Sort specified).

* Enrichment and overriding of Common attributes (eg. Vault.StateStatus and Contract State Types) using composite query criteria.
Remove unnecessary QueryEditor implementation from NodeVaultService.

* Updated documentation and changelog.

* Misc fixes to broken documentation code snippets.

* Incorporating changes from PR review feedback.
This commit is contained in:
josecoll
2017-08-25 08:38:12 +01:00
committed by GitHub
parent 1750ab07af
commit a3dbbc173b
8 changed files with 129 additions and 144 deletions

View File

@ -13,6 +13,9 @@ import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.toHexString
import net.corda.core.utilities.trace
import org.hibernate.query.criteria.internal.expression.LiteralExpression
import org.hibernate.query.criteria.internal.predicate.ComparisonPredicate
import org.hibernate.query.criteria.internal.predicate.InPredicate
import java.util.*
import javax.persistence.Tuple
import javax.persistence.criteria.*
@ -30,8 +33,9 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
// incrementally build list of join predicates
private val joinPredicates = mutableListOf<Predicate>()
// incrementally build list of root entities (for later use in Sort parsing)
private val rootEntities = mutableMapOf<Class<out PersistentState>, Root<*>>()
private val rootEntities = mutableMapOf<Class<out PersistentState>, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates))
private val aggregateExpressions = mutableListOf<Expression<*>>()
private val commonPredicates = mutableMapOf<Pair<String,Operator>, Predicate>() // schema attribute Name, operator -> predicate
var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED
@ -39,11 +43,6 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
log.trace { "Parsing VaultQueryCriteria: $criteria" }
val predicateSet = mutableSetOf<Predicate>()
// contract State Types
val contractTypes = deriveContractTypes(criteria.contractStateTypes)
if (contractTypes.isNotEmpty())
predicateSet.add(criteriaBuilder.and(vaultStates.get<String>("contractStateClassName").`in`(contractTypes)))
// soft locking
criteria.softLockingCondition?.let {
val softLocking = criteria.softLockingCondition
@ -91,12 +90,14 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
return predicateSet
}
private fun deriveContractTypes(contractStateTypes: Set<Class<out ContractState>>? = null): List<String> {
private fun deriveContractTypes(contractStateTypes: Set<Class<out ContractState>>? = null): Set<String> {
log.trace { "Contract types to be derived: primary ($contractType), additional ($contractStateTypes)" }
val combinedContractStateTypes = contractStateTypes?.plus(contractType) ?: setOf(contractType)
combinedContractStateTypes.filter { it.name != ContractState::class.java.name }.let {
val interfaces = it.flatMap { contractTypeMappings[it.name] ?: listOf(it.name) }
val interfaces = it.flatMap { contractTypeMappings[it.name] ?: setOf(it.name) }
val concrete = it.filter { !it.isInterface }.map { it.name }
return interfaces.plus(concrete)
log.trace { "Derived contract types: ${interfaces.union(concrete)}" }
return interfaces.union(concrete)
}
}
@ -233,11 +234,6 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultFungibleStates.get<PersistentStateRef>("stateRef"))
predicateSet.add(joinPredicate)
// contract State Types
val contractTypes = deriveContractTypes()
if (contractTypes.isNotEmpty())
predicateSet.add(criteriaBuilder.and(vaultStates.get<String>("contractStateClassName").`in`(contractTypes)))
// owner
criteria.owner?.let {
val owners = criteria.owner as List<AbstractParty>
@ -282,11 +278,6 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), vaultLinearStates.get<PersistentStateRef>("stateRef"))
joinPredicates.add(joinPredicate)
// contract State Types
val contractTypes = deriveContractTypes()
if (contractTypes.isNotEmpty())
predicateSet.add(criteriaBuilder.and(vaultStates.get<String>("contractStateClassName").`in`(contractTypes)))
// linear ids UUID
criteria.uuid?.let {
val uuids = criteria.uuid as List<UUID>
@ -322,11 +313,6 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), entityRoot.get<PersistentStateRef>("stateRef"))
joinPredicates.add(joinPredicate)
// contract State Types
val contractTypes = deriveContractTypes()
if (contractTypes.isNotEmpty())
predicateSet.add(criteriaBuilder.and(vaultStates.get<String>("contractStateClassName").`in`(contractTypes)))
// resolve general criteria expressions
parseExpression(entityRoot, criteria.expression, predicateSet)
}
@ -379,11 +365,11 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
val selections =
if (aggregateExpressions.isEmpty())
listOf(vaultStates).plus(rootEntities.map { it.value })
rootEntities.map { it.value }
else
aggregateExpressions
criteriaQuery.multiselect(selections)
val combinedPredicates = joinPredicates.plus(predicateSet)
val combinedPredicates = joinPredicates.plus(predicateSet).plus(commonPredicates.values)
criteriaQuery.where(*combinedPredicates.toTypedArray())
return predicateSet
@ -391,14 +377,39 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
override fun parseCriteria(criteria: CommonQueryCriteria): Collection<Predicate> {
log.trace { "Parsing CommonQueryCriteria: $criteria" }
val predicateSet = mutableSetOf<Predicate>()
// state status
stateTypes = criteria.status
if (criteria.status != Vault.StateStatus.ALL)
predicateSet.add(criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>("stateStatus"), criteria.status))
if (criteria.status != Vault.StateStatus.ALL) {
val predicateID = Pair(VaultSchemaV1.VaultStates::stateStatus.name, EqualityComparisonOperator.EQUAL)
if (commonPredicates.containsKey(predicateID)) {
val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal
if (existingStatus != criteria.status) {
log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::stateStatus.name}] value $existingStatus with ${criteria.status}")
commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), criteria.status))
}
}
else {
commonPredicates.put(predicateID, criteriaBuilder.equal(vaultStates.get<Vault.StateStatus>(VaultSchemaV1.VaultStates::stateStatus.name), criteria.status))
}
}
return predicateSet
// contract state types
val contractTypes = deriveContractTypes(criteria.contractStateTypes)
if (contractTypes.isNotEmpty()) {
val predicateID = Pair(VaultSchemaV1.VaultStates::contractStateClassName.name, CollectionOperator.IN)
if (commonPredicates.containsKey(predicateID)) {
val existingTypes = (commonPredicates[predicateID]!!.expressions[0] as InPredicate<*>).values.map { (it as LiteralExpression).literal }.toSet()
if (existingTypes != contractTypes) {
log.warn("Enriching previous attribute [${VaultSchemaV1.VaultStates::contractStateClassName.name}] values [$existingTypes] with [$contractTypes]")
commonPredicates.replace(predicateID, criteriaBuilder.and(vaultStates.get<String>(VaultSchemaV1.VaultStates::contractStateClassName.name).`in`(contractTypes.plus(existingTypes))))
}
} else {
commonPredicates.put(predicateID, criteriaBuilder.and(vaultStates.get<String>(VaultSchemaV1.VaultStates::contractStateClassName.name).`in`(contractTypes)))
}
}
return emptySet()
}
private fun parse(sorting: Sort) {

View File

@ -336,78 +336,6 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
}
}
// TODO We shouldn't need to rewrite the query if we could modify the defaults.
private class QueryEditor<out T : ContractState>(val services: ServiceHub,
val lockId: UUID,
val contractType: Class<out T>) : IQueryCriteriaParser {
var alreadyHasVaultQuery: Boolean = false
var modifiedCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(contractStateTypes = setOf(contractType),
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
status = Vault.StateStatus.UNCONSUMED)
override fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun <L : PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria.copy(contractStateTypes = setOf(contractType),
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
status = Vault.StateStatus.UNCONSUMED)
alreadyHasVaultQuery = true
return emptyList()
}
override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
parse(left)
val modifiedLeft = modifiedCriteria
parse(right)
val modifiedRight = modifiedCriteria
modifiedCriteria = modifiedLeft.or(modifiedRight)
return emptyList()
}
override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
parse(left)
val modifiedLeft = modifiedCriteria
parse(right)
val modifiedRight = modifiedCriteria
modifiedCriteria = modifiedLeft.and(modifiedRight)
return emptyList()
}
override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection<Predicate> {
val basicQuery = modifiedCriteria
criteria.visit(this)
modifiedCriteria = if (alreadyHasVaultQuery) modifiedCriteria else criteria.and(basicQuery)
return emptyList()
}
fun queryForEligibleStates(criteria: QueryCriteria): Vault.Page<T> {
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
parse(criteria, sorter)
return services.vaultQueryService.queryBy(contractType, modifiedCriteria, sorter)
}
}
@Suspendable
@Throws(StatesNotAvailableException::class)
override fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
@ -418,9 +346,13 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
return emptyList()
}
// TODO This helper code re-writes the query to alter the defaults on things such as soft locks
// and then runs the query. Ideally we would not need to do this.
val results = QueryEditor(services, lockId, contractType).queryForEligibleStates(eligibleStatesQuery)
// Enrich QueryCriteria with additional default attributes (such as soft locks)
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
val enrichedCriteria = QueryCriteria.VaultQueryCriteria(
contractStateTypes = setOf(contractType),
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)))
val results = services.vaultQueryService.queryBy(contractType, enrichedCriteria.and(eligibleStatesQuery), sorter)
var claimedAmount = 0L
val claimedStates = mutableListOf<StateAndRef<T>>()

View File

@ -192,8 +192,8 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
QueryCriteria vaultCriteria = new VaultQueryCriteria(status, contractStateTypes);
List<UUID> linearIds = Collections.singletonList(ids.getSecond().getId());
QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds);
QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds);
QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds, null, status);
QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds, status);
QueryCriteria compositeCriteria1 = dealCriteriaAll.or(linearCriteriaAll);
QueryCriteria compositeCriteria2 = vaultCriteria.and(compositeCriteria1);

View File

@ -11,7 +11,6 @@ import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.*
import net.corda.core.utilities.*
import net.corda.finance.*
import net.corda.node.services.schema.NodeSchemaService
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.contracts.Commodity
import net.corda.finance.contracts.DealState
@ -1338,8 +1337,8 @@ class VaultQueryTests : TestDependencyInjectionBase() {
val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID), Sort.Direction.DESC)))
val results = vaultQuerySvc.queryBy<LinearState>(compositeCriteria, sorting = sorting)
assertThat(results.statesMetadata).hasSize(13)
assertThat(results.states).hasSize(13)
assertThat(results.statesMetadata).hasSize(4)
assertThat(results.states).hasSize(4)
}
}
@ -1860,13 +1859,13 @@ class VaultQueryTests : TestDependencyInjectionBase() {
services.fillWithSomeTestLinearStates(1, "TEST1")
val aState = services.fillWithSomeTestLinearStates(1, "TEST2").states
services.consumeLinearStates(aState.toList(), DUMMY_NOTARY)
services.fillWithSomeTestLinearStates(1, "TEST3").states.first().state.data.linearId.id
services.fillWithSomeTestLinearStates(1, "TEST1").states.first().state.data.linearId.id
// 2 unconsumed states with same external ID, 1 with different external ID
// 2 unconsumed states with same external ID, 1 consumed with different external ID
}
database.transaction {
val results = builder {
val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2")
val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST1")
val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition)
val uuidCondition = VaultSchemaV1.VaultLinearStates::uuid.equal(uuid)
@ -1875,8 +1874,8 @@ class VaultQueryTests : TestDependencyInjectionBase() {
val criteria = externalIdCustomCriteria or uuidCustomCriteria
vaultQuerySvc.queryBy<LinearState>(criteria)
}
assertThat(results.statesMetadata).hasSize(3)
assertThat(results.states).hasSize(3)
assertThat(results.statesMetadata).hasSize(2)
assertThat(results.states).hasSize(2)
}
}
@ -1957,6 +1956,34 @@ class VaultQueryTests : TestDependencyInjectionBase() {
}
}
@Test
fun `enriched and overridden composite query handles defaults correctly`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L))
services.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!), notaryServices)
services.fillWithSomeTestLinearStates(1, "ABC")
services.fillWithSomeTestDeals(listOf("123"))
}
database.transaction {
// Base criteria
val baseCriteria = VaultQueryCriteria(notary = listOf(DUMMY_NOTARY),
status = Vault.StateStatus.CONSUMED)
// Enrich and override QueryCriteria with additional default attributes (such as soft locks)
val enrichedCriteria = VaultQueryCriteria(contractStateTypes = setOf(DealState::class.java), // enrich
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(UUID.randomUUID())),
status = Vault.StateStatus.UNCONSUMED) // override
// Sorting
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
// Execute query
val results = services.vaultQueryService.queryBy<FungibleAsset<*>>(baseCriteria and enrichedCriteria, sorter).states
assertThat(results).hasSize(4)
}
}
/**
* Dynamic trackBy() tests
*/
@ -1965,7 +1992,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun trackCashStates_unconsumed() {
val updates =
database.transaction {
// DOCSTART VaultQueryExample15
vaultQuerySvc.trackBy<Cash.State>().updates // UNCONSUMED default
// DOCEND VaultQueryExample15
}
val (linearStates,dealStates) =
database.transaction {