Repository: cayenne Updated Branches: refs/heads/master feaa5da08 -> f0b2ed009
CAY-2282 Various Update Issues With Vertical Inheritance Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/f0b2ed00 Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/f0b2ed00 Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/f0b2ed00 Branch: refs/heads/master Commit: f0b2ed009e0ffec167f002ed046bc9228e11993c Parents: feaa5da Author: Nikita Timofeev <[email protected]> Authored: Mon May 7 16:55:14 2018 +0300 Committer: Nikita Timofeev <[email protected]> Committed: Mon May 7 16:55:14 2018 +0300 ---------------------------------------------------------------------- .../cayenne/access/DataDomainDBDiffBuilder.java | 24 +- .../access/DataDomainIndirectDiffBuilder.java | 40 ++- .../cayenne/access/DataDomainInsertBucket.java | 45 +++ .../apache/cayenne/access/ObjectResolver.java | 39 ++- .../org/apache/cayenne/access/ObjectStore.java | 46 ++- .../select/DefaultSelectTranslator.java | 13 + .../apache/cayenne/reflect/ClassDescriptor.java | 12 + .../reflect/LazyClassDescriptorDecorator.java | 7 + .../cayenne/reflect/PersistentDescriptor.java | 24 ++ .../reflect/PersistentDescriptorFactory.java | 59 +++- .../org/apache/cayenne/CDOOneToOneFKIT.java | 11 + .../cayenne/access/VerticalInheritanceIT.java | 49 +-- .../VerticalInheritanceMultipleAttributes.java | 310 +++++++++++++++++++ .../resources/cayenne-inheritance-vertical.xml | 2 + .../test/resources/inheritance-vertical.map.xml | 8 +- 15 files changed, 608 insertions(+), 81 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainDBDiffBuilder.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainDBDiffBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainDBDiffBuilder.java index d1da31e..80db9c5 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainDBDiffBuilder.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainDBDiffBuilder.java @@ -24,7 +24,13 @@ import org.apache.cayenne.access.DataDomainSyncBucket.PropagatedValueFactory; import org.apache.cayenne.exp.parser.ASTDbPath; import org.apache.cayenne.graph.GraphChangeHandler; import org.apache.cayenne.graph.GraphDiff; -import org.apache.cayenne.map.*; +import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.DbJoin; +import org.apache.cayenne.map.DbRelationship; +import org.apache.cayenne.map.ObjAttribute; +import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.map.ObjRelationship; import java.util.HashMap; import java.util.Map; @@ -112,14 +118,15 @@ class DataDomainDBDiffBuilder implements GraphChangeHandler { if (relation == null) { dbRelation = dbEntity.getRelationship(arcIdString.substring(ASTDbPath.DB_PREFIX.length())); } else { - dbRelation = relation.getDbRelationships().get(0); + dbRelation = relation.getDbRelationships().get(relation.getDbRelationships().size() - 1); } // In case of a vertical inheritance, ensure that it belongs to this bucket... if (dbRelation.getSourceEntity() == dbEntity) { ObjectId targetId = (ObjectId) entry.getValue(); for (DbJoin join : dbRelation.getJoins()) { - Object value = (targetId != null) ? new PropagatedValueFactory(targetId, join.getTargetName()) + Object value = (targetId != null) + ? new PropagatedValueFactory(targetId, join.getTargetName()) : null; dbDiff.put(join.getSourceName(), value); @@ -161,9 +168,8 @@ class DataDomainDBDiffBuilder implements GraphChangeHandler { if (relationship == null) { // phantom FK if (arcIdString.startsWith(ASTDbPath.DB_PREFIX)) { - - DbRelationship dbRelationship = dbEntity.getRelationship(arcIdString.substring(ASTDbPath.DB_PREFIX - .length())); + String relName = arcIdString.substring(ASTDbPath.DB_PREFIX.length()); + DbRelationship dbRelationship = dbEntity.getRelationship(relName); if (!dbRelationship.isSourceIndependentFromTargetChange()) { doArcCreated(targetNodeId, arcId); } @@ -171,8 +177,10 @@ class DataDomainDBDiffBuilder implements GraphChangeHandler { throw new IllegalArgumentException("Bad arcId: " + arcId); } - } else if (!relationship.isSourceIndependentFromTargetChange()) { - doArcCreated(targetNodeId, arcId); + } else { + if (!relationship.isToMany() && relationship.isToPK()) { + doArcCreated(targetNodeId, arcId); + } } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainIndirectDiffBuilder.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainIndirectDiffBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainIndirectDiffBuilder.java index e7ec2d8..1c8e4d7 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainIndirectDiffBuilder.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainIndirectDiffBuilder.java @@ -27,6 +27,7 @@ import org.apache.cayenne.ObjectId; import org.apache.cayenne.graph.GraphChangeHandler; import org.apache.cayenne.graph.GraphDiff; import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.map.ObjRelationship; @@ -90,12 +91,20 @@ final class DataDomainIndirectDiffBuilder implements GraphChangeHandler { , relationship.getName(), relationship.getSourceEntity().getName()); } - // Register this combination (so we can remove it later if an insert occurs before commit) - FlattenedArcKey key = new FlattenedArcKey((ObjectId) nodeId, (ObjectId) targetNodeId, relationship); + String path = relationship.getDbRelationshipPath(); + int lastDot = path.lastIndexOf('.'); + if(lastDot > -1) { + path = path.substring(0, lastDot); + } + + if(!parent.getContext().getObjectStore().hasFlattenedPath(nodeObjectId, path)) { + // Register this combination (so we can remove it later if an insert occurs before commit) + FlattenedArcKey key = new FlattenedArcKey(nodeObjectId, (ObjectId) targetNodeId, relationship); - // If this combination has already been deleted, simply undelete it. - if (!flattenedDeletes.remove(key)) { - flattenedInserts.add(key); + // If this combination has already been deleted, simply undelete it. + if (!flattenedDeletes.remove(key)) { + flattenedInserts.add(key); + } } } } @@ -120,12 +129,23 @@ final class DataDomainIndirectDiffBuilder implements GraphChangeHandler { , relationship.getName()); } - // Register this combination (so we can remove it later if an insert occurs before commit) - FlattenedArcKey key = new FlattenedArcKey((ObjectId) nodeId, (ObjectId) targetNodeId, relationship); + // build path without last segment + StringBuilder path = new StringBuilder(); + for(int i=0; i<relationship.getDbRelationships().size() - 1; i++) { + if(path.length() > 0) { + path.append('.'); + } + path.append(relationship.getDbRelationships().get(i).getName()); + } + + if(!parent.getContext().getObjectStore().hasFlattenedPath(nodeObjectId, path.toString())) { + // Register this combination (so we can remove it later if an insert occurs before commit) + FlattenedArcKey key = new FlattenedArcKey(nodeObjectId, (ObjectId) targetNodeId, relationship); - // If this combination has already been inserted, simply "uninsert" it also do not delete it twice - if (!flattenedInserts.remove(key)) { - flattenedDeletes.add(key); + // If this combination has already been inserted, simply "uninsert" it also do not delete it twice + if (!flattenedInserts.remove(key)) { + flattenedDeletes.add(key); + } } } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainInsertBucket.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainInsertBucket.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainInsertBucket.java index 9df6f05..eee50ab 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainInsertBucket.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainInsertBucket.java @@ -21,6 +21,7 @@ package org.apache.cayenne.access; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -43,6 +44,8 @@ import org.apache.cayenne.query.Query; */ class DataDomainInsertBucket extends DataDomainSyncBucket { + List<FlattenedInsert> flattenedInserts; + DataDomainInsertBucket(DataDomainFlushAction parent) { super(parent); } @@ -83,6 +86,9 @@ class DataDomainInsertBucket extends DataDomainSyncBucket { } batch.add(snapshot, o.getObjectId()); + if(!descriptor.isMaster()) { + trackFlattenedInsert(descriptor, o); + } } } @@ -186,4 +192,43 @@ class DataDomainInsertBucket extends DataDomainSyncBucket { return false; } + + void trackFlattenedInsert(DbEntityClassDescriptor descriptor, Persistent object) { + if(flattenedInserts == null) { + flattenedInserts = new LinkedList<>(); + } + + StringBuilder sb = new StringBuilder(); + for(DbRelationship rel : descriptor.getPathFromMaster()) { + if(sb.length() > 0) { + sb.append('.'); + } + sb.append(rel.getName()); + } + + flattenedInserts.add(new FlattenedInsert(sb.toString(), object)); + } + + @Override + void postprocess() { + super.postprocess(); + if(flattenedInserts != null) { + for(FlattenedInsert insert : flattenedInserts) { + insert.register(parent.getContext().getObjectStore()); + } + } + } + + private static class FlattenedInsert { + private final String path; + private final Persistent object; + private FlattenedInsert(String path, Persistent object) { + this.path = path; + this.object = object; + } + + private void register(ObjectStore objectStore) { + objectStore.markFlattenedPath(object.getObjectId(), path); + } + } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java index dd1620b..d658abf 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java @@ -145,6 +145,9 @@ class ObjectResolver { // this will create a HOLLOW object if it is not registered yet Persistent object = context.findOrCreateObject(anId); + // resolve additional Object IDs for flattened attributes + resolveAdditionalIds(row, object, classDescriptor); + // deal with object state int state = object.getPersistenceState(); switch (state) { @@ -179,7 +182,24 @@ class ObjectResolver { return object; } - ObjEntity getEntity() { + private void resolveAdditionalIds(DataRow row, Persistent object, ClassDescriptor classDescriptor) { + if(classDescriptor.getAdditionalDbEntities().isEmpty()) { + return; + } + + for(Map.Entry<String, DbEntity> entry : classDescriptor.getAdditionalDbEntities().entrySet()) { + DbEntity dbEntity = entry.getValue(); + String path = entry.getKey(); + int lastDot = path.lastIndexOf('.'); + String prefix = lastDot == -1 ? path : path.substring(lastDot + 1); + ObjectId objectId = createObjectId(row, dbEntity.getName(), dbEntity.getPrimaryKeys(), prefix + '.', false); + if(objectId != null) { + context.getObjectStore().markFlattenedPath(object.getObjectId(), path); + } + } + } + + ObjEntity getEntity() { return descriptor.getEntity(); } @@ -196,10 +216,13 @@ class ObjectResolver { } ObjectId createObjectId(DataRow dataRow, ObjEntity objEntity, String namePrefix) { + Collection<DbAttribute> pk = objEntity == this.descriptor.getEntity() + ? this.primaryKey + : objEntity.getDbEntity().getPrimaryKeys(); + return createObjectId(dataRow, objEntity.getName(), pk, namePrefix, true); + } - Collection<DbAttribute> pk = objEntity == this.descriptor.getEntity() ? this.primaryKey : objEntity - .getDbEntity().getPrimaryKeys(); - + ObjectId createObjectId(DataRow dataRow, String name, Collection<DbAttribute> pk, String namePrefix, boolean strict) { boolean prefix = namePrefix != null && namePrefix.length() > 0; // ... handle special case - PK.size == 1 @@ -214,14 +237,14 @@ class ObjectResolver { // this is possible when processing left outer joint prefetches if (val == null) { - if(!dataRow.containsKey(key)) { + if(strict && !dataRow.containsKey(key)) { throw new CayenneRuntimeException("No PK column '%s' found in data row.", key); } return null; } // PUT without a prefix - return new ObjectId(objEntity.getName(), attribute.getName(), val); + return new ObjectId(name, attribute.getName(), val); } // ... handle generic case - PK.size > 1 @@ -235,7 +258,7 @@ class ObjectResolver { // this is possible when processing left outer joint prefetches if (val == null) { - if(!dataRow.containsKey(key)) { + if(strict && !dataRow.containsKey(key)) { throw new CayenneRuntimeException("No PK column '%s' found in data row.", key); } return null; @@ -245,7 +268,7 @@ class ObjectResolver { idMap.put(attribute.getName(), val); } - return new ObjectId(objEntity.getName(), idMap); + return new ObjectId(name, idMap); } interface DescriptorResolutionStrategy { http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java index bebada0..4bb72c2 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectStore.java @@ -52,6 +52,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * ObjectStore stores objects using their ObjectId as a key. It works as a dedicated @@ -66,6 +68,13 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa protected Map<Object, Persistent> objectMap; protected Map<Object, ObjectDiff> changes; + /** + * Map that tracks flattened paths for given object Id that is present in db. + * Presence of path in this map is used to separate insert from update case of flattened records. + * @since 4.1 + */ + protected Map<Object, Set<String>> trackedFlattenedPaths; + // a sequential id used to tag GraphDiffs so that they can later be sorted in the // original creation order int currentDiffId; @@ -293,6 +302,9 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa // remove object but not snapshot objectMap.remove(id); changes.remove(id); + if(id != null && trackedFlattenedPaths != null) { + trackedFlattenedPaths.remove(id); + } ids.add(id); object.setObjectContext(null); @@ -589,6 +601,13 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa changes.put(newId, change); } } + + if(trackedFlattenedPaths != null) { + Set<String> paths = trackedFlattenedPaths.remove(nodeId); + if(paths != null) { + trackedFlattenedPaths.put(newId, paths); + } + } } /** @@ -965,7 +984,32 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa registerLifecycleEventInducedChange(diff); } - registerDiff((ObjectId)nodeId, diff); + registerDiff(nodeId, diff); + } + + /** + * Check that flattened path for given object ID has data row in DB. + * @since 4.1 + */ + boolean hasFlattenedPath(ObjectId objectId, String path) { + if(trackedFlattenedPaths == null) { + return false; + } + return trackedFlattenedPaths + .getOrDefault(objectId, Collections.emptySet()).contains(path); + } + + /** + * Mark that flattened path for object has data row in DB. + * @since 4.1 + */ + void markFlattenedPath(ObjectId objectId, String path) { + if(trackedFlattenedPaths == null) { + trackedFlattenedPaths = new ConcurrentHashMap<>(); + } + trackedFlattenedPaths + .computeIfAbsent(objectId, o -> Collections.newSetFromMap(new ConcurrentHashMap<>())) + .add(path); } // an ObjectIdQuery optimized for retrieval of multiple snapshots - it can be reset http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java index b48329e..6cbf284 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java @@ -616,6 +616,19 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra public boolean visitToOne(ToOneProperty property) { visitRelationship(property); + + // add PKs for flattened tables in flattened path + ObjRelationship rel = property.getRelationship(); + for(int i=0; i<rel.getDbRelationships().size() - 1; i++) { + DbRelationship dbRel = rel.getDbRelationships().get(i); + dbRelationshipAdded(dbRel, JoinType.LEFT_OUTER, null); + + // append path PK attributes + for(DbAttribute dba : dbRel.getTargetEntity().getPrimaryKeys()) { + appendColumn(columns, null, dba, attributes, dbRel.getName() + '.' + dba.getName()); + } + } + return true; } http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java index 597b76e..d2e39fb 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/ClassDescriptor.java @@ -20,6 +20,7 @@ package org.apache.cayenne.reflect; import java.util.Collection; +import java.util.Map; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.map.DbEntity; @@ -52,6 +53,17 @@ public interface ClassDescriptor { Collection<DbEntity> getRootDbEntities(); /** + * Returns information about additional db entities that is used for this ObjEntity (i.e. for flattened attributes). + * <p> + * Keys are full paths for corresponding flattened attributes. + * <p> + * + * @since 4.1 + * @return information about additional db entities + */ + Map<String, DbEntity> getAdditionalDbEntities(); + + /** * @since 3.0 */ EntityInheritanceTree getEntityInheritanceTree(); http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java index 518e829..5fe618d 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/LazyClassDescriptorDecorator.java @@ -19,6 +19,7 @@ package org.apache.cayenne.reflect; import java.util.Collection; +import java.util.Map; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.map.DbEntity; @@ -88,6 +89,12 @@ public class LazyClassDescriptorDecorator implements ClassDescriptor { return descriptor.getRootDbEntities(); } + @Override + public Map<String, DbEntity> getAdditionalDbEntities() { + checkDescriptorInitialized(); + return descriptor.getAdditionalDbEntities(); + } + public EntityInheritanceTree getEntityInheritanceTree() { checkDescriptorInitialized(); return descriptor.getEntityInheritanceTree(); http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java index 548ea90..ba17201 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java @@ -60,6 +60,8 @@ public class PersistentDescriptor implements ClassDescriptor { protected ObjEntity entity; protected Collection<DbEntity> rootDbEntities; + protected Map<String, DbEntity> additionalDbEntities; + protected EntityInheritanceTree entityInheritanceTree; // combines declared and super properties @@ -118,6 +120,20 @@ public class PersistentDescriptor implements ClassDescriptor { this.rootDbEntities.add(dbEntity); } + /** + * Adds additional DbEntity for this descriptor. + * + * @param path path for entity + * @param targetEntity additional entity + */ + void addAdditionalDbEntity(String path, DbEntity targetEntity) { + if(additionalDbEntities == null) { + additionalDbEntities = new HashMap<>(); + } + + additionalDbEntities.put(path, targetEntity); + } + void sortProperties() { // ensure properties are stored in predictable order per CAY-1729 @@ -213,6 +229,14 @@ public class PersistentDescriptor implements ClassDescriptor { return rootDbEntities; } + @Override + public Map<String, DbEntity> getAdditionalDbEntities() { + if(additionalDbEntities == null) { + return Collections.emptyMap(); + } + return additionalDbEntities; + } + public boolean isFault(Object object) { if (superclassDescriptor != null) { return superclassDescriptor.isFault(object); http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptorFactory.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptorFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptorFactory.java index dbc05c7..b2e2e87 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptorFactory.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptorFactory.java @@ -19,6 +19,8 @@ package org.apache.cayenne.reflect; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; import org.apache.cayenne.CayenneRuntimeException; @@ -27,11 +29,13 @@ import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.TraversalHelper; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.EmbeddedAttribute; import org.apache.cayenne.map.EntityInheritanceTree; import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.map.ObjRelationship; +import org.apache.cayenne.util.CayenneMapEntry; /** * A convenience superclass for {@link ClassDescriptorFactory} implementors. @@ -104,8 +108,7 @@ public abstract class PersistentDescriptorFactory implements ClassDescriptorFact } } - EntityInheritanceTree inheritanceTree = descriptorMap.getResolver().getInheritanceTree( - descriptor.getEntity().getName()); + EntityInheritanceTree inheritanceTree = descriptorMap.getResolver().getInheritanceTree(descriptor.getEntity().getName()); descriptor.setEntityInheritanceTree(inheritanceTree); indexSubclassDescriptors(descriptor, inheritanceTree); indexQualifiers(descriptor, inheritanceTree); @@ -114,6 +117,7 @@ public abstract class PersistentDescriptorFactory implements ClassDescriptorFact indexRootDbEntities(descriptor, inheritanceTree); indexSuperclassProperties(descriptor); + indexAdditionalDbEntities(descriptor); descriptor.sortProperties(); @@ -290,6 +294,57 @@ public abstract class PersistentDescriptorFactory implements ClassDescriptorFact } } + protected void indexAdditionalDbEntities(final PersistentDescriptor descriptor) { + descriptor.visitProperties(new PropertyVisitor() { + @Override + public boolean visitAttribute(AttributeProperty property) { + if(!property.getAttribute().isFlattened()) { + return true; + } + + Iterator<CayenneMapEntry> it = property.getAttribute().getDbPathIterator(); + StringBuilder sb = new StringBuilder(); + while(it.hasNext()) { + CayenneMapEntry next = it.next(); + if(next instanceof DbRelationship) { + DbRelationship rel = (DbRelationship)next; + if(sb.length() > 0) { + sb.append('.'); + } + sb.append(rel.getName()); + descriptor.addAdditionalDbEntity(sb.toString(), rel.getTargetEntity()); + } + } + return true; + } + + @Override + public boolean visitToOne(ToOneProperty property) { + if(!property.getRelationship().isFlattened()) { + return true; + } + + List<DbRelationship> dbRelationships = property.getRelationship().getDbRelationships(); + StringBuilder sb = new StringBuilder(); + int count = dbRelationships.size(); + for(int i=0; i<count-1; i++) { + DbRelationship rel = dbRelationships.get(i); + if(sb.length() > 0) { + sb.append('.'); + } + sb.append(rel.getName()); + descriptor.addAdditionalDbEntity(sb.toString(), rel.getTargetEntity()); + } + return true; + } + + @Override + public boolean visitToMany(ToManyProperty property) { + return true; + } + }); + } + /** * Creates an accessor for the property. */ http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/test/java/org/apache/cayenne/CDOOneToOneFKIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/CDOOneToOneFKIT.java b/cayenne-server/src/test/java/org/apache/cayenne/CDOOneToOneFKIT.java index ea21563..8c54f69 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/CDOOneToOneFKIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/CDOOneToOneFKIT.java @@ -97,11 +97,22 @@ public class CDOOneToOneFKIT extends ServerCase { false, ObjectIdQuery.CACHE_REFRESH); ToOneFK2 src2 = (ToOneFK2) Cayenne.objectForQuery(context1, refetch); + assertNull(src2.getToOneToFK()); assertEquals(src.getObjectId(), src2.getObjectId()); // *** TESTING THIS *** src2.setToOneToFK(null); assertNull(src2.getToOneToFK()); + + context.commitChanges(); + + refetch = new ObjectIdQuery( + src.getObjectId(), + false, + ObjectIdQuery.CACHE_REFRESH); + src2 = (ToOneFK2) Cayenne.objectForQuery(context1, refetch); + assertNull(src2.getToOneToFK()); + assertEquals(src.getObjectId(), src2.getObjectId()); } @Test http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java index fb01ff2..f5da5bb 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceIT.java @@ -30,7 +30,6 @@ import org.apache.cayenne.testdo.inheritance_vertical.*; import org.apache.cayenne.unit.di.server.CayenneProjects; import org.apache.cayenne.unit.di.server.ServerCase; import org.apache.cayenne.unit.di.server.UseServerRuntime; -import org.apache.cayenne.validation.ValidationException; import org.junit.Ignore; import org.junit.Test; @@ -234,7 +233,6 @@ public class VerticalInheritanceIT extends ServerCase { /** * @link https://issues.apache.org/jira/browse/CAY-2282 */ - @Ignore("Test case for unfixed issue CAY-2282") @Test public void testUpdateRelation_Sub3() throws Exception { TableHelper ivRootTable = new TableHelper(dbHelper, "IV_ROOT"); @@ -614,7 +612,7 @@ public class VerticalInheritanceIT extends ServerCase { context.commitChanges(); } - @Test(expected = ValidationException.class) // other2 is missing now + @Test//(expected = ValidationException.class) // other2 is not mandatory for now public void testInsertWithAttributeAndRelationship() { IvOther other = context.newObject(IvOther.class); other.setName("other"); @@ -679,51 +677,6 @@ public class VerticalInheritanceIT extends ServerCase { /** * @link https://issues.apache.org/jira/browse/CAY-2282 */ - @Ignore("Test case for unfixed issue CAY-2282") - @Test - public void testUpdateTwoObjectsWithMultipleAttributeAndMultipleRelationship() 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", "ATTR2", "OTHER1_ID", "OTHER2_ID") - .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.VARCHAR, Types.INTEGER, Types.INTEGER); - - // Insert records we want to update - ivOtherTable.insert(1, "other1"); - ivOtherTable.insert(2, "other2"); - - ivBaseTable.insert(1, "Impl 1", "I"); - ivBaseTable.insert(2, "Impl 2", "I"); - - ivImplTable.insert(1, "attr1", "attr2", 1, 2); - ivImplTable.insert(2, "attr1", "attr2", 1, 2); - - // Fetch and update the records - IvOther other1 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other1")).selectOne(context); - IvOther other2 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other2")).selectOne(context); - - for(IvImpl record : ObjectSelect.query(IvImpl.class).select(context)) { - record.setName(record.getName() + "-Change"); - record.setAttr1(record.getAttr1() + "-Change"); - record.setAttr2(record.getAttr2() + "-Change"); - record.setOther1(other2); - record.setOther2(other1); - } - - context.commitChanges(); - - // todo: add some assertions after fixing commit bug above - - } - - /** - * @link https://issues.apache.org/jira/browse/CAY-2282 - */ @Test public void testUpdateWithOptimisticLocks() throws SQLException { TableHelper ivOtherTable = new TableHelper(dbHelper, "IV_OTHER"); http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceMultipleAttributes.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceMultipleAttributes.java b/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceMultipleAttributes.java new file mode 100644 index 0000000..8969f67 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/access/VerticalInheritanceMultipleAttributes.java @@ -0,0 +1,310 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ + +package org.apache.cayenne.access; + +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.query.ObjectSelect; +import org.apache.cayenne.test.jdbc.DBHelper; +import org.apache.cayenne.test.jdbc.TableHelper; +import org.apache.cayenne.testdo.inheritance_vertical.IvImpl; +import org.apache.cayenne.testdo.inheritance_vertical.IvOther; +import org.apache.cayenne.unit.di.server.CayenneProjects; +import org.apache.cayenne.unit.di.server.ServerCase; +import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @since 4.1 + */ +@UseServerRuntime(CayenneProjects.INHERITANCE_VERTICAL_PROJECT) +public class VerticalInheritanceMultipleAttributes extends ServerCase { + + @Inject + protected ObjectContext context; + + @Inject + protected DBHelper dbHelper; + + @Inject + protected ServerRuntime runtime; + + TableHelper ivOtherTable, ivBaseTable, ivImplTable; + + @Before + public void setupTableHelpers() throws Exception { + ivOtherTable = new TableHelper(dbHelper, "IV_OTHER"); + ivOtherTable.setColumns("ID", "NAME") + .setColumnTypes(Types.INTEGER, Types.VARCHAR); + + ivBaseTable = new TableHelper(dbHelper, "IV_BASE"); + ivBaseTable.setColumns("ID", "NAME", "TYPE") + .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.CHAR); + + ivImplTable = new TableHelper(dbHelper, "IV_IMPL"); + ivImplTable.setColumns("ID", "ATTR1", "ATTR2", "OTHER1_ID", "OTHER2_ID") + .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.VARCHAR, Types.INTEGER, Types.INTEGER); + + ivImplTable.deleteAll(); + ivBaseTable.deleteAll(); + ivOtherTable.deleteAll(); + } + + /** + * @link https://issues.apache.org/jira/browse/CAY-2282 + */ + @Test + public void testUpdateTwoObjects() throws SQLException { + // Insert records we want to update + ivOtherTable.insert(1, "other1"); + ivOtherTable.insert(2, "other2"); + + ivBaseTable.insert(1, "Impl 1", "I"); + ivBaseTable.insert(2, "Impl 2", "I"); + + ivImplTable.insert(1, "attr1", "attr2", 1, 2); + ivImplTable.insert(2, "attr1", "attr2", 1, 2); + + // Fetch and update the records + IvOther other1 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other1")).selectOne(context); + IvOther other2 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other2")).selectOne(context); + + List<IvImpl> implResult = ObjectSelect.query(IvImpl.class).select(context); + assertEquals(2, implResult.size()); + for(IvImpl record : implResult) { + record.setName(record.getName() + "-Change"); + record.setAttr1(record.getAttr1() + "-Change"); + record.setAttr2(record.getAttr2() + "-Change"); + record.setOther1(other2); + record.setOther2(other1); + } + + context.commitChanges(); + + // Check result via clean context + ObjectContext cleanContext = runtime.newContext(); + implResult = ObjectSelect.query(IvImpl.class).select(cleanContext); + assertEquals(2, implResult.size()); + for(IvImpl record : implResult) { + assertTrue(record.getName().endsWith("-Change")); + assertTrue(record.getAttr1().endsWith("-Change")); + assertTrue(record.getAttr2().endsWith("-Change")); + assertEquals(other2.getObjectId(), record.getOther1().getObjectId()); + assertEquals(other1.getObjectId(), record.getOther2().getObjectId()); + } + } + + @Test + public void testCreateObjectsWithData() throws SQLException { + ivOtherTable.insert(1, "other1"); + ivOtherTable.insert(2, "other2"); + + IvOther other1 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other1")).selectOne(context); + IvOther other2 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other2")).selectOne(context); + + IvImpl impl1 = context.newObject(IvImpl.class); + impl1.setName("name"); + impl1.setAttr1("attr1"); + impl1.setAttr2("attr2"); + impl1.setOther1(other1); + impl1.setOther2(other2); + + IvImpl impl2 = context.newObject(IvImpl.class); + impl2.setName("name"); + impl2.setAttr1("attr1"); + impl2.setAttr2("attr2"); + impl2.setOther1(other1); + impl2.setOther2(other2); + + context.commitChanges(); + + // Check result via clean context + ObjectContext cleanContext = runtime.newContext(); + List<IvImpl> implResult = ObjectSelect.query(IvImpl.class).select(cleanContext); + assertEquals(2, implResult.size()); + for(IvImpl record : implResult) { + assertEquals("name", record.getName()); + assertEquals("attr1", record.getAttr1()); + assertEquals("attr2", record.getAttr2()); + assertEquals(other1.getObjectId(), record.getOther1().getObjectId()); + assertEquals(other2.getObjectId(), record.getOther2().getObjectId()); + } + } + + @Test + public void testCreateEmptyObjects() throws SQLException { + IvImpl impl1 = context.newObject(IvImpl.class); + impl1.setName("name"); + + IvImpl impl2 = context.newObject(IvImpl.class); + impl2.setName("name"); + + context.commitChanges(); + + ObjectContext cleanContext = runtime.newContext(); + List<IvImpl> implResult = ObjectSelect.query(IvImpl.class).select(cleanContext); + assertEquals(2, implResult.size()); + for(IvImpl record : implResult) { + assertEquals("name", record.getName()); + assertNull(record.getAttr1()); + assertNull(record.getAttr2()); + assertNull(record.getOther1()); + assertNull(record.getOther2()); + } + } + + @Test + public void testCreateEmptyObjectsWithUpdate() throws SQLException { + ivOtherTable.insert(1, "other1"); + ivOtherTable.insert(2, "other2"); + + IvOther other1 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other1")).selectOne(context); + IvOther other2 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other2")).selectOne(context); + + IvImpl impl1 = context.newObject(IvImpl.class); + impl1.setName("name"); + + IvImpl impl2 = context.newObject(IvImpl.class); + impl2.setName("name"); + + context.commitChanges(); + + ObjectContext cleanContext = runtime.newContext(); + List<IvImpl> implResult = ObjectSelect.query(IvImpl.class).select(cleanContext); + assertEquals(2, implResult.size()); + for(IvImpl record : implResult) { + assertEquals("name", record.getName()); + assertNull(record.getAttr1()); + assertNull(record.getAttr2()); + assertNull(record.getOther1()); + assertNull(record.getOther2()); + } + + impl1.setAttr1("attr1"); + impl1.setAttr2("attr2"); + impl1.setOther1(other1); + impl1.setOther2(other2); + + impl2.setAttr1("attr1"); + impl2.setAttr2("attr2"); + impl2.setOther1(other1); + impl2.setOther2(other2); + + context.commitChanges(); + + cleanContext = runtime.newContext(); + implResult = ObjectSelect.query(IvImpl.class).select(cleanContext); + assertEquals(2, implResult.size()); + for(IvImpl record : implResult) { + assertEquals("name", record.getName()); + assertEquals("attr1", record.getAttr1()); + assertEquals("attr2", record.getAttr2()); + assertEquals(other1.getObjectId(), record.getOther1().getObjectId()); + assertEquals(other2.getObjectId(), record.getOther2().getObjectId()); + } + } + + @Test + public void testPartialCreateObjectsWithUpdate() throws SQLException { + ivOtherTable.insert(1, "other1"); + ivOtherTable.insert(2, "other2"); + + IvOther other1 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other1")).selectOne(context); + IvOther other2 = ObjectSelect.query(IvOther.class).where(IvOther.NAME.eq("other2")).selectOne(context); + + IvImpl impl1 = context.newObject(IvImpl.class); + impl1.setName("name"); + impl1.setAttr1("attr1"); + + IvImpl impl2 = context.newObject(IvImpl.class); + impl2.setName("name"); + impl2.setAttr1("attr1"); + + context.commitChanges(); + + ObjectContext cleanContext = runtime.newContext(); + List<IvImpl> implResult = ObjectSelect.query(IvImpl.class).select(cleanContext); + assertEquals(2, implResult.size()); + for(IvImpl record : implResult) { + assertEquals("name", record.getName()); + assertEquals("attr1", record.getAttr1()); + assertNull(record.getAttr2()); + assertNull(record.getOther1()); + assertNull(record.getOther2()); + } + + impl1.setAttr1("attr1"); + impl1.setAttr2("attr2"); + impl1.setOther1(other1); + impl1.setOther2(other2); + + impl2.setAttr1("attr1"); + impl2.setAttr2("attr2"); + impl2.setOther1(other1); + impl2.setOther2(other2); + + context.commitChanges(); + + cleanContext = runtime.newContext(); + implResult = ObjectSelect.query(IvImpl.class).select(cleanContext); + assertEquals(2, implResult.size()); + for(IvImpl record : implResult) { + assertEquals("name", record.getName()); + assertEquals("attr1", record.getAttr1()); + assertEquals("attr2", record.getAttr2()); + assertEquals(other1.getObjectId(), record.getOther1().getObjectId()); + assertEquals(other2.getObjectId(), record.getOther2().getObjectId()); + } + } + + @Test + public void testDeleteObjects() throws SQLException { + // Insert records we want to update + ivOtherTable.insert(1, "other1"); + ivOtherTable.insert(2, "other2"); + + ivBaseTable.insert(1, "Impl 1", "I"); + ivBaseTable.insert(2, "Impl 2", "I"); + + ivImplTable.insert(1, "attr1", "attr2", 1, 2); + ivImplTable.insert(2, "attr1", "attr2", 1, 2); + + List<IvImpl> implResult = ObjectSelect.query(IvImpl.class).select(context); + assertEquals(2, implResult.size()); + + for(IvImpl iv : implResult) { + context.deleteObject(iv); + } + + context.commitChanges(); + + assertEquals(0L, ObjectSelect.query(IvImpl.class).selectCount(context)); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/test/resources/cayenne-inheritance-vertical.xml ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/resources/cayenne-inheritance-vertical.xml b/cayenne-server/src/test/resources/cayenne-inheritance-vertical.xml index 9d8da09..3118079 100644 --- a/cayenne-server/src/test/resources/cayenne-inheritance-vertical.xml +++ b/cayenne-server/src/test/resources/cayenne-inheritance-vertical.xml @@ -1,5 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <domain xmlns="http://cayenne.apache.org/schema/10/domain" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain http://cayenne.apache.org/schema/10/domain.xsd" project-version="10"> <map name="inheritance-vertical"/> </domain> http://git-wip-us.apache.org/repos/asf/cayenne/blob/f0b2ed00/cayenne-server/src/test/resources/inheritance-vertical.map.xml ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/resources/inheritance-vertical.map.xml b/cayenne-server/src/test/resources/inheritance-vertical.map.xml index a437439..4af9df1 100644 --- a/cayenne-server/src/test/resources/inheritance-vertical.map.xml +++ b/cayenne-server/src/test/resources/inheritance-vertical.map.xml @@ -44,11 +44,11 @@ <db-attribute name="NAME" type="VARCHAR" length="100"/> </db-entity> <db-entity name="IV_IMPL"> - <db-attribute name="ATTR1" type="VARCHAR" isMandatory="true" length="100"/> - <db-attribute name="ATTR2" type="VARCHAR" isMandatory="true" length="100"/> + <db-attribute name="ATTR1" type="VARCHAR" length="100"/> + <db-attribute name="ATTR2" type="VARCHAR" length="100"/> <db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/> - <db-attribute name="OTHER1_ID" type="INTEGER" isMandatory="true"/> - <db-attribute name="OTHER2_ID" type="INTEGER" isMandatory="true"/> + <db-attribute name="OTHER1_ID" type="INTEGER"/> + <db-attribute name="OTHER2_ID" type="INTEGER"/> </db-entity> <db-entity name="IV_IMPL_WITH_LOCK"> <db-attribute name="ATTR1" type="VARCHAR" isMandatory="true" length="100"/>
