问题
Summary
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.
What are the best practices for persisting hierarchical data? Is it possible to implement this using JPA annotations in state schemas?
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.
Other Notes
I've also tried to implement a different strategy where both Tokens
and TokenChildren are unique states (rather than one monolithic
state). See this Github issue for more details.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.
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()
andsupportedSchemas()
to create the data model I need?
TokenState
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);
}
}
TokenSchemaV1
@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;
}
}
}
TokenIssueFlow
@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));
}
}
回答1:
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.
回答2:
@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.
@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;
}
}
}
来源:https://stackoverflow.com/questions/53233011/corda-how-to-implement-hierarchical-relationships-between-state-data-persisted