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 2019/05/11 12:02:51 UTC

[cayenne] branch master updated: CAY-2578 Wrong bindings in select of related entity by compound FK

This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git


The following commit(s) were added to refs/heads/master by this push:
     new 672b638  CAY-2578 Wrong bindings in select of related entity by compound FK
672b638 is described below

commit 672b638bdd101786f3bb4fbbebd0e6165453929d
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Sat May 11 14:42:38 2019 +0300

    CAY-2578 Wrong bindings in select of related entity by compound FK
---
 .../translator/select/QualifierTranslator.java     |  76 ++++++++-----
 .../cayenne/access/DataContextCompoundRelIT.java   |  32 ++++++
 .../cayenne/testdo/compound/CompoundOrder.java     |   9 ++
 .../cayenne/testdo/compound/CompoundOrderLine.java |   9 ++
 .../testdo/compound/CompoundOrderLineInfo.java     |   9 ++
 .../testdo/compound/auto/_CharFkTestEntity.java    |   3 -
 .../testdo/compound/auto/_CharPkTestEntity.java    |   1 -
 .../compound/auto/_CompoundFkTestEntity.java       |   3 -
 .../testdo/compound/auto/_CompoundIntPk.java       |   1 -
 .../testdo/compound/auto/_CompoundOrder.java       | 112 ++++++++++++++++++
 .../testdo/compound/auto/_CompoundOrderLine.java   | 125 +++++++++++++++++++++
 ...TestEntity.java => _CompoundOrderLineInfo.java} |  66 ++++++-----
 .../compound/auto/_CompoundPkTestEntity.java       |   1 -
 .../src/test/resources/cayenne-compound.xml        |   2 +
 cayenne-server/src/test/resources/compound.map.xml |  82 ++++++++++++--
 15 files changed, 448 insertions(+), 83 deletions(-)

diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
index 6e7c1b1..d18d5e6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
@@ -23,7 +23,7 @@ import java.util.ArrayDeque;
 import java.util.Collection;
 import java.util.Deque;
 import java.util.HashSet;
-import java.util.Iterator;
+import java.util.Map;
 import java.util.Set;
 
 import org.apache.cayenne.CayenneRuntimeException;
@@ -54,6 +54,8 @@ import org.apache.cayenne.exp.parser.PatternMatchNode;
 import org.apache.cayenne.exp.parser.SimpleNode;
 import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
 
 import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.aliased;
 import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.function;
