This is an automated email from the ASF dual-hosted git repository. ntimofeev pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit e9df41a835becf2d5d3f66267e2b2ad58007529f Author: Nikita Timofeev <stari...@gmail.com> AuthorDate: Mon May 27 14:35:12 2024 +0400 CAY-2848 Vertical Inheritance: Updating one-to-many with inverse nullifies other columns --- .../access/flush/ArcValuesCreationHandler.java | 56 ++++++++++++++++++---- .../access/flush/ValuesCreationHandler.java | 6 ++- .../cayenne/access/VerticalInheritanceIT.java | 35 ++++++++++++++ .../testdo/inheritance_vertical/auto/_IvImpl.java | 17 +++++++ .../testdo/inheritance_vertical/auto/_IvOther.java | 22 +++++++++ .../test/resources/inheritance-vertical.map.xml | 11 ++++- 6 files changed, 137 insertions(+), 10 deletions(-) diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java index 881e0de11..8e6d1e500 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java @@ -89,23 +89,29 @@ class ArcValuesCreationHandler implements GraphChangeHandler { } if(objRelationship.isFlattened()) { - processFlattenedPath(arcTarget.getSourceId(), arcTarget.getTargetId(), entity.getDbEntity(), + FlattenedPathProcessingResult result = processFlattenedPath(arcTarget.getSourceId(), arcTarget.getTargetId(), entity.getDbEntity(), objRelationship.getDbRelationshipPath(), created); + if(result.isProcessed()) { + factory.getProcessedArcs().add(arcTarget); + } } else { DbRelationship dbRelationship = objRelationship.getDbRelationships().get(0); processRelationship(dbRelationship, arcTarget.getSourceId(), arcTarget.getTargetId(), created); + factory.getProcessedArcs().add(arcTarget); } - - factory.getProcessedArcs().add(arcTarget); } - ObjectId processFlattenedPath(ObjectId id, ObjectId finalTargetId, DbEntity entity, CayennePath dbPath, boolean add) { - Iterator<CayenneMapEntry> dbPathIterator = entity.resolvePathComponents(dbPath); + FlattenedPathProcessingResult processFlattenedPath(ObjectId id, ObjectId finalTargetId, DbEntity entity, CayennePath dbPath, boolean add) { + if(shouldSkipFlattenedOp(id, finalTargetId)) { + return flattenedResultNotProcessed(); + } + CayennePath flattenedPath = CayennePath.EMPTY_PATH; ObjectId srcId = id; ObjectId targetId = null; + Iterator<CayenneMapEntry> dbPathIterator = entity.resolvePathComponents(dbPath); while(dbPathIterator.hasNext()) { CayenneMapEntry entry = dbPathIterator.next(); flattenedPath = flattenedPath.dot(entry.getName()); @@ -139,8 +145,8 @@ class ArcValuesCreationHandler implements GraphChangeHandler { } else { type = add ? DbRowOpType.INSERT : DbRowOpType.UPDATE; factory.<DbRowOpWithValues>getOrCreate(target, targetId, type) - .getValues() - .addFlattenedId(flattenedPath, targetId); + .getValues() + .addFlattenedId(flattenedPath, targetId); } } else if(dbPathIterator.hasNext()) { // should update existing DB row @@ -151,7 +157,15 @@ class ArcValuesCreationHandler implements GraphChangeHandler { } } - return targetId; + return flattenedResultId(targetId); + } + + private boolean shouldSkipFlattenedOp(ObjectId id, ObjectId finalTargetId) { + // as we get two sides of the relationship processed, + // check if we got more information for a reverse operation + return finalTargetId != null + && factory.getStore().getFlattenedIds(id).isEmpty() + && !factory.getStore().getFlattenedIds(finalTargetId).isEmpty(); } private boolean shouldProcessAsAddition(DbRelationship relationship, boolean add) { @@ -281,4 +295,30 @@ class ArcValuesCreationHandler implements GraphChangeHandler { return null; } } + + static FlattenedPathProcessingResult flattenedResultId(ObjectId id) { + return new FlattenedPathProcessingResult(true, id); + } + + static FlattenedPathProcessingResult flattenedResultNotProcessed() { + return new FlattenedPathProcessingResult(false, null); + } + + final static class FlattenedPathProcessingResult { + private final boolean processed; + private final ObjectId id; + + private FlattenedPathProcessingResult(boolean processed, ObjectId id) { + this.processed = processed; + this.id = id; + } + + public boolean isProcessed() { + return processed; + } + + public ObjectId getId() { + return id; + } + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/ValuesCreationHandler.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/ValuesCreationHandler.java index defdb1a3a..10a6d309a 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/ValuesCreationHandler.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/ValuesCreationHandler.java @@ -52,7 +52,11 @@ class ValuesCreationHandler extends ArcValuesCreationHandler { if(attribute.isFlattened()) { // get target row ID - id = processFlattenedPath(id, null, dbEntity, attribute.getDbAttributePath(), newValue != null); + FlattenedPathProcessingResult result + = processFlattenedPath(id, null, dbEntity, attribute.getDbAttributePath(), newValue != null); + if(result.isProcessed()) { + id = result.getId(); + } } if(id == null) { diff --git a/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java b/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java index 32a7fed05..1fd162bbb 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java @@ -688,6 +688,41 @@ public class VerticalInheritanceIT extends RuntimeCase { } } + @Test + public void testUpdateFlattenedRelationshipWithInverse() throws SQLException { + TableHelper ivOtherTable = new TableHelper(dbHelper, "IV_OTHER"); + ivOtherTable.setColumns("ID", "NAME").setColumnTypes(Types.INTEGER, Types.VARCHAR); + + TableHelper ivBaseTable = new TableHelper(dbHelper, "IV_BASE"); + ivBaseTable.setColumns("ID", "NAME", "TYPE").setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.CHAR); + + TableHelper ivImplTable = new TableHelper(dbHelper, "IV_IMPL"); + ivImplTable.setColumns("ID", "ATTR1", "OTHER3_ID").setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.INTEGER); + + ivOtherTable.insert(1, "other1"); + ivOtherTable.insert(2, "other2"); + ivBaseTable.insert(1, "Impl 1", "I"); + ivImplTable.insert(1, "attr1", 1); + + IvImpl impl = SelectById.query(IvImpl.class, 1).selectOne(context); + IvOther other = SelectById.query(IvOther.class, 2).selectOne(context); + + impl.setOther3(other); + context.commitChanges(); + assertEquals("Impl 1", impl.getName()); + assertEquals("attr1", impl.getAttr1()); + assertEquals(impl.getOther3(), other); + + { + ObjectContext cleanContext = runtime.newContext(); + IvImpl implFetched = SelectById.query(IvImpl.class, 1).selectOne(cleanContext); + IvOther otherFetched = SelectById.query(IvOther.class, 2).selectOne(cleanContext); + assertEquals("Impl 1", implFetched.getName()); + assertEquals("attr1", implFetched.getAttr1()); + assertEquals(implFetched.getOther3(), otherFetched); + } + } + @Test public void testDeleteFlattenedNoValues() throws SQLException { ivAbstractTable.insert(1, null, "S"); diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/inheritance_vertical/auto/_IvImpl.java b/cayenne/src/test/java/org/apache/cayenne/testdo/inheritance_vertical/auto/_IvImpl.java index a3214b2d6..f7b998530 100644 --- a/cayenne/src/test/java/org/apache/cayenne/testdo/inheritance_vertical/auto/_IvImpl.java +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/inheritance_vertical/auto/_IvImpl.java @@ -35,6 +35,7 @@ public abstract class _IvImpl extends IvBase { public static final StringProperty<String> ATTR2 = PropertyFactory.createString("attr2", String.class); public static final EntityProperty<IvOther> OTHER1 = PropertyFactory.createEntity("other1", IvOther.class); public static final EntityProperty<IvOther> OTHER2 = PropertyFactory.createEntity("other2", IvOther.class); + public static final EntityProperty<IvOther> OTHER3 = PropertyFactory.createEntity("other3", IvOther.class); protected Date attr0; protected String attr1; @@ -42,6 +43,7 @@ public abstract class _IvImpl extends IvBase { protected Object other1; protected Object other2; + protected Object other3; public void setAttr0(Date attr0) { beforePropertyWrite("attr0", this.attr0, attr0); @@ -89,6 +91,14 @@ public abstract class _IvImpl extends IvBase { return (IvOther)readProperty("other2"); } + public void setOther3(IvOther other3) { + setToOneTarget("other3", other3, true); + } + + public IvOther getOther3() { + return (IvOther)readProperty("other3"); + } + @Override public Object readPropertyDirectly(String propName) { if(propName == null) { @@ -106,6 +116,8 @@ public abstract class _IvImpl extends IvBase { return this.other1; case "other2": return this.other2; + case "other3": + return this.other3; default: return super.readPropertyDirectly(propName); } @@ -133,6 +145,9 @@ public abstract class _IvImpl extends IvBase { case "other2": this.other2 = val; break; + case "other3": + this.other3 = val; + break; default: super.writePropertyDirectly(propName, val); } @@ -154,6 +169,7 @@ public abstract class _IvImpl extends IvBase { out.writeObject(this.attr2); out.writeObject(this.other1); out.writeObject(this.other2); + out.writeObject(this.other3); } @Override @@ -164,6 +180,7 @@ public abstract class _IvImpl extends IvBase { this.attr2 = (String)in.readObject(); this.other1 = in.readObject(); this.other2 = in.readObject(); + this.other3 = in.readObject(); } } diff --git a/cayenne/src/test/java/org/apache/cayenne/testdo/inheritance_vertical/auto/_IvOther.java b/cayenne/src/test/java/org/apache/cayenne/testdo/inheritance_vertical/auto/_IvOther.java index 63ee46bee..fc18cbc55 100644 --- a/cayenne/src/test/java/org/apache/cayenne/testdo/inheritance_vertical/auto/_IvOther.java +++ b/cayenne/src/test/java/org/apache/cayenne/testdo/inheritance_vertical/auto/_IvOther.java @@ -35,12 +35,14 @@ public abstract class _IvOther extends PersistentObject { public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class); public static final EntityProperty<IvBase> BASE = PropertyFactory.createEntity("base", IvBase.class); public static final ListProperty<IvImpl> IMPLS = PropertyFactory.createList("impls", IvImpl.class); + public static final ListProperty<IvImpl> IMPLS_WITH_INVERSE = PropertyFactory.createList("implsWithInverse", IvImpl.class); public static final ListProperty<IvImplWithLock> IMPLS_WITH_LOCK = PropertyFactory.createList("implsWithLock", IvImplWithLock.class); protected String name; protected Object base; protected Object impls; + protected Object implsWithInverse; protected Object implsWithLock; public void setName(String name) { @@ -74,6 +76,19 @@ public abstract class _IvOther extends PersistentObject { return (List<IvImpl>)readProperty("impls"); } + public void addToImplsWithInverse(IvImpl obj) { + addToManyTarget("implsWithInverse", obj, true); + } + + public void removeFromImplsWithInverse(IvImpl obj) { + removeToManyTarget("implsWithInverse", obj, true); + } + + @SuppressWarnings("unchecked") + public List<IvImpl> getImplsWithInverse() { + return (List<IvImpl>)readProperty("implsWithInverse"); + } + public void addToImplsWithLock(IvImplWithLock obj) { addToManyTarget("implsWithLock", obj, true); } @@ -100,6 +115,8 @@ public abstract class _IvOther extends PersistentObject { return this.base; case "impls": return this.impls; + case "implsWithInverse": + return this.implsWithInverse; case "implsWithLock": return this.implsWithLock; default: @@ -123,6 +140,9 @@ public abstract class _IvOther extends PersistentObject { case "impls": this.impls = val; break; + case "implsWithInverse": + this.implsWithInverse = val; + break; case "implsWithLock": this.implsWithLock = val; break; @@ -145,6 +165,7 @@ public abstract class _IvOther extends PersistentObject { out.writeObject(this.name); out.writeObject(this.base); out.writeObject(this.impls); + out.writeObject(this.implsWithInverse); out.writeObject(this.implsWithLock); } @@ -154,6 +175,7 @@ public abstract class _IvOther extends PersistentObject { this.name = (String)in.readObject(); this.base = in.readObject(); this.impls = in.readObject(); + this.implsWithInverse = in.readObject(); this.implsWithLock = in.readObject(); } diff --git a/cayenne/src/test/resources/inheritance-vertical.map.xml b/cayenne/src/test/resources/inheritance-vertical.map.xml index ff892937b..368ef9ee8 100644 --- a/cayenne/src/test/resources/inheritance-vertical.map.xml +++ b/cayenne/src/test/resources/inheritance-vertical.map.xml @@ -60,6 +60,7 @@ <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> <db-attribute name="OTHER1_ID" type="INTEGER"/> <db-attribute name="OTHER2_ID" type="INTEGER"/> + <db-attribute name="OTHER3_ID" type="INTEGER"/> </db-entity> <db-entity name="IV_IMPL_WITH_LOCK"> <db-attribute name="ATTR1" type="VARCHAR" isMandatory="true" length="100"/> @@ -234,6 +235,9 @@ <db-relationship name="other2" source="IV_IMPL" target="IV_OTHER"> <db-attribute-pair source="OTHER2_ID" target="ID"/> </db-relationship> + <db-relationship name="other3" source="IV_IMPL" target="IV_OTHER"> + <db-attribute-pair source="OTHER3_ID" target="ID"/> + </db-relationship> <db-relationship name="base" source="IV_IMPL_WITH_LOCK" target="IV_BASE_WITH_LOCK"> <db-attribute-pair source="ID" target="ID"/> </db-relationship> @@ -246,6 +250,9 @@ <db-relationship name="impls" source="IV_OTHER" target="IV_IMPL" toMany="true"> <db-attribute-pair source="ID" target="OTHER_ID"/> </db-relationship> + <db-relationship name="implsWithInverse" source="IV_OTHER" target="IV_IMPL" toMany="true"> + <db-attribute-pair source="ID" target="OTHER3_ID"/> + </db-relationship> <db-relationship name="implsWithLock" source="IV_OTHER" target="IV_IMPL_WITH_LOCK" toMany="true"> <db-attribute-pair source="ID" target="OTHER_ID"/> </db-relationship> @@ -280,13 +287,15 @@ <obj-relationship name="relatedConcrete" source="IvAbstract" target="IvConcrete" deleteRule="Nullify" db-relationship-path="relatedConcrete.abstract"/> <obj-relationship name="others" source="IvBase" target="IvOther" deleteRule="Deny" db-relationship-path="others"/> <obj-relationship name="children" source="IvConcrete" target="IvConcrete" deleteRule="Deny" db-relationship-path="children"/> - <obj-relationship name="relatedAbstract" source="IvConcrete" target="IvAbstract" deleteRule="Nullify" db-relationship-path="concrete.relatedAbstract"/> <obj-relationship name="parent" source="IvConcrete" target="IvConcrete" deleteRule="Nullify" db-relationship-path="parent"/> + <obj-relationship name="relatedAbstract" source="IvConcrete" target="IvAbstract" deleteRule="Nullify" db-relationship-path="concrete.relatedAbstract"/> <obj-relationship name="other1" source="IvImpl" target="IvOther" deleteRule="Nullify" db-relationship-path="impl.other1"/> <obj-relationship name="other2" source="IvImpl" target="IvOther" deleteRule="Nullify" db-relationship-path="impl.other2"/> + <obj-relationship name="other3" source="IvImpl" target="IvOther" deleteRule="Nullify" db-relationship-path="impl.other3"/> <obj-relationship name="other1" source="IvImplWithLock" target="IvOther" deleteRule="Nullify" db-relationship-path="impl.other1"/> <obj-relationship name="base" source="IvOther" target="IvBase" deleteRule="Nullify" db-relationship-path="base"/> <obj-relationship name="impls" source="IvOther" target="IvImpl" deleteRule="Deny" db-relationship-path="impls.base"/> + <obj-relationship name="implsWithInverse" source="IvOther" target="IvImpl" deleteRule="Deny" db-relationship-path="implsWithInverse.base"/> <obj-relationship name="implsWithLock" source="IvOther" target="IvImplWithLock" deleteRule="Deny" db-relationship-path="impls.base"/> <obj-relationship name="ivRoot" source="IvSub3" target="IvRoot" deleteRule="Nullify" db-relationship-path="sub3.ivRoot1"/> </data-map>