14721

Corda: How to implement hierarchical relationships between state data persisted to H2

Question:

<strong><em>Summary</em></strong>

I've adapted the basic Token Issuance Corda Bootcamp application to demonstrate this issue. I want to create a bidirectional mapping between TokenStates and TokenChildren where the relationship is one-to-many.

<em>What are the best practices for persisting hierarchical data? Is it possible to implement this using JPA annotations in state schemas?</em>

I have one state - TokenState, that contains some arbitrary data as well as a Collection of objects with the class TokenChild. The purpose of this list is to facilitate a one-to-many relationship between the records in H2. The state's associated schema has corresponding JPA annotations (@OneToMany and @ManyToOne - see code snippet below). The TokenState class references the appropriate schema - TokenSchemaV1 in the supportedSchemas and generateMappedObject methods.

When I run TokenIssueFlow (also included as a snippet) from the console after deploying and running the nodes, the transaction succeeds but no token_child_states table is persisted to h2.

<strong><em>Other Notes</em></strong>

<ul><li>

I've also tried to implement a different strategy where both Tokens<br /> and TokenChildren are unique states (rather than one monolithic<br /> state). See <a href="https://github.com/corda/corda/issues/4193" rel="nofollow">this Github issue</a> for more details.

</li> <li>

Another solution might be having Tokens and TokenChildren as separate states and manually persisting foreign keys in h2 to facilitate this relationship but that seems like a work-around more than a solution.

</li> <li>

What are the ramifications for an even more deeply nested relationship between classes? (e.g. - a contrived example of TokenChildren having TokenGrandChildren and so forth). How do I use generateMappedObject() and supportedSchemas() to create the data model I need?

</li> </ul>

<strong><em>TokenState</em></strong>

<pre class="snippet-code-html lang-html prettyprint-override">public class TokenState implements LinearState, QueryableState { private final Party owner; private final Party issuer; private final int amount; private final UniqueIdentifier linearId; private List<TokenSchemaV1.PersistentChildToken> listOfPersistentChildTokens; public TokenState (Party issuer, Party owner, int amount, UniqueIdentifier linearId, List<TokenSchemaV1.PersistentChildToken> listOfPersistentChildTokens) { this.owner = owner; this.issuer = issuer; this.amount = amount; this.linearId = linearId; this.listOfPersistentChildTokens = listOfPersistentChildTokens; } public Party getOwner() { return owner; } public Party getIssuer() { return issuer; } public int getAmount() { return amount; } @Override public UniqueIdentifier getLinearId() { return linearId; } public List<TokenSchemaV1.PersistentChildToken> getListOfPersistentChildTokens() { return listOfPersistentChildTokens; } @Override public PersistentState generateMappedObject(MappedSchema schema) { if (schema instanceof TokenSchemaV1) { return new TokenSchemaV1.PersistentToken( this.getOwner().getName().toString(), this.getIssuer().getName().toString(), this.getAmount(), this.linearId.getId(), this.getListOfPersistentChildTokens() ); } else { throw new IllegalArgumentException("Unrecognised schema $schema"); } } @Override public Iterable<MappedSchema> supportedSchemas() { return ImmutableList.of(new TokenSchemaV1()); } @NotNull @Override public List<AbstractParty> getParticipants() { return ImmutableList.of(issuer, owner); } }

<strong><em>TokenSchemaV1</em></strong>

