diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 40cb8de23f..f34d76eba8 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -29,6 +29,7 @@ CAY-2964 ClassCastException for non-generated meaningful PKs CAY-2965 MCP Cgen should not fail on an absent "" tag CAY-2966 "comment" field is lost when upgrading from v10 to v12 CAY-2967 SQLTemplate/SQLSelect broken pagination +CAY-2968 Vertical Inheritance: INSERT instead of UPDATE after updating flattened attribute ---------------------------------- Release: 5.0-M2 diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContextSnapshotBuilder.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContextSnapshotBuilder.java index b873b63918..a7bfb574ba 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/DataContextSnapshotBuilder.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContextSnapshotBuilder.java @@ -22,8 +22,10 @@ import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.DataRow; import org.apache.cayenne.Fault; +import org.apache.cayenne.ObjectId; import org.apache.cayenne.PersistenceState; import org.apache.cayenne.Persistent; +import org.apache.cayenne.exp.path.CayennePath; import org.apache.cayenne.map.DbJoin; import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.EntityResolver; @@ -80,6 +82,16 @@ public DataRow build() { } } + // Ensure primary keys of additional entities are also included in the snapshot + for (CayennePath path : descriptor.getAdditionalDbEntities().keySet()) { + ObjectId flattenedId = objectStore.getFlattenedId(object.getObjectId(), path); + if (flattenedId != null) { + for (Map.Entry idEntry : flattenedId.getIdSnapshot().entrySet()) { + snapshot.putIfAbsent(path.dot(idEntry.getKey()).value(), idEntry.getValue()); + } + } + } + return snapshot; } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectResolver.java b/cayenne/src/main/java/org/apache/cayenne/access/ObjectResolver.java index c9ac306cb6..2995b5891a 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectResolver.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectResolver.java @@ -186,8 +186,7 @@ private void resolveAdditionalIds(DataRow row, Persistent object, ClassDescripto for(Map.Entry entry : classDescriptor.getAdditionalDbEntities().entrySet()) { DbEntity dbEntity = entry.getValue().getDbEntity(); CayennePath path = entry.getKey(); - CayennePath prefix = path.length() == 1 ? path : path.tail(path.length() - 1); - ObjectId objectId = createObjectId(row, "db:" + dbEntity.getName(), dbEntity.getPrimaryKeys(), prefix, false); + ObjectId objectId = createObjectId(row, "db:" + dbEntity.getName(), dbEntity.getPrimaryKeys(), path, false); if(objectId != null) { context.getObjectStore().markFlattenedPath(object.getObjectId(), path, objectId); } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/PrefetchProcessorJointNode.java b/cayenne/src/main/java/org/apache/cayenne/access/PrefetchProcessorJointNode.java index 8334ed350a..150850a937 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/PrefetchProcessorJointNode.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/PrefetchProcessorJointNode.java @@ -228,6 +228,14 @@ private boolean visitRelationship(ArcProperty arc) { appendColumn(targetSource, pkName, prefix + pkName); } + // append id columns of additional entities... + descriptor.getAdditionalDbEntities().forEach((additionalPath, additionalDescriptor) -> { + for (DbAttribute pk : additionalDescriptor.getDbEntity().getPrimaryKeys()) { + String name = additionalPath.dot(pk.getName()).value(); + appendColumn(targetSource, name, prefix + name); + } + }); + // append inheritance discriminator columns... for (ObjAttribute column : descriptor.getDiscriminatorColumns()) { CayennePath target = column.getDbAttributePath(); 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 c51e8d7a3a..f6ae0e2a20 100644 --- a/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java @@ -26,6 +26,7 @@ import org.apache.cayenne.query.ColumnSelect; import org.apache.cayenne.query.EJBQLQuery; import org.apache.cayenne.query.ObjectSelect; +import org.apache.cayenne.query.PrefetchTreeNode; import org.apache.cayenne.query.SelectById; import org.apache.cayenne.runtime.CayenneRuntime; import org.apache.cayenne.test.jdbc.TableHelper; @@ -1194,4 +1195,98 @@ public void insertTwoGenericVerticalInheritanceObjects() { final List boys = ObjectSelect.query(Persistent.class, "GenBoy").select(env.context()); assertEquals(1, boys.size()); } + + private void updateFlattenedAttributeOfPrefetchedInheritedChild(int prefetchSemantics) throws SQLException { + TableHelper ivOtherTable = env.table("IV_OTHER", "ID"); + TableHelper ivBaseTable = env.table("IV_BASE", "ID", "NAME", "TYPE"); + TableHelper ivImplTable = env.table("IV_IMPL", "ID", "ATTR1", "OTHER3_ID"); + + ivOtherTable.insert(1); + ivBaseTable.insert(1, "name", "I"); + ivImplTable.insert(1, "attr1", 1); + + IvOther other = ObjectSelect.query(IvOther.class) + .prefetch(IvOther.IMPLS_WITH_INVERSE.getName(), prefetchSemantics) + .selectOne(env.context()); + + IvImpl impl = other.getImplsWithInverse().getFirst(); + assertEquals("attr1", impl.getAttr1()); + + impl.setAttr1("attr1-updated"); + env.context().commitChanges(); + + assertEquals(1, ivImplTable.getRowCount()); + ObjectContext cleanContext = runtime.newContext(); + IvImpl reread = SelectById.queryId(IvImpl.class, 1).selectOne(cleanContext); + assertEquals("attr1-updated", reread.getAttr1()); + } + + @Test + public void updateFlattenedAttributeOfInheritedChildJointPrefetch() throws SQLException { + updateFlattenedAttributeOfPrefetchedInheritedChild(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS); + } + + @Test + public void updateFlattenedAttributeOfInheritedChildDisjointPrefetch() throws SQLException { + updateFlattenedAttributeOfPrefetchedInheritedChild(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS); + } + + @Test + public void updateFlattenedAttributeOfInheritedChildDisjointByIdPrefetch() throws SQLException { + updateFlattenedAttributeOfPrefetchedInheritedChild(PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS); + } + + @Test + public void updateFlattenedAttributeOfInheritedChildRepeatedSaveResolvedFromCachedSnapshot() throws SQLException { + TableHelper ivBaseTable = env.table("IV_BASE", "ID", "NAME", "TYPE"); + TableHelper ivImplTable = env.table("IV_IMPL", "ID", "ATTR1"); + + ivBaseTable.insert(1, "name", "I"); + ivImplTable.insert(1, "attr1"); + + // First request: loads the child fresh (cold cache) + // This rebuilds the shared cached snapshot for the child. + { + ObjectContext freshContext = runtime.newContext(); + IvImpl impl = Cayenne.objectForPK(freshContext, IvImpl.class, 1); + impl.setAttr1("attr1-first"); + freshContext.commitChanges(); + } + + // Second request: (new context, shared snapshot cache) + // objectForPK is served from the cached snapshot + { + ObjectContext freshContext = runtime.newContext(); + IvImpl impl = Cayenne.objectForPK(freshContext, IvImpl.class, 1); + impl.setAttr1("attr1-second"); + freshContext.commitChanges(); + } + + assertEquals(1, ivImplTable.getRowCount()); + ObjectContext cleanContext = runtime.newContext(); + IvImpl reread = SelectById.queryId(IvImpl.class, 1).selectOne(cleanContext); + assertEquals("attr1-second", reread.getAttr1()); + } + + @Test + public void updateFlattenedAttributeOfThreeLevelInheritanceChild() throws SQLException { + TableHelper ivRootTable = env.table("IV_ROOT", "ID", "DISCRIMINATOR"); + TableHelper ivSub1Table = env.table("IV_SUB1", "ID"); + TableHelper ivSub1Sub1Table = env.table("IV_SUB1_SUB1", "ID", "SUB1_SUB1_NAME"); + + ivRootTable.insert(1, "IvSub1Sub1"); + ivSub1Table.insert(1); + ivSub1Sub1Table.insert(1, "sub1sub1name"); + + IvSub1Sub1 sub1Sub1 = SelectById.queryId(IvSub1Sub1.class, 1).selectOne(env.context()); + assertEquals("sub1sub1name", sub1Sub1.getSub1Sub1Name()); + + sub1Sub1.setSub1Sub1Name("sub1sub1name-updated"); + env.context().commitChanges(); + + assertEquals(1, ivSub1Sub1Table.getRowCount()); + ObjectContext cleanContext = runtime.newContext(); + IvSub1Sub1 reread = SelectById.queryId(IvSub1Sub1.class, 1).selectOne(cleanContext); + assertEquals("sub1sub1name-updated", reread.getSub1Sub1Name()); + } }