Vault-Named Queries
Vault-named queries enable you to query the Corda database. The user can define the following:
- The name of the query
- The query functionality (the type that the query works on and the
WHERE
clause) - Optional filtering logic of the result set
- Optional transformation logic of the result set
- Optional collection logic of the result set
The query creator must follow pre-defined steps so that the query is registered and usable.
This section contains the following:
- Representing a State in the Database
- Creating and Registering a Vault-Named Query
- Vault-Named Query Operators
Representing a State in the Database
Each state
An immutable object representing a fact known by one or more participants at a specific point in time. You can use states to represent any type of data, and any kind of fact.
type can be represented as a pre-defined JSON string (custom_representation
column in the database). Use this JSON representation to write the vault-named queries.
Implement the net.corda.v5.ledger.utxo.query.json.ContractStateVaultJsonFactory<T>
interface. The <T>
parameter is the type of the state that we want to represent.
For example, a state type called TestState
and a simple contract called TestContract
, would look like the following:
package com.r3.corda.demo.contract
class TestContract : Contract {
override fun verify(transaction: UtxoLedgerTransaction) {}
}
@BelongsToContract(TestContract::class)
class TestState(
val testField: String,
private val participants: List<PublicKey>
) : ContractState {
override fun getParticipants(): List<PublicKey> = participants
}
package com.r3.corda.demo.contract;
class TestContract implements Contract {
@Override
public void verify(@NotNull UtxoLedgerTransaction transaction) {}
}
@BelongsToContract(TestContract.class)
public class TestState implements ContractState {
private final List<PublicKey> participants;
private final String testField;
public TestState(@NotNull String testField, @NotNull List<PublicKey> participants) {
this.testField = testField;
this.participants = participants;
}
@NotNull
@Override
public List<PublicKey> getParticipants() {
return new LinkedList<>(this.participants);
}
@NotNull
public String getTestField() {
return testField;
}
}
testField
property defined for JSON representation and constructing queries.To represent a state as a JSON string, use ContractStateVaultJsonFactory
as follows:
class TestStateJsonFactory : ContractStateVaultJsonFactory<TestState> {
override fun getStateType(): Class<TestState> = TestState::class.java
override fun create(state: TestState, jsonMarshallingService: JsonMarshallingService): String {
return jsonMarshallingService.format(state)
}
}
class TestStateJsonFactory implements ContractStateVaultJsonFactory<TestState> {
@NotNull
@Override
public Class<TestState> getStateType() {
return TestState.class;
}
@Override
@NotNull
public String create(@NotNull TestState state, @NotNull JsonMarshallingService jsonMarshallingService) {
return jsonMarshallingService.format(state);
}
}
After the output state finalizes, it is represented as the following in the database (custom_representation
column)
with a stateRef
field stored under the ContractState
JSON object:
{
"net.corda.v5.ledger.utxo.ContractState" : {
"stateRef": "<TransactionID>:<StateIndex>"
},
"com.r3.corda.demo.contract.TestState" : {
"testField": ""
}
}
- The
net.corda.v5.ledger.utxo.ContractState
field is a part of the JSON representation for all state types. - The implementation of the JSON factory must be defined in the same CPK as the state that it is working on, so that the platform can get hold of it when persisting a state to the database.
Use this representation to create the vault-named queries in the next section.
Creating and Registering a Vault-Named Query
Registration means that the query is stored on sandbox
An execution environment within a JVM process that provides isolation for a CorDapp. It shields it from outside threats but it also restricts what it can do so that running potentially dangerous code cannot harm others.
creation time and can be executed later.
Vault-named queries must be part of a contract CPK. Corda installs the vault-named query when the contract CPK is uploaded.
To create and register a query, the net.corda.v5.ledger.utxo.query.VaultNamedQueryFactory
interface must be implemented.
Basic Vault-Named Query Registration Example
A simple vault-named query implementation for this TestState
would look like this:
class DummyCustomQueryFactory : VaultNamedQueryFactory {
override fun create(vaultNamedQueryBuilderFactory: VaultNamedQueryBuilderFactory) {
vaultNamedQueryBuilderFactory.create("DUMMY_CUSTOM_QUERY")
.whereJson(
"WHERE visible_states.custom_representation -> 'com.r3.corda.demo.contract.TestState' " +
"->> 'testField' = :testField"
)
.register()
}
}
public class DummyCustomQueryFactory implements VaultNamedQueryFactory {
@Override
public void create(@NotNull VaultNamedQueryBuilderFactory vaultNamedQueryBuilderFactory) {
vaultNamedQueryBuilderFactory.create("DUMMY_CUSTOM_QUERY")
.whereJson(
"WHERE visible_states.custom_representation -> 'com.r3.corda.demo.contract.TestState' " +
"->> 'testField' = :testField"
)
.register();
}
}
The interface called VaultNamesQueryFactory
has one method:
void create(@NotNull VaultNamedQueryBuilderFactory vaultNamedQueryBuilderFactory);
.
This function is called during start-up and it defines how a query operates in it with the following steps:
To create a vault-named query with a given name, in this case it is
DUMMY_CUSTOM_QUERY
, callvaultNamedQueryBuilderFactory.create()
.To define how a query’s
WHERE
clause will work, callwhereJson()
.- Always start with the actual
WHERE
statement and then write the rest of the clause. You must prefix fields with thevisible_states.
qualifier. Sincevisible_states.custom_representation
is a JSON column, you can use some JSON-specific operations. - Parameters can be used in the query in a
:parameter
format. In this example, use a parameter called:testField
which can be set when executing this query. This works similarly to popular Java SQL libraries such as Hibernate.
- Always start with the actual
To finalize query creation and to store the created query in the registry to be executed later, call
register()
. This call must be the last step when defining a query.
Complex Query Example
For example, to add some extra logic to the query:
- Only keep the results that have “Alice” in their participant list.
- Transform the result set to only keep the transaction A transaction is a proposal to update the ledger. IDs.
- Collect the result set into one single integer.
These optional logics are always applied in the following order:
whereJson
returns StateAndRef
objects and the data going into the filtering and transforming logic consists of StateAndRefs
.Filtering
To create a filtering logic, implement the net.corda.v5.ledger.utxo.query.VaultNamedQueryStateAndRefFilter<T>
interface. The <T>
type here is the state type that will be returned from the database, which in this case is TestState
. This interface has only one function.
This defines whether or not to keep the given element (row
) from the result set. Elements returning true are kept and the rest are discarded.
In this example, keep the elements that have “Alice” in their participant list:
class DummyCustomQueryFilter : VaultNamedQueryStateAndRefFilter<TestState> {
override fun filter(data: StateAndRef<TestState>, parameters: MutableMap<String, Any>): Boolean {
return data.state.contractState.participantNames.contains("Alice")
}
}
class DummyCustomQueryFilter implements VaultNamedQueryStateAndRefFilter<TestState> {
@NotNull
@Override
public Boolean filter(@NotNull StateAndRef<TestState> data, @NotNull Map<String, Object> parameters) {
return data.getState().getContractState().getParticipantNames().contains("Alice");
}
}
Transforming
To create a transformer class, only keep the transaction IDs of each record. Transformer classes must implement the VaultNamedQueryStateAndRefTransformer<T, R>
interface, where:
<T>
is the type of results returned from the database, which in this case isTestState
.<R>
is the type to transform the results into.- Transaction IDs are specified as
Strings
.
This interface has one function:
@NotNull`
`R transform(@NotNull T data, @NotNull Map<String, Object> parameters);
This defines how each record (“row”) will be transformed (mapped):
class DummyCustomQueryTransformer : VaultNamedQueryStateAndRefTransformer<TestState, String> {
override fun transform(data: StateAndRef<TestState>, parameters: MutableMap<String, Any>): String {
return data.ref.transactionId.toString()
}
}
class DummyCustomQueryMapper implements VaultNamedQueryStateAndRefTransformer<TestState, String> {
@NotNull
@Override
public String transform(@NotNull StateAndRef<TestState> data, @NotNull Map<String, Object> parameters) {
return data.getRef().getTransactionId().toString();
}
}
String
object, which is the given state’s transaction ID.Collecting
Collecting is used to collect results set into one single integer.
For this, implement the net.corda.v5.ledger.utxo.query.VaultNamedQueryCollector<R, T>
interface, where:
<T>
is the type collected into (in this case, anInt
).<R>
is the type of the original result set (in this caseString
because of transformation).
This interface has only one method:
@NotNull
Result<T> collect(@NotNull List<R> resultSet, @NotNull Map<String, Object> parameters);
This defines how to collect the result set. The collector class should look like the following:
class DummyCustomQueryCollector : VaultNamedQueryCollector<String, Int> {
override fun collect(
resultSet: MutableList<String>,
parameters: MutableMap<String, Any>
): VaultNamedQueryCollector.Result<Int> {
return VaultNamedQueryCollector.Result(
listOf(resultSet.size),
true
)
}
}
class DummyCustomQueryCollector implements VaultNamedQueryCollector<String, Integer> {
@NotNull
@Override
public Result<Integer> collect(@NotNull List<String> resultSet, @NotNull Map<String, Object> parameters) {
return new Result<>(
List.of(resultSet.size()),
true
);
}
}
isDone
should only be set to true
if the result set is complete.Registering a Complex Query
Register a complex query with a filter, a transformer, and a collector with the following example:
class DummyCustomQueryFactory : VaultNamedQueryFactory {
override fun create(vaultNamedQueryBuilderFactory: VaultNamedQueryBuilderFactory) {
vaultNamedQueryBuilderFactory.create("DUMMY_CUSTOM_QUERY")
.whereJson(
"WHERE visible_states.custom_representation -> 'com.r3.corda.demo.contract.TestState' " +
"->> 'testField' = :testField"
)
.filter(DummyCustomQueryFilter())
.map(DummyCustomQueryMapper())
.collect(DummyCustomQueryCollector())
.register()
}
}
public class JsonQueryFactory implements VaultNamedQueryFactory {
@Override
public void create(@NotNull VaultNamedQueryBuilderFactory vaultNamedQueryBuilderFactory) {
vaultNamedQueryBuilderFactory.create("DUMMY_CUSTOM_QUERY")
.whereJson(
"WHERE visible_states.custom_representation -> 'com.r3.corda.demo.contract.TestState' " +
"->> 'testField' = :testField"
)
.filter(new DummyCustomQueryFilter())
.map(new DummyCustomQueryMapper())
.collect(new DummyCustomQueryCollector())
.register();
}
}
Executing a Vault-Named Query
To execute a query use UtxoLedgerService
. This can be injected to a flow
Communication between participants in an application network is peer-to-peer using flows.
via @CordaInject
.
To instantiate a query call the following:
utxoLedgerService.query("DUMMY_CUSTOM_QUERY", Int::class.java)
utxoLedgerService.query("DUMMY_CUSTOM_QUERY", Integer.class)
Provide the name of the query (in this case DUMMY_CUSTOM_QUERY
) and the return type. Since the result set is collected into an integer in the complex query example, use Int
(or Integer
in Java).
Before executing, define the following:
- How many results each page of the query should return (
setLimit
). The default value isInt.MAX
(2,147,483,647). - Named parameters that are in the query and the actual value for them (
setParameter
orsetParameters
). All parameters must be defined, otherwise the execution will fail. - An upper limit (
setTimestampLimit
) to only return states that were inserted before a given time. Each state in the database has a timestamp value for when it was inserted.
It is not necessary to call ParameterizedQuery.setOffset
as the query pages the results based on each state’s created timestamp.
In this case it would look like this:
val resultSet = utxoLedgerService.query("DUMMY_CUSTOM_QUERY", Int::class.java) // Instantiate the query
.setLimit(1000) // Only return 1000 records per page
.setParameter("testField", "dummy") // Set the parameter to a dummy value
.setCreatedTimestampLimit(Instant.now()) // Set the timestamp limit to the current time
.execute() // execute the query
PagedQuery.ResultSet<Integer> resultSet = utxoLedgerService.query("DUMMY_CUSTOM_QUERY", Integer.class) // Instantiate the query
.setLimit(1000) // Only return 1000 records per page
.setParameter("testField", "dummy") // Set the parameter to a dummy value
.setCreatedTimestampLimit(Instant.now()) // Set the timestamp limit to the current time
.execute(); // execute the query
testField
parameter in this query, but it can be replaced. There is only one parameter in this example query: :testField
.Results can be acquired by calling getResults()
on the ResultSet
. Call hasNext()
to check if there are more results to retrieve and next()
to move onto the next page:
val resultSet = utxoLedgerService.query("DUMMY_CUSTOM_QUERY", Int::class.java) // Instantiate the query
.setLimit(1000) // Only return 1000 records
.setParameter("testField", "dummy") // Set the parameter to a dummy value
.setCreatedTimestampLimit(Instant.now()) // Set the timestamp limit to the current time
.execute()
var results = resultSet.results
while (resultSet.hasNext()) {
results = resultSet.next()
}
ResultSet<Integer> resultSet = utxoLedgerService.query("DUMMY_CUSTOM_QUERY", Integer.class) // Instantiate the query
.setLimit(1000) // Only return 1000 records
.setParameter("testField", "dummy") // Set the parameter to a dummy value
.setCreatedTimestampLimit(Instant.now()) // Set the timestamp limit to the current time
.execute();
List<Integer> results = resultSet.getResults();
while (resultSet.hasNext()) {
results = resultSet.next();
}
Vault-Named Query Operators
The following are the standard operators for vault-named queries:
IN
LIKE
IS NOT NULL
IS NULL
AS
OR
AND
!=
>
<
>=
<=
->
->>
?
::
Where the behavior is not standard, the operators are explained with examples in the following table:
Operator | Right Operand Type | Description | Example |
---|---|---|---|
-> | Int | Gets JSON array element. | custom_representation -> 'com.r3.corda.demo.ArrayObjState' -> 0 For example, for the following JSON:
the following is returned:
|
-> | Text | Gets JSON object field. | custom_representation -> 'com.r3.corda.demo.TestState' selects the top-level JSON field called com.r3.corda.demo.TestState from the JSON object in the custom_representation database column.For example, for the following JSON:
the following is returned:
|
->> | Int | Get JSON array element as text. | custom_representation -> 'com.r3.corda.demo.ArrayState' ->> 2 selects the third element (indexing from 0) of the array type top-level JSON field called com.r3.corda.demo.ArrayState from the JSON object in the custom_representation database column.For example, 7 is returned for the following JSON:
|
->> | Text | Get JSON object field as text. | custom_representation -> 'com.r3.corda.demo.TestState' ->> 'testField' selects the `testField` JSON field from the top-level JSON object called com.r3.corda.demo.TestState in the custom_representation database column.For example, ABC is returned for the following JSON:
|
? | Text | Checks if JSON object field exists. | custom_representation ? 'com.r3.corda.demo.TestState' checks if the object in the custom_representation database column has a top-level field called com.r3.corda.demo.TestState .For example, true is returned for the following JSON:
|
:: | A type, for example, Int. | Casts the element/object field to the specified type. | (visible_states.field ->> property)::int = 1234 |
Was this page helpful?
Thanks for your feedback!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.
We're sorry this page wasn't helpful. Let us know how we can make it better!
Chat with us
Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.
Create an issue
Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.
Propose documentation improvements directly
Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.