@@ -249,30 +251,22 @@ class QualifierTranslator implements TraversalHandler {
     }
 
     private Node createMultiAttributeMatch(Expression node, Expression parentNode, PathTranslationResult result) {
-        ObjectId objectId = null;
+        DbRelationship relationship = result.getDbRelationship()
+                .orElseThrow(() -> new CayenneRuntimeException("Incorrect path '%s' translation, relationship expected"
+                        , result.getFinalPath()));
+
+        DbEntity targetEntity = relationship.getTargetEntity();
+        if(result.getDbAttributes().size() != targetEntity.getPrimaryKeys().size()) {
+            throw new CayenneRuntimeException("Unsupported or incorrect mapping for relationship '%s.%s': " +
+                    "target entity has different count of primary keys than count of joins."
+                    , relationship.getSourceEntityName(), relationship.getName());
+        }
+
         expressionsToSkip.add(node);
         expressionsToSkip.add(parentNode);
 
-        int siblings = parentNode.getOperandCount();
-        for(int i=0; i<siblings; i++) {
-            Object operand = parentNode.getOperand(i);
-            if(node == operand) {
-                continue;
-            }
-
-            if(operand instanceof Persistent) {
-                objectId = ((Persistent) operand).getObjectId();
-                break;
-            } else if(operand instanceof ObjectId) {
-                objectId = (ObjectId)operand;
-                break;
-            } else if(operand instanceof ASTObjPath) {
-                // TODO: support comparision of multi attribute ObjPath with other multi attribute ObjPath
-                throw new UnsupportedOperationException("Comparision of multiple attributes not supported for ObjPath");
-            }
-        }
-
-        if(objectId == null) {
+        Map<String, Object> valueSnapshot = getMultiAttributeValueSnapshot(node, parentNode);
+        if(valueSnapshot == null) {
             throw new CayenneRuntimeException("Multi attribute ObjPath isn't matched with valid value. " +
                     "List or Persistent object required.");
         }
@@ -280,15 +274,16 @@ class QualifierTranslator implements TraversalHandler {
         ExpressionNodeBuilder expressionNodeBuilder = null;
         ExpressionNodeBuilder eq;
 
-        Iterator<DbAttribute> pkIt = result.getDbRelationship().get().getTargetEntity().getPrimaryKeys().iterator();
-        int count = result.getDbAttributes().size();
+        String path = result.getLastAttributePath();
+        String alias = context.getTableTree().aliasForPath(path);
 
-        for(int i=0; i<count; i++) {
-            String path = result.getAttributePaths().get(i);
-            DbAttribute attribute = result.getDbAttributes().get(i);
-            DbAttribute pk = pkIt.next();
-            String alias = context.getTableTree().aliasForPath(path);
-            Object nextValue = objectId.getIdSnapshot().get(pk.getName());
+        // convert snapshot if we have attributes from source, not target
+        if(result.getLastAttribute().getEntity() == relationship.getSourceEntity()) {
+            valueSnapshot = relationship.srcFkSnapshotWithTargetSnapshot(valueSnapshot);
+        }
+
+        for(DbAttribute attribute : result.getDbAttributes()) {
+            Object nextValue = valueSnapshot.get(attribute.getName());
             eq = table(alias).column(attribute).eq(value(nextValue));
             if (expressionNodeBuilder == null) {
                 expressionNodeBuilder = eq;
@@ -300,6 +295,27 @@ class QualifierTranslator implements TraversalHandler {
         return expressionNodeBuilder.build();
     }
 
+    private Map<String, Object> getMultiAttributeValueSnapshot(Expression node, Expression parentNode) {
+        int siblings = parentNode.getOperandCount();
+        for(int i=0; i<siblings; i++) {
+            Object operand = parentNode.getOperand(i);
+            if(node == operand) {
+                continue;
+            }
+
+            if(operand instanceof Persistent) {
+                return ((Persistent) operand).getObjectId().getIdSnapshot();
+            } else if(operand instanceof ObjectId) {
+                return  ((ObjectId) operand).getIdSnapshot();
+            } else if(operand instanceof ASTObjPath) {
+                // TODO: support comparision of multi attribute ObjPath with other multi attribute ObjPath
+                throw new UnsupportedOperationException("Comparision of multiple attributes not supported for ObjPath");
+            }
+        }
+
+        return null;
+    }
+
     private boolean nodeProcessed(Expression node) {
         // must be in sync with expressionNodeToSqlNode() method
         switch (node.getType()) {
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextCompoundRelIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextCompoundRelIT.java
index c7eaa20..5073e75 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextCompoundRelIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextCompoundRelIT.java
@@ -22,8 +22,12 @@ package org.apache.cayenne.access;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.testdo.compound.CompoundFkTestEntity;
+import org.apache.cayenne.testdo.compound.CompoundOrder;
+import org.apache.cayenne.testdo.compound.CompoundOrderLine;
+import org.apache.cayenne.testdo.compound.CompoundOrderLineInfo;
 import org.apache.cayenne.testdo.compound.CompoundPkTestEntity;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
@@ -104,6 +108,34 @@ public class DataContextCompoundRelIT extends ServerCase {
     }
 
     @Test
+    public void testToOne() {
+        {
+            CompoundOrder order = context.newObject(CompoundOrder.class);
+            order.setInfo("order");
+
+            CompoundOrderLine line = context.newObject(CompoundOrderLine.class);
+            line.setOrder(order);
+            line.setLineNumber(1);
+
+            CompoundOrderLineInfo info = context.newObject(CompoundOrderLineInfo.class);
+            info.setLine(line);
+            info.setInfo("info");
+
+            context.commitChanges();
+            context.invalidateObjects(order, line, info);
+        }
+
+        {
+            CompoundOrder order = ObjectSelect.query(CompoundOrder.class).selectFirst(context1);
+            CompoundOrderLine line = order.getLines().get(0);
+            CompoundOrderLineInfo info = line.getInfo();
+
+            assertEquals(1, line.getLineNumber());
+            assertEquals("info", info.getInfo());
+        }
+    }
+
+    @Test
     public void testFetchQualifyingToMany() {
         CompoundPkTestEntity master  = context.newObject(CompoundPkTestEntity.class);
         CompoundPkTestEntity master1 = context.newObject(CompoundPkTestEntity.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrder.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrder.java
new file mode 100644
index 0000000..541a3d5
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrder.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.compound;
+
+import org.apache.cayenne.testdo.compound.auto._CompoundOrder;
+
+public class CompoundOrder extends _CompoundOrder {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrderLine.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrderLine.java
new file mode 100644
index 0000000..1b90bc4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrderLine.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.compound;
+
+import org.apache.cayenne.testdo.compound.auto._CompoundOrderLine;
+
+public class CompoundOrderLine extends _CompoundOrderLine {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrderLineInfo.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrderLineInfo.java
new file mode 100644
index 0000000..dc7ad16
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/CompoundOrderLineInfo.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.compound;
+
+import org.apache.cayenne.testdo.compound.auto._CompoundOrderLineInfo;
+
+public class CompoundOrderLineInfo extends _CompoundOrderLineInfo {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharFkTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharFkTestEntity.java
index ad8f8a8..195b054 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharFkTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharFkTestEntity.java
@@ -5,9 +5,7 @@ import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.EntityProperty;
-import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
 import org.apache.cayenne.testdo.compound.CharPkTestEntity;
@@ -22,7 +20,6 @@ public abstract class _CharFkTestEntity extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> PK_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("PK"), Integer.class);
     public static final String PK_PK_COLUMN = "PK";
 
     public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharPkTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharPkTestEntity.java
index 4a08241..70253d9 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharPkTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharPkTestEntity.java
@@ -6,7 +6,6 @@ import java.io.ObjectOutputStream;
 import java.util.List;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.ListProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundFkTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundFkTestEntity.java
index 52aef6f..3ec04a0 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundFkTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundFkTestEntity.java
@@ -5,9 +5,7 @@ import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.EntityProperty;
-import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
 import org.apache.cayenne.testdo.compound.CompoundPkTestEntity;
@@ -22,7 +20,6 @@ public abstract class _CompoundFkTestEntity extends BaseDataObject {
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> PKEY_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("PKEY"), Integer.class);
     public static final String PKEY_PK_COLUMN = "PKEY";
 
     public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundIntPk.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundIntPk.java
index a20eeff..b0bf34e 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundIntPk.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundIntPk.java
@@ -5,7 +5,6 @@ import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrder.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrder.java
new file mode 100644
index 0000000..25a1230
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrder.java
@@ -0,0 +1,112 @@
+package org.apache.cayenne.testdo.compound.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.compound.CompoundOrderLine;
+
+/**
+ * Class _CompoundOrder was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _CompoundOrder extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ORDER_NUMBER_PK_COLUMN = "order_number";
+
+    public static final StringProperty<String> INFO = PropertyFactory.createString("info", String.class);
+    public static final ListProperty<CompoundOrderLine> LINES = PropertyFactory.createList("lines", CompoundOrderLine.class);
+
+    protected String info;
+
+    protected Object lines;
+
+    public void setInfo(String info) {
+        beforePropertyWrite("info", this.info, info);
+        this.info = info;
+    }
+
+    public String getInfo() {
+        beforePropertyRead("info");
+        return this.info;
+    }
+
+    public void addToLines(CompoundOrderLine obj) {
+        addToManyTarget("lines", obj, true);
+    }
+
+    public void removeFromLines(CompoundOrderLine obj) {
+        removeToManyTarget("lines", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<CompoundOrderLine> getLines() {
+        return (List<CompoundOrderLine>)readProperty("lines");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "info":
+                return this.info;
+            case "lines":
+                return this.lines;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "info":
+                this.info = (String)val;
+                break;
+            case "lines":
+                this.lines = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(this.info);
+        out.writeObject(this.lines);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.info = (String)in.readObject();
+        this.lines = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrderLine.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrderLine.java
new file mode 100644
index 0000000..9651eb1
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrderLine.java
@@ -0,0 +1,125 @@
+package org.apache.cayenne.testdo.compound.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.CayenneDataObject;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.NumericProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.testdo.compound.CompoundOrder;
+import org.apache.cayenne.testdo.compound.CompoundOrderLineInfo;
+
+/**
+ * Class _CompoundOrderLine was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _CompoundOrderLine extends CayenneDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ORDER_NUMBER_PK_COLUMN = "order_number";
+    public static final String ORDER_LINE_NUMBER_PK_COLUMN = "order_line_number";
+
+    public static final NumericProperty<Integer> LINE_NUMBER = PropertyFactory.createNumeric("lineNumber", Integer.class);
+    public static final EntityProperty<CompoundOrder> ORDER = PropertyFactory.createEntity("order", CompoundOrder.class);
+    public static final EntityProperty<CompoundOrderLineInfo> INFO = PropertyFactory.createEntity("info", CompoundOrderLineInfo.class);
+
+    protected int lineNumber;
+
+    protected Object order;
+    protected Object info;
+
+    public void setLineNumber(int lineNumber) {
+        beforePropertyWrite("lineNumber", this.lineNumber, lineNumber);
+        this.lineNumber = lineNumber;
+    }
+
+    public int getLineNumber() {
+        beforePropertyRead("lineNumber");
+        return this.lineNumber;
+    }
+
+    public void setOrder(CompoundOrder order) {
+        setToOneTarget("order", order, true);
+    }
+
+    public CompoundOrder getOrder() {
+        return (CompoundOrder)readProperty("order");
+    }
+
+    public void setInfo(CompoundOrderLineInfo info) {
+        setToOneTarget("info", info, true);
+    }
+
+    public CompoundOrderLineInfo getInfo() {
+        return (CompoundOrderLineInfo)readProperty("info");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "lineNumber":
+                return this.lineNumber;
+            case "order":
+                return this.order;
+            case "info":
+                return this.info;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "lineNumber":
+                this.lineNumber = val == null ? 0 : (int)val;
+                break;
+            case "order":
+                this.order = val;
+                break;
+            case "info":
+                this.info = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeInt(this.lineNumber);
+        out.writeObject(this.order);
+        out.writeObject(this.info);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.lineNumber = in.readInt();
+        this.order = in.readObject();
+        this.info = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharFkTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrderLineInfo.java
similarity index 51%
copy from cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharFkTestEntity.java
copy to cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrderLineInfo.java
index ad8f8a8..427178f 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharFkTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundOrderLineInfo.java
@@ -4,50 +4,48 @@ import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 
-import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.CayenneDataObject;
 import org.apache.cayenne.exp.property.EntityProperty;
-import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
-import org.apache.cayenne.testdo.compound.CharPkTestEntity;
+import org.apache.cayenne.testdo.compound.CompoundOrderLine;
 
 /**
- * Class _CharFkTestEntity was generated by Cayenne.
+ * Class _CompoundOrderLineInfo was generated by Cayenne.
  * It is probably a good idea to avoid changing this class manually,
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
  */
-public abstract class _CharFkTestEntity extends BaseDataObject {
+public abstract class _CompoundOrderLineInfo extends CayenneDataObject {
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> PK_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("PK"), Integer.class);
-    public static final String PK_PK_COLUMN = "PK";
+    public static final String ORDER_NUMBER_PK_COLUMN = "order_number";
+    public static final String ORDER_LINE_NUMBER_PK_COLUMN = "order_line_number";
 
-    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
-    public static final EntityProperty<CharPkTestEntity> TO_CHAR_PK = PropertyFactory.createEntity("toCharPK", CharPkTestEntity.class);
+    public static final StringProperty<String> INFO = PropertyFactory.createString("info", String.class);
+    public static final EntityProperty<CompoundOrderLine> LINE = PropertyFactory.createEntity("line", CompoundOrderLine.class);
 
-    protected String name;
+    protected String info;
 
-    protected Object toCharPK;
+    protected Object line;
 
-    public void setName(String name) {
-        beforePropertyWrite("name", this.name, name);
-        this.name = name;
+    public void setInfo(String info) {
+        beforePropertyWrite("info", this.info, info);
+        this.info = info;
     }
 
-    public String getName() {
-        beforePropertyRead("name");
-        return this.name;
+    public String getInfo() {
+        beforePropertyRead("info");
+        return this.info;
     }
 
-    public void setToCharPK(CharPkTestEntity toCharPK) {
-        setToOneTarget("toCharPK", toCharPK, true);
+    public void setLine(CompoundOrderLine line) {
+        setToOneTarget("line", line, true);
     }
 
-    public CharPkTestEntity getToCharPK() {
-        return (CharPkTestEntity)readProperty("toCharPK");
+    public CompoundOrderLine getLine() {
+        return (CompoundOrderLine)readProperty("line");
     }
 
     @Override
@@ -57,10 +55,10 @@ public abstract class _CharFkTestEntity extends BaseDataObject {
         }
 
         switch(propName) {
-            case "name":
-                return this.name;
-            case "toCharPK":
-                return this.toCharPK;
+            case "info":
+                return this.info;
+            case "line":
+                return this.line;
             default:
                 return super.readPropertyDirectly(propName);
         }
@@ -73,11 +71,11 @@ public abstract class _CharFkTestEntity extends BaseDataObject {
         }
 
         switch (propName) {
-            case "name":
-                this.name = (String)val;
+            case "info":
+                this.info = (String)val;
                 break;
-            case "toCharPK":
-                this.toCharPK = val;
+            case "line":
+                this.line = val;
                 break;
             default:
                 super.writePropertyDirectly(propName, val);
@@ -95,15 +93,15 @@ public abstract class _CharFkTestEntity extends BaseDataObject {
     @Override
     protected void writeState(ObjectOutputStream out) throws IOException {
         super.writeState(out);
-        out.writeObject(this.name);
-        out.writeObject(this.toCharPK);
+        out.writeObject(this.info);
+        out.writeObject(this.line);
     }
 
     @Override
     protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
         super.readState(in);
-        this.name = (String)in.readObject();
-        this.toCharPK = in.readObject();
+        this.info = (String)in.readObject();
+        this.line = in.readObject();
     }
 
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundPkTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundPkTestEntity.java
index a5d775a..80f3937 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundPkTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/compound/auto/_CompoundPkTestEntity.java
@@ -6,7 +6,6 @@ import java.io.ObjectOutputStream;
 import java.util.List;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.ListProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
diff --git a/cayenne-server/src/test/resources/cayenne-compound.xml b/cayenne-server/src/test/resources/cayenne-compound.xml
index 1445b53..c5c8c33 100644
--- a/cayenne-server/src/test/resources/cayenne-compound.xml
+++ b/cayenne-server/src/test/resources/cayenne-compound.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 https://cayenne.apache.org/schema/10/domain.xsd"
 	 project-version="10">
 	<map name="compound"/>
 </domain>
diff --git a/cayenne-server/src/test/resources/compound.map.xml b/cayenne-server/src/test/resources/compound.map.xml
index ab2e0fe..da8a373 100644
--- a/cayenne-server/src/test/resources/compound.map.xml
+++ b/cayenne-server/src/test/resources/compound.map.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap http://cayenne.apache.org/schema/10/modelMap.xsd"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd"
 	 project-version="10">
 	<property name="defaultPackage" value="org.apache.cayenne.testdo.compound"/>
 	<property name="defaultSuperclass" value="org.apache.cayenne.CayenneDataObject"/>
@@ -23,15 +23,28 @@
 		<db-attribute name="NAME" type="VARCHAR" length="255"/>
 		<db-attribute name="PKEY" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
+	<db-entity name="COMPOUND_INT_PK">
+		<db-attribute name="id1" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="id2" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="name" type="VARCHAR" length="100"/>
+	</db-entity>
 	<db-entity name="COMPOUND_PK_TEST">
 		<db-attribute name="KEY1" type="VARCHAR" isPrimaryKey="true" isMandatory="true" length="20"/>
 		<db-attribute name="KEY2" type="VARCHAR" isPrimaryKey="true" isMandatory="true" length="20"/>
 		<db-attribute name="NAME" type="VARCHAR" length="255"/>
 	</db-entity>
-	<db-entity name="COMPOUND_INT_PK">
-		<db-attribute name="id1" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
-		<db-attribute name="id2" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
-		<db-attribute name="name" type="VARCHAR" length="100"/>
+	<db-entity name="compound_order">
+		<db-attribute name="info" type="VARCHAR" length="255"/>
+		<db-attribute name="order_number" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
+	<db-entity name="compound_order_line">
+		<db-attribute name="order_line_number" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="order_number" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
+	<db-entity name="compound_order_line_info">
+		<db-attribute name="info" type="VARCHAR" length="255"/>
+		<db-attribute name="order_line_number" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="order_number" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
 	<obj-entity name="CharFkTestEntity" className="org.apache.cayenne.testdo.compound.CharFkTestEntity" dbEntityName="CHAR_FK_TEST">
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
@@ -43,16 +56,25 @@
 	<obj-entity name="CompoundFkTestEntity" className="org.apache.cayenne.testdo.compound.CompoundFkTestEntity" dbEntityName="COMPOUND_FK_TEST">
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
 	</obj-entity>
-	<obj-entity name="CompoundPkTestEntity" className="org.apache.cayenne.testdo.compound.CompoundPkTestEntity" dbEntityName="COMPOUND_PK_TEST">
-		<obj-attribute name="key1" type="java.lang.String" db-attribute-path="KEY1"/>
-		<obj-attribute name="key2" type="java.lang.String" db-attribute-path="KEY2"/>
-		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
-	</obj-entity>
 	<obj-entity name="CompoundIntPk" className="org.apache.cayenne.testdo.compound.CompoundIntPk" dbEntityName="COMPOUND_INT_PK">
 		<obj-attribute name="id1" type="java.lang.Integer" db-attribute-path="id1"/>
 		<obj-attribute name="id2" type="java.lang.Integer" db-attribute-path="id2"/>
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="name"/>
 	</obj-entity>
+	<obj-entity name="CompoundOrder" className="org.apache.cayenne.testdo.compound.CompoundOrder" clientClassName="test.client.CompoundOrder" dbEntityName="compound_order" superClassName="org.apache.cayenne.CayenneDataObject" clientSuperClassName="org.apache.cayenne.PersistentObject">
+		<obj-attribute name="info" type="java.lang.String" db-attribute-path="info"/>
+	</obj-entity>
+	<obj-entity name="CompoundOrderLine" className="org.apache.cayenne.testdo.compound.CompoundOrderLine" clientClassName="test.client.CompoundOrderLine" dbEntityName="compound_order_line" superClassName="org.apache.cayenne.CayenneDataObject" clientSuperClassName="org.apache.cayenne.PersistentObject">
+		<obj-attribute name="lineNumber" type="int" db-attribute-path="order_line_number"/>
+	</obj-entity>
+	<obj-entity name="CompoundOrderLineInfo" className="org.apache.cayenne.testdo.compound.CompoundOrderLineInfo" clientClassName="test.client.CompoundOrderLineInfo" dbEntityName="compound_order_line_info" superClassName="org.apache.cayenne.CayenneDataObject" clientSuperClassName="org.apache.cayenne.PersistentObject">
+		<obj-attribute name="info" type="java.lang.String" db-attribute-path="info"/>
+	</obj-entity>
+	<obj-entity name="CompoundPkTestEntity" className="org.apache.cayenne.testdo.compound.CompoundPkTestEntity" dbEntityName="COMPOUND_PK_TEST">
+		<obj-attribute name="key1" type="java.lang.String" db-attribute-path="KEY1"/>
+		<obj-attribute name="key2" type="java.lang.String" db-attribute-path="KEY2"/>
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+	</obj-entity>
 	<db-relationship name="toCharPK" source="CHAR_FK_TEST" target="CHAR_PK_TEST">
 		<db-attribute-pair source="FK_COL" target="PK_COL"/>
 	</db-relationship>
@@ -67,8 +89,48 @@
 		<db-attribute-pair source="KEY1" target="F_KEY1"/>
 		<db-attribute-pair source="KEY2" target="F_KEY2"/>
 	</db-relationship>
+	<db-relationship name="lines" source="compound_order" target="compound_order_line" toMany="true">
+		<db-attribute-pair source="order_number" target="order_number"/>
+	</db-relationship>
+	<db-relationship name="order" source="compound_order_line" target="compound_order">
+		<db-attribute-pair source="order_number" target="order_number"/>
+	</db-relationship>
+	<db-relationship name="info" source="compound_order_line" target="compound_order_line_info" toDependentPK="true">
+		<db-attribute-pair source="order_number" target="order_number"/>
+		<db-attribute-pair source="order_line_number" target="order_line_number"/>
+	</db-relationship>
+	<db-relationship name="line" source="compound_order_line_info" target="compound_order_line">
+		<db-attribute-pair source="order_number" target="order_number"/>
+		<db-attribute-pair source="order_line_number" target="order_line_number"/>
+	</db-relationship>
 	<obj-relationship name="toCharPK" source="CharFkTestEntity" target="CharPkTestEntity" db-relationship-path="toCharPK"/>
 	<obj-relationship name="charFKs" source="CharPkTestEntity" target="CharFkTestEntity" db-relationship-path="charFKs"/>
 	<obj-relationship name="toCompoundPk" source="CompoundFkTestEntity" target="CompoundPkTestEntity" db-relationship-path="toCompoundPk"/>
+	<obj-relationship name="lines" source="CompoundOrder" target="CompoundOrderLine" deleteRule="Deny" db-relationship-path="lines"/>
+	<obj-relationship name="order" source="CompoundOrderLine" target="CompoundOrder" deleteRule="Nullify" db-relationship-path="order"/>
+	<obj-relationship name="info" source="CompoundOrderLine" target="CompoundOrderLineInfo" deleteRule="Nullify" db-relationship-path="info"/>
+	<obj-relationship name="line" source="CompoundOrderLineInfo" target="CompoundOrderLine" deleteRule="Nullify" db-relationship-path="line"/>
 	<obj-relationship name="compoundFkArray" source="CompoundPkTestEntity" target="CompoundFkTestEntity" db-relationship-path="compoundFkArray"/>
+	<dbImport xmlns="http://cayenne.apache.org/schema/10/dbimport">
+		<forceDataMapCatalog>false</forceDataMapCatalog>
+		<forceDataMapSchema>false</forceDataMapSchema>
+		<namingStrategy>org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator</namingStrategy>
+		<skipPrimaryKeyLoading>false</skipPrimaryKeyLoading>
+		<skipRelationshipsLoading>false</skipRelationshipsLoading>
+		<useJava7Types>false</useJava7Types>
+		<usePrimitives>true</usePrimitives>
+	</dbImport>
+	<cgen xmlns="http://cayenne.apache.org/schema/10/cgen">
+		<destDir>../java</destDir>
+		<mode>entity</mode>
+		<template>templates/v4_1/subclass.vm</template>
+		<superTemplate>templates/v4_1/superclass.vm</superTemplate>
+		<outputPattern>*.java</outputPattern>
+		<makePairs>true</makePairs>
+		<usePkgPath>true</usePkgPath>
+		<overwrite>false</overwrite>
+		<createPropertyNames>false</createPropertyNames>
+		<createPKProperties>false</createPKProperties>
+		<client>false</client>
+	</cgen>
 </data-map>