<pre class="snippet-code-html lang-html prettyprint-override">@CordaSerializable public class TokenSchemaV1 extends MappedSchema { public TokenSchemaV1() { super(TokenSchema.class, 1, ImmutableList.of(PersistentToken.class, PersistentChildToken.class)); } @Entity @Table(name = "token_states") public static class PersistentToken extends PersistentState { @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") private final UUID linearId; @OneToMany(mappedBy = "persistentToken") private final List<PersistentChildToken> listOfPersistentChildTokens; //get() = field public PersistentToken(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 PersistentToken() { 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 = "token_child_states") public static class PersistentChildToken { @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; @Column(name = "child proof") private final String childProof; @ManyToOne(targetEntity = PersistentToken.class) private final TokenState persistentToken; public PersistentChildToken(String owner, String issuer, int amount) { this.Id = UUID.randomUUID(); this.owner = owner; this.issuer = issuer; this.amount = amount; this.persistentToken = null; this.childProof = "I am a child"; } // Default constructor required by hibernate. public PersistentChildToken() { this.Id = UUID.randomUUID(); this.owner = ""; this.issuer = ""; this.amount = 0; this.persistentToken = null; this.childProof = "I am a child"; } 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; } } }

<strong><em>TokenIssueFlow</em></strong>

<pre class="snippet-code-html lang-html prettyprint-override">@InitiatingFlow @StartableByRPC public class TokenIssueFlow extends FlowLogic<SignedTransaction> { private final Party owner; private final int amount; public TokenIssueFlow(Party owner, int amount) { this.owner = owner; this.amount = amount; } private final ProgressTracker progressTracker = new ProgressTracker(); @Override public ProgressTracker getProgressTracker() { return progressTracker; } @Suspendable @Override public SignedTransaction call() throws FlowException { // We choose our transaction's notary (the notary prevents double-spends). Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0); // We get a reference to our own identity. Party issuer = getOurIdentity(); /* ============================================================================ * Create our TokenState to represent on-ledger tokens * ===========================================================================*/ List<TokenSchemaV1.PersistentChildToken> listOfPersistentChildTokens = new ArrayList<>(); for (int count = 0; count <=5; count++) { TokenSchemaV1.PersistentChildToken child = new TokenSchemaV1.PersistentChildToken(owner.getName().toString(), issuer.getName().toString(), amount + 2); listOfPersistentChildTokens.add(child); } // We create our new TokenState. TokenState tokenState = new TokenState(issuer, owner, amount, new UniqueIdentifier(), listOfPersistentChildTokens); /* ============================================================================ * Build our token issuance transaction to update the ledger * ===========================================================================*/ // We build our transaction. TransactionBuilder txBuilder = new TransactionBuilder(); txBuilder.setNotary(notary); txBuilder.addOutputState(tokenState, TokenContract.ID); TokenContract.Commands.Issue commandData = new TokenContract.Commands.Issue(); List<PublicKey> requiredSigners = ImmutableList.of(issuer.getOwningKey()); txBuilder.addCommand(commandData, requiredSigners); /* ============================================================================ * Write our TokenContract to control token issuance! * ===========================================================================*/ // We sign the transaction with our private key, making it immutable. SignedTransaction signedTransaction = getServiceHub().signInitialTransaction(txBuilder); // We check our transaction is valid based on its contracts. txBuilder.verify(getServiceHub()); // We get the transaction notarised and recorded automatically by the platform. return subFlow(new FinalityFlow(signedTransaction)); } }

Answer1:

I suspect you may need to add an explicit @Cascade(CascadeType.PERSIST) annotation on your @OneToMany relationship (in the parent class).

Have a look at the following working code snippet:

class SchemaFamily object TestSchema : MappedSchema(SchemaFamily::class.java, 1, setOf(Parent::class.java, Child::class.java)) { @Entity @Table(name = "Parents") class Parent : PersistentState() { @OneToMany(fetch = FetchType.LAZY) @JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index")) @OrderColumn @Cascade(CascadeType.PERSIST) var children: MutableSet<Child> = mutableSetOf() } @Suppress("unused") @Entity @Table(name = "Children") class Child { @Id @GeneratedValue @Column(name = "child_id", unique = true, nullable = false) var childId: Int? = null @ManyToOne(fetch = FetchType.LAZY) @JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index")) var parent: Parent? = null } }

Please adjust your code to the above and report back.

Answer2:

@Jose Coll - Thanks that was definitely it. Following up here with additional information on my implementation.

See the snippet below for a working implementation of the update schema classes. @OneToMany(cascade = CascadeType.PERSIST) caused the child table to be persisted to the DB on initialization of the node. I also had to include an @JoinColumn tag with the appropriate fields.

<pre class="snippet-code-html lang-html prettyprint-override">@CordaSerializable public class TokenSchemaV1 extends MappedSchema { public TokenSchemaV1() { super(TokenSchema.class, 1, ImmutableList.of(PersistentToken.class, PersistentChildToken.class)); } @Entity @Table(name = "token_states") public static class PersistentToken extends PersistentState { @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") private final UUID linearId; @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 PersistentToken(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 PersistentToken() { 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 = "token_child_states") public static class PersistentChildToken { @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; @Column(name = "child_proof") private final String childProof; @ManyToOne(targetEntity = PersistentToken.class) private final TokenState persistentToken; public PersistentChildToken(String owner, String issuer, int amount) { this.Id = UUID.randomUUID(); this.owner = owner; this.issuer = issuer; this.amount = amount; this.persistentToken = null; this.childProof = "I am a child"; } // Default constructor required by hibernate. public PersistentChildToken() { this.Id = UUID.randomUUID(); this.owner = ""; this.issuer = ""; this.amount = 0; this.persistentToken = null; this.childProof = "I am a child"; } 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; } } }

Recommend

  • Can I run a windows batch file with hidden cmd window but see the programs started by the batch?
  • Why I can't access http://hadoop-master:50070 when i define dfs.namenode.http-address
  • How to add test cases to an existing test run with the API from Java to TestRail?
  • Bamboo does not transform Web.config file for any custom build configuration
  • Firing application wide events from button handlers
  • How do I click on a Cell of a DataGridView programmatically?
  • Incompatible block pointer types SDWebImage
  • Overlay histograms in R
  • Why context.Wait in StartAsync didn't stop the dialog
  • How does `arguments.callee` refer to anonymous functions?
  • Is there way to structure a QueryExpression so that you could dynamically handle a unknown number of
  • How to run python3.7 based flask web api on azure
  • Button On Click event not firing
  • Window-select multiple artists and drag them on canvas
  • Is there a way to convert all existing table data to UTF8 collation?
  • Multiple git user in single device
  • Custom progress dialog not working
  • Simultaneous animation when entering editing mode of UITableViewCell
  • Homomorphic filtering in OpenCV
  • Scheme procedure to compute the nth repeated application of a function?
  • view details for exception in vs 2017
  • PHP: Need to close STDIN in order to read STDOUT?
  • Arraylist of strings into one comma separated string
  • Spotify cocoalibspotify offline status set to 1 but all tracks stuck at waiting
  • How to make Rss News Reader application in android …? [closed]
  • Floating parent div grows to hypothetical width of floating child div
  • Stacked bar chart with continuous time-axis as x-axis
  • reshape/remould data frame to create normalized bar chart and pie chart
  • Will this work on all screen sizes?
  • Google App Engine Datastore: Dealing with eventual consistency
  • read part of h5 dataset python
  • Codeigniniter insert data through models and controller
  • Accessing Arguments, Workflow Variables from custom activities
  • Write to .csv file with PHP (Commas in Data Error)
  • Spring Boot not autowiring @Repository
  • ReferenceError: TextEncoder is not defined
  • ARKit code issue {unknown error -1=ffffffffffffffff error: Task failed with exit 1}