CAY-2777 Reverse relationship is not set with single table inheritance
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 806b26b..b3c3921 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -51,6 +51,7 @@
 CAY-2764 Split expressions do not work with DB relationships
 CAY-2765 dbimport: check excluded catalogs and schemas for the SQLServer
 CAY-2774 Overriding service ordering in DI List causes DIRuntimeException
+CAY-2777 Reverse relationship is not set with single table inheritance
 CAY-2782 Modeler: save button becomes active on DataMap comment field focus
 CAY-2783 DbEntity to ObjEntity synchronization should check mandatory flag for primitive java types
 CAY-2792 Fix Insertion Order For Reflexive DataObjects
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
index 5a673f2..3d99e79 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
@@ -41,6 +41,7 @@
 import org.apache.cayenne.access.flush.operation.DbRowOpSorter;
 import org.apache.cayenne.access.flush.operation.DbRowOp;
 import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.OpIdFactory;
 import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
 import org.apache.cayenne.graph.CompoundDiff;
 import org.apache.cayenne.graph.GraphDiff;
@@ -130,7 +131,7 @@
     protected List<DbRowOp> mergeSameObjectIds(List<DbRowOp> dbRowOps) {
         Map<ObjectId, DbRowOp> index = new HashMap<>(dbRowOps.size());
         // new EffectiveOpId()
-        dbRowOps.forEach(row -> index.merge(row.getChangeId(), row, new DbRowOpMerger()));
+        dbRowOps.forEach(row -> index.merge(OpIdFactory.idForOperation(row.getChangeId()), row, new DbRowOpMerger()));
         // reuse list
         dbRowOps.clear();
         dbRowOps.addAll(index.values());
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
index 5b4d048..458125d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
@@ -61,7 +61,7 @@
                 Object initial = value;
                 int safeguard = 0;
                 while (value instanceof Supplier && safeguard < MAX_NESTED_SUPPLIER_LEVEL) {
-                    value = ((Supplier) value).get();
+                    value = ((Supplier<?>) value).get();
                     safeguard++;
                 }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/OpIdFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/OpIdFactory.java
new file mode 100644
index 0000000..b29d9e1
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/OpIdFactory.java
@@ -0,0 +1,120 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.flush.operation;
+
+import org.apache.cayenne.ObjectId;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Factory that wraps provided ID to be suitable for the better processing in the flush operation.
+ *
+ * @since 4.2
+ */
+public class OpIdFactory {
+
+    private static final String DB_PREFIX = "db:";
+
+    static public ObjectId idForOperation(ObjectId originalId) {
+        if(originalId.isReplacementIdAttached() && originalId.getEntityName().startsWith(DB_PREFIX)) {
+            return new ReplacementAwareObjectId(originalId);
+        } else {
+            return originalId;
+        }
+    }
+
+    /**
+     * Special wrapper for the ObjectId, that uses entity name + replacement map for hashCode() and equals()
+     */
+    static class ReplacementAwareObjectId implements ObjectId {
+
+        private final ObjectId originalId;
+
+        ReplacementAwareObjectId(ObjectId originalId) {
+            this.originalId = Objects.requireNonNull(originalId);
+        }
+
+        @Override
+        public boolean isTemporary() {
+            return originalId.isTemporary();
+        }
+
+        @Override
+        public String getEntityName() {
+            return originalId.getEntityName();
+        }
+
+        @Override
+        public byte[] getKey() {
+            return originalId.getKey();
+        }
+
+        @Override
+        public Map<String, Object> getIdSnapshot() {
+            return originalId.getIdSnapshot();
+        }
+
+        @Override
+        public Map<String, Object> getReplacementIdMap() {
+            return originalId.getReplacementIdMap();
+        }
+
+        @Override
+        public ObjectId createReplacementId() {
+            return originalId.createReplacementId();
+        }
+
+        @Override
+        public boolean isReplacementIdAttached() {
+            return originalId.isReplacementIdAttached();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if(this == obj) {
+                return true;
+            }
+            if(!(obj instanceof ObjectId)) {
+                return false;
+            }
+
+            ObjectId other = (ObjectId) obj;
+            if(!other.isReplacementIdAttached()) {
+                return false;
+            }
+            if(!Objects.equals(originalId.getEntityName(), other.getEntityName())) {
+                return false;
+            }
+            return originalId.getReplacementIdMap().equals(other.getReplacementIdMap());
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 * getEntityName().hashCode() + originalId.getReplacementIdMap().hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return "OpId: " + originalId;
+        }
+    }
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/ObjRelationship.java b/cayenne-server/src/main/java/org/apache/cayenne/map/ObjRelationship.java
index ee36fbf..82f0abf 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/ObjRelationship.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/ObjRelationship.java
@@ -196,8 +196,8 @@
         ObjEntity source = getSourceEntity();
 
         for (ObjRelationship relationship : target.getRelationships()) {
-
-            if (relationship.getTargetEntity() != source) {
+            ObjEntity maybeSameSource = relationship.getTargetEntity();
+            if (maybeSameSource != source && !source.isSubentityOf(maybeSameSource)) {
                 continue;
             }
 
@@ -306,9 +306,8 @@
             return true;
         }
 
-        // entities with qualifiers may result in filtering even existing target
-        // rows, so
-        // such relationships are optional
+        // entities with qualifiers may result in filtering even existing target rows,
+        // so such relationships are optional
         if (isQualifiedEntity(getTargetEntity())) {
             return true;
         }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/ManyToManyJoinIT.java b/cayenne-server/src/test/java/org/apache/cayenne/ManyToManyJoinIT.java
index 0949b5e..efa5813 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/ManyToManyJoinIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/ManyToManyJoinIT.java
@@ -22,6 +22,8 @@
 
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.testdo.relationships_many_to_many_join.Author;
+import org.apache.cayenne.testdo.relationships_many_to_many_join.SelfRelationship;
+import org.apache.cayenne.testdo.relationships_many_to_many_join.SelfRelationshipSub;
 import org.apache.cayenne.testdo.relationships_many_to_many_join.Song;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
@@ -48,4 +50,29 @@
         assertEquals(author, song.getAuthors().iterator().next());
     }
 
+    @Test
+    public void testManyToManySelfRelationship() {
+        SelfRelationship parent1 = context.newObject(SelfRelationship.class);
+        parent1.setName("parent1");
+
+        SelfRelationshipSub child1 = context.newObject(SelfRelationshipSub.class);
+        child1.setName("child1");
+
+        SelfRelationshipSub child2 = context.newObject(SelfRelationshipSub.class);
+        child2.setName("child2");
+
+        // this sets both forward and reverse relationships
+        child2.addToSelfParents(parent1);
+
+        // this still couldn't set reverse relationship, as it present in the Subclass only
+        parent1.addToSelfChildren(child1);
+
+        context.commitChanges();
+
+        assertEquals(2, parent1.getSelfChildren().size());
+
+        assertEquals(1, child2.getSelfParents().size());
+        assertEquals(1, child1.getSelfParents().size());
+    }
+
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionTest.java
index 8f08fc0..29b89a4 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushActionTest.java
@@ -92,6 +92,35 @@
     }
 
     @Test
+    public void mergeSameObjectsId_ReplacementId() {
+        ObjectId id1  = ObjectId.of("db:test2");
+        id1.getReplacementIdMap().put("id", 1);
+        ObjectId id2  = ObjectId.of("db:test");
+        id2.getReplacementIdMap().put("id", 1);
+        ObjectId id3  = ObjectId.of("db:test");
+        id3.getReplacementIdMap().put("id", 1);
+        ObjectId id4  = ObjectId.of("db:test");
+        id4.getReplacementIdMap().put("id", 2);
+
+        DbEntity test = mockEntity("test");
+        DbEntity test2 = mockEntity("test2");
+        BaseDbRowOp[] op = new BaseDbRowOp[4];
+        op[0] = new InsertDbRowOp(mockObject(id1),  test2, id1); // +
+        op[1] = new InsertDbRowOp(mockObject(id2),  test,  id2); // -
+        op[2] = new DeleteDbRowOp(mockObject(id3),  test,  id3); // -
+        op[3] = new UpdateDbRowOp(mockObject(id4),  test,  id4); // +
+
+        DefaultDataDomainFlushAction action = mock(DefaultDataDomainFlushAction.class);
+        when(action.mergeSameObjectIds((List<DbRowOp>) any(List.class))).thenCallRealMethod();
+
+        Collection<DbRowOp> merged = action.mergeSameObjectIds(new ArrayList<>(Arrays.asList(op)));
+        assertEquals(3, merged.size());
+
+        assertThat(merged, hasItems(op[0], op[2], op[3]));
+        assertThat(merged, not(hasItem(sameInstance(op[1]))));
+    }
+
+    @Test
     public void createQueries() {
         ObjectId id1  = ObjectId.of("test",  "id", 1);
         ObjectId id2  = ObjectId.of("test",  "id", 2);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/OpIdFactoryTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/OpIdFactoryTest.java
new file mode 100644
index 0000000..a921f2e
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/operation/OpIdFactoryTest.java
@@ -0,0 +1,67 @@
+package org.apache.cayenne.access.flush.operation;
+
+import org.apache.cayenne.ObjectId;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class OpIdFactoryTest {
+
+    @Test
+    public void testEqualsAndHashCode() {
+        ObjectId idSource1 = ObjectId.of("db:test");
+        idSource1.getReplacementIdMap().put("id", 1);
+
+        ObjectId idSource2 = ObjectId.of("db:test");
+        idSource2.getReplacementIdMap().put("id", 1);
+
+        ObjectId idSource3 = ObjectId.of("db:test");
+        idSource3.getReplacementIdMap().put("id2", 1);
+
+        ObjectId idSource4 = ObjectId.of("db:test2");
+        idSource4.getReplacementIdMap().put("id", 1);
+
+        ObjectId idSource5 = ObjectId.of("db:test2");
+        idSource5.getReplacementIdMap().put("id", 1);
+
+        ObjectId idSource6 = ObjectId.of("db:test", "id", 1);
+
+        ObjectId id1 = OpIdFactory.idForOperation(idSource1);
+        ObjectId id2 = OpIdFactory.idForOperation(idSource2);
+        ObjectId id3 = OpIdFactory.idForOperation(idSource3);
+        ObjectId id4 = OpIdFactory.idForOperation(idSource4);
+        ObjectId id5 = OpIdFactory.idForOperation(idSource5);
+        ObjectId id6 = OpIdFactory.idForOperation(idSource6);
+
+        assertEquals(id1, id1);
+        assertEquals(id2, id2);
+        assertEquals(id1, id2);
+        assertEquals(id1.hashCode(), id2.hashCode());
+
+        assertEquals(id4, id4);
+        assertEquals(id5, id5);
+        assertEquals(id4, id5);
+        assertEquals(id4.hashCode(), id5.hashCode());
+
+        assertNotEquals(id1, id3);
+        assertNotEquals(id1.hashCode(), id3.hashCode());
+        assertNotEquals(id1, id4);
+        assertNotEquals(id1.hashCode(), id4.hashCode());
+        assertNotEquals(id2, id5);
+        assertNotEquals(id2.hashCode(), id5.hashCode());
+        assertNotEquals(id3, id4);
+        assertNotEquals(id3.hashCode(), id4.hashCode());
+
+        assertNotEquals(id1, id6);
+        assertNotEquals(id1.hashCode(), id6.hashCode());
+
+        assertNotSame(idSource1, id1);
+        assertNotSame(idSource2, id2);
+        assertNotSame(idSource3, id3);
+        assertNotSame(idSource4, id4);
+        assertNotSame(idSource5, id5);
+        assertSame(idSource6, id6);
+    }
+
+
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/SelfRelationship.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/SelfRelationship.java
new file mode 100644
index 0000000..91a30e2
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/SelfRelationship.java
@@ -0,0 +1,28 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.testdo.relationships_many_to_many_join;
+
+import org.apache.cayenne.testdo.relationships_many_to_many_join.auto._SelfRelationship;
+
+public class SelfRelationship extends _SelfRelationship {
+
+    private static final long serialVersionUID = 1L;
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/SelfRelationshipSub.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/SelfRelationshipSub.java
new file mode 100644
index 0000000..457a745
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/SelfRelationshipSub.java
@@ -0,0 +1,28 @@
+/*****************************************************************
+ *   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
+ *
+ *    https://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.testdo.relationships_many_to_many_join;
+
+import org.apache.cayenne.testdo.relationships_many_to_many_join.auto._SelfRelationshipSub;
+
+public class SelfRelationshipSub extends _SelfRelationshipSub {
+
+    private static final long serialVersionUID = 1L;
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_Author.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_Author.java
index 77a0f08..809495d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_Author.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_Author.java
@@ -16,7 +16,7 @@
  */
 public abstract class _Author extends BaseDataObject {
 
-    private static final long serialVersionUID = 1L; 
+    private static final long serialVersionUID = 1L;
 
     public static final String AUTHOR_ID_PK_COLUMN = "AUTHOR_ID";
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_SelfRelationship.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_SelfRelationship.java
new file mode 100644
index 0000000..35929fd
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_SelfRelationship.java
@@ -0,0 +1,132 @@
+package org.apache.cayenne.testdo.relationships_many_to_many_join.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.exp.property.ListProperty;
+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.relationships_many_to_many_join.SelfRelationshipSub;
+
+/**
+ * Class _SelfRelationship 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 _SelfRelationship extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final String SELF_ID_PK_COLUMN = "SELF_ID";
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final NumericProperty<Integer> TYPE = PropertyFactory.createNumeric("type", Integer.class);
+    public static final ListProperty<SelfRelationshipSub> SELF_CHILDREN = PropertyFactory.createList("selfChildren", SelfRelationshipSub.class);
+
+    protected String name;
+    protected int type;
+
+    protected Object selfChildren;
+
+    public void setName(String name) {
+        beforePropertyWrite("name", this.name, name);
+        this.name = name;
+    }
+
+    public String getName() {
+        beforePropertyRead("name");
+        return this.name;
+    }
+
+    public void setType(int type) {
+        beforePropertyWrite("type", this.type, type);
+        this.type = type;
+    }
+
+    public int getType() {
+        beforePropertyRead("type");
+        return this.type;
+    }
+
+    public void addToSelfChildren(SelfRelationshipSub obj) {
+        addToManyTarget("selfChildren", obj, true);
+    }
+
+    public void removeFromSelfChildren(SelfRelationshipSub obj) {
+        removeToManyTarget("selfChildren", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<SelfRelationshipSub> getSelfChildren() {
+        return (List<SelfRelationshipSub>)readProperty("selfChildren");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name":
+                return this.name;
+            case "type":
+                return this.type;
+            case "selfChildren":
+                return this.selfChildren;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "name":
+                this.name = (String)val;
+                break;
+            case "type":
+                this.type = val == null ? 0 : (int)val;
+                break;
+            case "selfChildren":
+                this.selfChildren = 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.name);
+        out.writeInt(this.type);
+        out.writeObject(this.selfChildren);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.name = (String)in.readObject();
+        this.type = in.readInt();
+        this.selfChildren = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_SelfRelationshipSub.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_SelfRelationshipSub.java
new file mode 100644
index 0000000..e35e562
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_SelfRelationshipSub.java
@@ -0,0 +1,91 @@
+package org.apache.cayenne.testdo.relationships_many_to_many_join.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.testdo.relationships_many_to_many_join.SelfRelationship;
+
+/**
+ * Class _SelfRelationshipSub 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 _SelfRelationshipSub extends SelfRelationship {
+
+    private static final long serialVersionUID = 1L;
+
+    public static final String SELF_ID_PK_COLUMN = "SELF_ID";
+
+    public static final ListProperty<SelfRelationship> SELF_PARENTS = PropertyFactory.createList("selfParents", SelfRelationship.class);
+
+
+    protected Object selfParents;
+
+    public void addToSelfParents(SelfRelationship obj) {
+        addToManyTarget("selfParents", obj, true);
+    }
+
+    public void removeFromSelfParents(SelfRelationship obj) {
+        removeToManyTarget("selfParents", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<SelfRelationship> getSelfParents() {
+        return (List<SelfRelationship>)readProperty("selfParents");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "selfParents":
+                return this.selfParents;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "selfParents":
+                this.selfParents = 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.selfParents);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.selfParents = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_Song.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_Song.java
index b2d18d9..fa99551 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_Song.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/relationships_many_to_many_join/auto/_Song.java
@@ -19,7 +19,7 @@
  */
 public abstract class _Song extends BaseDataObject {
 
-    private static final long serialVersionUID = 1L; 
+    private static final long serialVersionUID = 1L;
 
     public static final String SONG_ID_PK_COLUMN = "SONG_ID";
 
diff --git a/cayenne-server/src/test/resources/relationships-many-to-many-join.map.xml b/cayenne-server/src/test/resources/relationships-many-to-many-join.map.xml
index 4da95b3..899ebfc 100644
--- a/cayenne-server/src/test/resources/relationships-many-to-many-join.map.xml
+++ b/cayenne-server/src/test/resources/relationships-many-to-many-join.map.xml
@@ -8,6 +8,15 @@
 		<db-attribute name="AUTHOR_ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 		<db-attribute name="AUTHOR_NAME" type="VARCHAR" isMandatory="true" length="50"/>
 	</db-entity>
+	<db-entity name="X_SELF">
+		<db-attribute name="NAME" type="VARCHAR" isMandatory="true" length="255"/>
+		<db-attribute name="SELF_ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="TYPE" type="INTEGER" isMandatory="true"/>
+	</db-entity>
+	<db-entity name="X_SELF_JOIN">
+		<db-attribute name="SELF_ID_FROM" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="SELF_ID_TO" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
 	<db-entity name="X_SONG">
 		<db-attribute name="SONG_ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 		<db-attribute name="SONG_NAME" type="VARCHAR" isMandatory="true" length="50"/>
@@ -19,12 +28,32 @@
 	<obj-entity name="Author" className="org.apache.cayenne.testdo.relationships_many_to_many_join.Author" dbEntityName="X_AUTHOR">
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="AUTHOR_NAME"/>
 	</obj-entity>
+	<obj-entity name="SelfRelationship" className="org.apache.cayenne.testdo.relationships_many_to_many_join.SelfRelationship" dbEntityName="X_SELF">
+		<qualifier><![CDATA[type = 1]]></qualifier>
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+		<obj-attribute name="type" type="int" db-attribute-path="TYPE"/>
+	</obj-entity>
+	<obj-entity name="SelfRelationshipSub" superEntityName="SelfRelationship" className="org.apache.cayenne.testdo.relationships_many_to_many_join.SelfRelationshipSub">
+		<qualifier><![CDATA[type = 2]]></qualifier>
+	</obj-entity>
 	<obj-entity name="Song" className="org.apache.cayenne.testdo.relationships_many_to_many_join.Song" dbEntityName="X_SONG">
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="SONG_NAME"/>
 	</obj-entity>
 	<db-relationship name="songAuthor" source="X_AUTHOR" target="X_SONGAUTHOR" toDependentPK="true" toMany="true">
 		<db-attribute-pair source="AUTHOR_ID" target="AUTHOR_ID"/>
 	</db-relationship>
+	<db-relationship name="selfJoinFrom" source="X_SELF" target="X_SELF_JOIN" toDependentPK="true" toMany="true">
+		<db-attribute-pair source="SELF_ID" target="SELF_ID_FROM"/>
+	</db-relationship>
+	<db-relationship name="selfJoinTo" source="X_SELF" target="X_SELF_JOIN" toDependentPK="true" toMany="true">
+		<db-attribute-pair source="SELF_ID" target="SELF_ID_TO"/>
+	</db-relationship>
+	<db-relationship name="selfFrom" source="X_SELF_JOIN" target="X_SELF">
+		<db-attribute-pair source="SELF_ID_FROM" target="SELF_ID"/>
+	</db-relationship>
+	<db-relationship name="selfTo" source="X_SELF_JOIN" target="X_SELF">
+		<db-attribute-pair source="SELF_ID_TO" target="SELF_ID"/>
+	</db-relationship>
 	<db-relationship name="songAuthor" source="X_SONG" target="X_SONGAUTHOR" toDependentPK="true" toMany="true">
 		<db-attribute-pair source="SONG_ID" target="SONG_ID"/>
 	</db-relationship>
@@ -34,5 +63,7 @@
 	<db-relationship name="song" source="X_SONGAUTHOR" target="X_SONG">
 		<db-attribute-pair source="SONG_ID" target="SONG_ID"/>
 	</db-relationship>
+	<obj-relationship name="selfChildren" source="SelfRelationship" target="SelfRelationshipSub" deleteRule="Deny" db-relationship-path="selfJoinFrom.selfTo"/>
+	<obj-relationship name="selfParents" source="SelfRelationshipSub" target="SelfRelationship" deleteRule="Nullify" db-relationship-path="selfJoinTo.selfFrom"/>
 	<obj-relationship name="authors" source="Song" target="Author" collection-type="java.util.Set" deleteRule="Cascade" db-relationship-path="songAuthor.author"/>
 </data-map>