You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2018/05/07 13:55:28 UTC

cayenne git commit: CAY-2282 Various Update Issues With Vertical Inheritance

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 <st...@gmail.com>
Authored: Mon May 7 16:55:14 2018 +0300
Committer: Nikita Timofeev <st...@gmail.com>
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"/>