mirror of
https://github.com/corda/corda.git
synced 2025-02-06 11:09:18 +00:00
ENT-1712 Warning on cross-reference between JPA entities from different mapped schemas (#3282)
At startup node warns about any MappedSchema containing a JPA entity referencing to another JPA entity from a different MappedSchema. Cross reference between JPA entities across different MappedSchema objects can cause operational issues while evolving one MappedSchema object or migrating it's data. Doc API: Persistence documentation no longer suggests mapping between different schemas.
This commit is contained in:
parent
b9bf624e7a
commit
baa6479575
@ -91,3 +91,43 @@ data class PersistentStateRef(
|
|||||||
* Marker interface to denote a persistable Corda state entity that will always have a transaction id and index
|
* Marker interface to denote a persistable Corda state entity that will always have a transaction id and index
|
||||||
*/
|
*/
|
||||||
interface StatePersistable : Serializable
|
interface StatePersistable : Serializable
|
||||||
|
|
||||||
|
object MappedSchemaValidator {
|
||||||
|
fun fieldsFromOtherMappedSchema(schema: MappedSchema) : List<SchemaCrossReferenceReport> =
|
||||||
|
schema.mappedTypes.map { entity ->
|
||||||
|
entity.declaredFields.filter { field ->
|
||||||
|
field.type.enclosingClass != null
|
||||||
|
&& MappedSchema::class.java.isAssignableFrom(field.type.enclosingClass)
|
||||||
|
&& hasJpaAnnotation(field.declaredAnnotations)
|
||||||
|
&& field.type.enclosingClass != schema.javaClass
|
||||||
|
}.map { field -> SchemaCrossReferenceReport(schema.javaClass.name, entity.simpleName, field.type.enclosingClass.name, field.name, field.type.simpleName)}
|
||||||
|
}.flatMap { it.toSet() }
|
||||||
|
|
||||||
|
fun methodsFromOtherMappedSchema(schema: MappedSchema) : List<SchemaCrossReferenceReport> =
|
||||||
|
schema.mappedTypes.map { entity ->
|
||||||
|
entity.declaredMethods.filter { method ->
|
||||||
|
method.returnType.enclosingClass != null
|
||||||
|
&& MappedSchema::class.java.isAssignableFrom(method.returnType.enclosingClass)
|
||||||
|
&& method.returnType.enclosingClass != schema.javaClass
|
||||||
|
&& hasJpaAnnotation(method.declaredAnnotations)
|
||||||
|
}.map { method -> SchemaCrossReferenceReport(schema.javaClass.name, entity.simpleName, method.returnType.enclosingClass.name, method.name, method.returnType.simpleName)}
|
||||||
|
}.flatMap { it.toSet() }
|
||||||
|
|
||||||
|
fun crossReferencesToOtherMappedSchema(schema: MappedSchema) : List<SchemaCrossReferenceReport> =
|
||||||
|
fieldsFromOtherMappedSchema(schema) + methodsFromOtherMappedSchema(schema)
|
||||||
|
|
||||||
|
/** Returns true if [javax.persistence] annotation expect [javax.persistence.Transient] is found. */
|
||||||
|
private inline fun hasJpaAnnotation(annotations: Array<Annotation>) =
|
||||||
|
annotations.any { annotation -> annotation.toString().startsWith("@javax.persistence.") && annotation !is javax.persistence.Transient }
|
||||||
|
|
||||||
|
class SchemaCrossReferenceReport(private val schema: String, private val entity: String, private val referencedSchema: String,
|
||||||
|
private val fieldOrMethod: String, private val fieldOrMethodType: String) {
|
||||||
|
|
||||||
|
override fun toString() = "Cross-reference between MappedSchemas '$schema' and '$referencedSchema'. " +
|
||||||
|
"MappedSchema '${schema.substringAfterLast(".")}' entity '$entity' field '$fieldOrMethod' is of type '$fieldOrMethodType' " +
|
||||||
|
"defined in another MappedSchema '${referencedSchema.substringAfterLast(".")}'."
|
||||||
|
|
||||||
|
fun toWarning() = toString() + " This may cause issues when evolving MappedSchema or migrating its data, " +
|
||||||
|
"ensure JPA entities are defined within the same enclosing MappedSchema."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package net.corda.core.schemas;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class BadSchemaJavaV1 extends MappedSchema {
|
||||||
|
|
||||||
|
public BadSchemaJavaV1() {
|
||||||
|
super(TestJavaSchemaFamily.class, 1, Arrays.asList(State.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public static class State extends PersistentState {
|
||||||
|
private String id;
|
||||||
|
private GoodSchemaJavaV1.State other;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JoinColumns({@JoinColumn(name = "itid"), @JoinColumn(name = "outid")})
|
||||||
|
@OneToOne
|
||||||
|
@MapsId
|
||||||
|
public GoodSchemaJavaV1.State getOther() {
|
||||||
|
return other;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOther(GoodSchemaJavaV1.State other) {
|
||||||
|
this.other = other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package net.corda.core.schemas;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class BadSchemaNoGetterJavaV1 extends MappedSchema {
|
||||||
|
|
||||||
|
public BadSchemaNoGetterJavaV1() {
|
||||||
|
super(TestJavaSchemaFamily.class, 1, Arrays.asList(State.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public static class State extends PersistentState {
|
||||||
|
@JoinColumns({@JoinColumn(name = "itid"), @JoinColumn(name = "outid")})
|
||||||
|
@OneToOne
|
||||||
|
@MapsId
|
||||||
|
public GoodSchemaJavaV1.State other;
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package net.corda.core.schemas;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class GoodSchemaJavaV1 extends MappedSchema {
|
||||||
|
|
||||||
|
public GoodSchemaJavaV1() {
|
||||||
|
super(TestJavaSchemaFamily.class, 1, Arrays.asList(State.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public static class State extends PersistentState {
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package net.corda.core.schemas;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Transient;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class PoliteSchemaJavaV1 extends MappedSchema {
|
||||||
|
|
||||||
|
public PoliteSchemaJavaV1() {
|
||||||
|
super(TestJavaSchemaFamily.class, 1, Arrays.asList(State.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public static class State extends PersistentState {
|
||||||
|
private String id;
|
||||||
|
private GoodSchemaJavaV1.State other;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
public GoodSchemaJavaV1.State getOther() {
|
||||||
|
return other;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOther(GoodSchemaJavaV1.State other) {
|
||||||
|
this.other = other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package net.corda.core.schemas;
|
||||||
|
|
||||||
|
public class TestJavaSchemaFamily {
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package net.corda.core.schemas;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class TrickySchemaJavaV1 extends MappedSchema {
|
||||||
|
|
||||||
|
public TrickySchemaJavaV1() {
|
||||||
|
super(TestJavaSchemaFamily.class, 1, Arrays.asList(TrickySchemaJavaV1.State.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public static class State extends PersistentState {
|
||||||
|
private String id;
|
||||||
|
private GoodSchemaJavaV1.State other;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
//the field is a cross-reference to other MappedSchema however the field is not persistent (no JPA annotation)
|
||||||
|
public GoodSchemaJavaV1.State getOther() {
|
||||||
|
return other;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOther(GoodSchemaJavaV1.State other) {
|
||||||
|
this.other = other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
package net.corda.core.schemas
|
||||||
|
|
||||||
|
import net.corda.core.schemas.MappedSchemaValidator.fieldsFromOtherMappedSchema
|
||||||
|
import net.corda.core.schemas.MappedSchemaValidator.methodsFromOtherMappedSchema
|
||||||
|
import net.corda.finance.schemas.CashSchema
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
import javax.persistence.*
|
||||||
|
|
||||||
|
class MappedSchemasCrossReferenceDetectionTests {
|
||||||
|
|
||||||
|
object GoodSchema : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(State::class.java)) {
|
||||||
|
@Entity
|
||||||
|
class State(
|
||||||
|
@Column
|
||||||
|
var id: String
|
||||||
|
) : PersistentState()
|
||||||
|
}
|
||||||
|
|
||||||
|
object BadSchema : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(State::class.java)) {
|
||||||
|
@Entity
|
||||||
|
class State(
|
||||||
|
@Column
|
||||||
|
var id: String,
|
||||||
|
|
||||||
|
@JoinColumns(JoinColumn(name = "itid"), JoinColumn(name = "outid"))
|
||||||
|
@OneToOne
|
||||||
|
@MapsId
|
||||||
|
var other: GoodSchema.State
|
||||||
|
) : PersistentState()
|
||||||
|
}
|
||||||
|
|
||||||
|
object TrickySchema : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(State::class.java)) {
|
||||||
|
@Entity
|
||||||
|
class State(
|
||||||
|
@Column
|
||||||
|
var id: String,
|
||||||
|
|
||||||
|
//the field is a cross-reference to other MappedSchema however the field is not persistent (no JPA annotation)
|
||||||
|
var other: GoodSchema.State
|
||||||
|
) : PersistentState()
|
||||||
|
}
|
||||||
|
|
||||||
|
object PoliteSchema : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(State::class.java)) {
|
||||||
|
@Entity
|
||||||
|
class State(
|
||||||
|
@Column
|
||||||
|
var id: String,
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var other: GoodSchema.State
|
||||||
|
) : PersistentState()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no cross reference to other schema`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(GoodSchema)).isEmpty()
|
||||||
|
assertThat(methodsFromOtherMappedSchema(GoodSchema)).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cross reference to other schema is detected`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(BadSchema)).isNotEmpty
|
||||||
|
assertThat(methodsFromOtherMappedSchema(BadSchema)).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cross reference via non JPA field is allowed`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(TrickySchema)).isEmpty()
|
||||||
|
assertThat(methodsFromOtherMappedSchema(TrickySchema)).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cross reference via transient field is allowed`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(PoliteSchema)).isEmpty()
|
||||||
|
assertThat(methodsFromOtherMappedSchema(PoliteSchema)).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no cross reference to other schema java`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(GoodSchemaJavaV1())).isEmpty()
|
||||||
|
assertThat(methodsFromOtherMappedSchema(GoodSchemaJavaV1())).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cross reference to other schema is detected java`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(BadSchemaJavaV1())).isEmpty()
|
||||||
|
assertThat(methodsFromOtherMappedSchema(BadSchemaJavaV1())).isNotEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cross reference to other schema via field is detected java`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(BadSchemaNoGetterJavaV1())).isNotEmpty
|
||||||
|
assertThat(methodsFromOtherMappedSchema(BadSchemaNoGetterJavaV1())).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cross reference via non JPA field is allowed java`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(TrickySchemaJavaV1())).isEmpty()
|
||||||
|
assertThat(methodsFromOtherMappedSchema(TrickySchemaJavaV1())).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cross reference via transient field is allowed java`() {
|
||||||
|
assertThat(fieldsFromOtherMappedSchema(PoliteSchemaJavaV1())).isEmpty()
|
||||||
|
assertThat(methodsFromOtherMappedSchema(PoliteSchemaJavaV1())).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
@ -57,11 +57,15 @@ integration points and not necessarily with every upgrade to the contract code.
|
|||||||
``MappedSchema`` offered by a ``QueryableState``, automatically upgrade to a later version of a schema or even
|
``MappedSchema`` offered by a ``QueryableState``, automatically upgrade to a later version of a schema or even
|
||||||
provide a ``MappedSchema`` not originally offered by the ``QueryableState``.
|
provide a ``MappedSchema`` not originally offered by the ``QueryableState``.
|
||||||
|
|
||||||
It is expected that multiple different contract state implementations might provide mappings to some common schema.
|
It is expected that multiple different contract state implementations might provide mappings within a single schema.
|
||||||
For example an Interest Rate Swap contract and an Equity OTC Option contract might both provide a mapping to a common
|
For example an Interest Rate Swap contract and an Equity OTC Option contract might both provide a mapping to
|
||||||
Derivative schema. The schemas should typically not be part of the contract itself and should exist independently of it
|
a Derivative contract within the same schema. The schemas should typically not be part of the contract itself and should exist independently
|
||||||
to encourage re-use of a common set within a particular business area or Cordapp.
|
to encourage re-use of a common set within a particular business area or Cordapp.
|
||||||
|
|
||||||
|
.. note:: It's advisable to avoid cross-references between different schemas as this may cause issues when evolving ``MappedSchema``
|
||||||
|
or migrating its data. At startup, nodes log such violations as warnings stating that there's a cross-reference between ``MappedSchema``'s.
|
||||||
|
The detailed messages incorporate information about what schemas, entities and fields are involved.
|
||||||
|
|
||||||
``MappedSchema`` offer a family name that is disambiguated using Java package style name-spacing derived from the
|
``MappedSchema`` offer a family name that is disambiguated using Java package style name-spacing derived from the
|
||||||
class name of a *schema family* class that is constant across versions, allowing the ``SchemaService`` to select a
|
class name of a *schema family* class that is constant across versions, allowing the ``SchemaService`` to select a
|
||||||
preferred version of a schema.
|
preferred version of a schema.
|
||||||
|
@ -132,6 +132,10 @@ Unreleased
|
|||||||
|
|
||||||
* Table name with a typo changed from ``NODE_ATTCHMENTS_CONTRACTS`` to ``NODE_ATTACHMENTS_CONTRACTS``.
|
* Table name with a typo changed from ``NODE_ATTCHMENTS_CONTRACTS`` to ``NODE_ATTACHMENTS_CONTRACTS``.
|
||||||
|
|
||||||
|
* Node logs a warning for any ``MappedSchema`` containing a JPA entity referencing another JPA entity from a different ``MappedSchema`.
|
||||||
|
The log entry starts with `Cross-reference between MappedSchemas.`.
|
||||||
|
API: Persistence documentation no longer suggests mapping between different schemas.
|
||||||
|
|
||||||
.. _changelog_v3.1:
|
.. _changelog_v3.1:
|
||||||
|
|
||||||
Version 3.1
|
Version 3.1
|
||||||
|
@ -232,6 +232,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
|
|||||||
initCertificate()
|
initCertificate()
|
||||||
initialiseJVMAgents()
|
initialiseJVMAgents()
|
||||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null)
|
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null)
|
||||||
|
schemaService.mappedSchemasWarnings().forEach {
|
||||||
|
val warning = it.toWarning()
|
||||||
|
log.warn(warning)
|
||||||
|
Node.printWarning(warning)
|
||||||
|
}
|
||||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||||
|
|
||||||
// Wrapped in an atomic reference just to allow setting it before the closure below gets invoked.
|
// Wrapped in an atomic reference just to allow setting it before the closure below gets invoked.
|
||||||
|
@ -7,6 +7,8 @@ import net.corda.core.schemas.CommonSchemaV1
|
|||||||
import net.corda.core.schemas.MappedSchema
|
import net.corda.core.schemas.MappedSchema
|
||||||
import net.corda.core.schemas.PersistentState
|
import net.corda.core.schemas.PersistentState
|
||||||
import net.corda.core.schemas.QueryableState
|
import net.corda.core.schemas.QueryableState
|
||||||
|
import net.corda.core.schemas.*
|
||||||
|
import net.corda.core.schemas.MappedSchemaValidator.crossReferencesToOtherMappedSchema
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import net.corda.node.internal.schemas.NodeInfoSchemaV1
|
import net.corda.node.internal.schemas.NodeInfoSchemaV1
|
||||||
import net.corda.node.services.api.SchemaService
|
import net.corda.node.services.api.SchemaService
|
||||||
@ -91,4 +93,9 @@ class NodeSchemaService(extraSchemas: Set<MappedSchema> = emptySet(), includeNot
|
|||||||
return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants)
|
return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants)
|
||||||
return (state as QueryableState).generateMappedObject(schema)
|
return (state as QueryableState).generateMappedObject(schema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns list of [MappedSchemaValidator.SchemaCrossReferenceReport] violations. */
|
||||||
|
fun mappedSchemasWarnings(): List<MappedSchemaValidator.SchemaCrossReferenceReport> =
|
||||||
|
schemaOptions.keys.map { schema -> crossReferencesToOtherMappedSchema(schema) }.flatMap { it.toList() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user