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/03/07 10:53:56 UTC

[cayenne] 01/04: CAY-2521 Expression without Object ID disjoint issue

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

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

commit eedc9adef5e6048303131d07c4300bc8999edb26
Author: Arseni Bulatski <an...@gmail.com>
AuthorDate: Fri Feb 8 17:50:31 2019 +0300

    CAY-2521 Expression without Object ID disjoint issue
---
 RELEASE-NOTES.txt                                  |   1 +
 .../translator/select/QueryAssemblerHelper.java    |  24 ++--
 .../java/org/apache/cayenne/query/CAY_2521IT.java  | 141 ++++++++++++++++++++
 .../org/apache/cayenne/testdo/cay_2521/Issue.java  |  28 ++++
 .../apache/cayenne/testdo/cay_2521/Location.java   |  28 ++++
 .../org/apache/cayenne/testdo/cay_2521/Team.java   |  28 ++++
 .../cayenne/testdo/cay_2521/auto/_Issue.java       | 103 +++++++++++++++
 .../cayenne/testdo/cay_2521/auto/_Location.java    | 145 +++++++++++++++++++++
 .../apache/cayenne/testdo/cay_2521/auto/_Team.java | 131 +++++++++++++++++++
 .../cayenne/unit/di/server/CayenneProjects.java    |   1 +
 .../cayenne/unit/di/server/SchemaBuilder.java      |  32 ++---
 cayenne-server/src/test/resources/cay-2521.map.xml |  80 ++++++++++++
 .../src/test/resources/cayenne-cay-2521.xml        |   7 +
 13 files changed, 725 insertions(+), 24 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 115feb7..a463065 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -57,6 +57,7 @@ CAY-2501 Modeler: DbImport ui not loading columns for MySQL connector v8.0
 CAY-2502 DataMap in DataNode tree view disappears after dbImport
 CAY-2504 Broken detection logic of NoopEventBridge in DataRowStoreFactory
 CAY-2505 EventBridge providers should be bound without scope
+CAY-2521 Expression without Object ID disjoint issue
 
 ----------------------------------
 Release: 4.1.M2
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
index fcdad65..6f624ba 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java
@@ -19,6 +19,12 @@
 
 package org.apache.cayenne.access.translator.select;
 
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
@@ -26,15 +32,17 @@ import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.parser.PatternMatchNode;
 import org.apache.cayenne.exp.parser.SimpleNode;
-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.JoinType;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.map.PathComponent;
 import org.apache.cayenne.util.CayenneMapEntry;
 
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
 /**
  * Translates parts of the query to SQL. Always works in the context of parent
  * Translator.
@@ -444,7 +452,7 @@ public abstract class QueryAssemblerHelper {
 	 */
 	protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) {
 
-		if (forceJoinForRelations || rel.isToMany()) {
+		if (forceJoinForRelations || rel.isToMany() || !rel.isToPK()) {
 			// append joins
 			queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
 		}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/CAY_2521IT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/CAY_2521IT.java
new file mode 100644
index 0000000..9bab5e4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/CAY_2521IT.java
@@ -0,0 +1,141 @@
+/*****************************************************************
+ *   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.query;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.List;
+
+import org.apache.cayenne.Cayenne;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.cay_2521.Issue;
+import org.apache.cayenne.testdo.cay_2521.Location;
+import org.apache.cayenne.testdo.cay_2521.Team;
+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.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@UseServerRuntime(CayenneProjects.CAY_2521)
+public class CAY_2521IT extends ServerCase {
+
+    @Inject
+    private ObjectContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    private TableHelper tTeam;
+    private TableHelper tIssue;
+    private TableHelper tLocation;
+
+    @Before
+    public void before() {
+        this.tTeam = new TableHelper(dbHelper, "TEAM1").setColumns("home_location_id", "id")
+                .setColumnTypes(Types.INTEGER, Types.INTEGER);
+        this.tIssue = new TableHelper(dbHelper, "ISSUE").setColumns("home_team_id", "id", "location_id")
+                .setColumnTypes(Types.INTEGER, Types.INTEGER, Types.INTEGER);
+        this.tLocation = new TableHelper(dbHelper, "LOCATION").setColumns("id", "name", "team_id")
+                .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.INTEGER);
+    }
+
+    @After
+    public void after() throws SQLException {
+        this.tIssue.deleteAll();
+        this.tTeam.deleteAll();
+        this.tLocation.deleteAll();
+    }
+
+    private void createDataSet() throws SQLException {
+        tLocation.insert(71, "Test", null);
+        tTeam.insert(71, 8);
+        tIssue.insert(8, 100, 71);
+    }
+
+    @Test
+    public void testCay_2521() throws SQLException {
+        createDataSet();
+
+        List<Issue> result = ObjectSelect.query(Issue.class)
+                .where(ExpressionFactory.exp("homeTeam = 8"))
+                .prefetch(Issue.HOME_TEAM.disjoint())
+                .prefetch(Issue.HOME_TEAM.dot(Team.HOME_LOCATION).disjoint())
+                .select(context);
+        assertEquals(1, result.size());
+        assertNotNull(result.get(0).getHomeTeam().getHomeLocation());
+    }
+
+    @Test
+    public void testCay_2521_ObjId() throws SQLException {
+        createDataSet();
+
+        Team team = Cayenne.objectForPK(context, Team.class, 8);
+        List<Issue> result = ObjectSelect.query(Issue.class)
+                .where(ExpressionFactory.exp("homeTeam = $id" , (Object) team.getObjectId()))
+                .prefetch(Issue.HOME_TEAM.disjoint())
+                .prefetch(Issue.HOME_TEAM.dot(Team.HOME_LOCATION).disjoint())
+                .select(context);
+        assertEquals(1, result.size());
+        assertNotNull(result.get(0).getHomeTeam().getHomeLocation());
+    }
+
+    @Test
+    public void testColumnQuery() throws SQLException {
+        createDataSet();
+
+        List<Location> result = ObjectSelect
+                .columnQuery(Issue.class, Issue.HOME_TEAM.dot(Team.HOME_LOCATION))
+                .select(context);
+        assertEquals(1, result.size());
+        assertEquals("Test", result.get(0).getName());
+    }
+
+    @Test
+    public void testWithJoin() throws SQLException {
+        createDataSet();
+
+        List<Location> locations = ObjectSelect.query(Location.class)
+                .prefetch(Location.HOME_TEAM.disjoint())
+                .select(context);
+        assertEquals(1, locations.size());
+        assertEquals("Test", locations.get(0).getName());
+        assertNotNull(locations.get(0).getHomeTeam());
+    }
+
+    @Test
+    public void testWithoutJoin() throws SQLException {
+        createDataSet();
+
+        List<Team> teams = ObjectSelect.query(Team.class)
+                .prefetch(Team.HOME_LOCATION.disjoint())
+                .select(context);
+        assertEquals(1, teams.size());
+        assertNotNull(teams.get(0).getHomeLocation());
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Issue.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Issue.java
new file mode 100644
index 0000000..e517bbf
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Issue.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
+ *
+ *    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.testdo.cay_2521;
+
+import org.apache.cayenne.testdo.cay_2521.auto._Issue;
+
+public class Issue extends _Issue {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Location.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Location.java
new file mode 100644
index 0000000..38a6165
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Location.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
+ *
+ *    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.testdo.cay_2521;
+
+import org.apache.cayenne.testdo.cay_2521.auto._Location;
+
+public class Location extends _Location {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Team.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Team.java
new file mode 100644
index 0000000..75fe28a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/Team.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
+ *
+ *    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.testdo.cay_2521;
+
+import org.apache.cayenne.testdo.cay_2521.auto._Team;
+
+public class Team extends _Team {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Issue.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Issue.java
new file mode 100644
index 0000000..118fbb0
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Issue.java
@@ -0,0 +1,103 @@
+package org.apache.cayenne.testdo.cay_2521.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.testdo.cay_2521.Location;
+import org.apache.cayenne.testdo.cay_2521.Team;
+
+/**
+ * Class _Issue 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 _Issue extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "id";
+
+    public static final Property<Location> LOCATION = Property.create("location", Location.class);
+    public static final Property<Team> HOME_TEAM = Property.create("homeTeam", Team.class);
+
+
+    protected Object location;
+    protected Object homeTeam;
+
+    public void setLocation(Location location) {
+        setToOneTarget("location", location, true);
+    }
+
+    public Location getLocation() {
+        return (Location)readProperty("location");
+    }
+
+    public void setHomeTeam(Team homeTeam) {
+        setToOneTarget("homeTeam", homeTeam, true);
+    }
+
+    public Team getHomeTeam() {
+        return (Team)readProperty("homeTeam");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "location":
+                return this.location;
+            case "homeTeam":
+                return this.homeTeam;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "location":
+                this.location = val;
+                break;
+            case "homeTeam":
+                this.homeTeam = 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.location);
+        out.writeObject(this.homeTeam);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.location = in.readObject();
+        this.homeTeam = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Location.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Location.java
new file mode 100644
index 0000000..e9f5043
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Location.java
@@ -0,0 +1,145 @@
+package org.apache.cayenne.testdo.cay_2521.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;
+import org.apache.cayenne.testdo.cay_2521.Issue;
+import org.apache.cayenne.testdo.cay_2521.Team;
+
+/**
+ * Class _Location 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 _Location extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "id";
+
+    public static final Property<String> NAME = Property.create("name", String.class);
+    public static final Property<List<Issue>> ISSUES = Property.create("issues", List.class);
+    public static final Property<Team> TEAM = Property.create("team", Team.class);
+    public static final Property<Team> HOME_TEAM = Property.create("homeTeam", Team.class);
+
+    protected String name;
+
+    protected Object issues;
+    protected Object team;
+    protected Object homeTeam;
+
+    public void setName(String name) {
+        beforePropertyWrite("name", this.name, name);
+        this.name = name;
+    }
+
+    public String getName() {
+        beforePropertyRead("name");
+        return this.name;
+    }
+
+    public void addToIssues(Issue obj) {
+        addToManyTarget("issues", obj, true);
+    }
+
+    public void removeFromIssues(Issue obj) {
+        removeToManyTarget("issues", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<Issue> getIssues() {
+        return (List<Issue>)readProperty("issues");
+    }
+
+    public void setTeam(Team team) {
+        setToOneTarget("team", team, true);
+    }
+
+    public Team getTeam() {
+        return (Team)readProperty("team");
+    }
+
+    public void setHomeTeam(Team homeTeam) {
+        setToOneTarget("homeTeam", homeTeam, true);
+    }
+
+    public Team getHomeTeam() {
+        return (Team)readProperty("homeTeam");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name":
+                return this.name;
+            case "issues":
+                return this.issues;
+            case "team":
+                return this.team;
+            case "homeTeam":
+                return this.homeTeam;
+            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 "issues":
+                this.issues = val;
+                break;
+            case "team":
+                this.team = val;
+                break;
+            case "homeTeam":
+                this.homeTeam = 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.writeObject(this.issues);
+        out.writeObject(this.team);
+        out.writeObject(this.homeTeam);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.name = (String)in.readObject();
+        this.issues = in.readObject();
+        this.team = in.readObject();
+        this.homeTeam = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Team.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Team.java
new file mode 100644
index 0000000..bd6523c
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2521/auto/_Team.java
@@ -0,0 +1,131 @@
+package org.apache.cayenne.testdo.cay_2521.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;
+import org.apache.cayenne.testdo.cay_2521.Issue;
+import org.apache.cayenne.testdo.cay_2521.Location;
+
+/**
+ * Class _Team 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 _Team extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "id";
+
+    public static final Property<List<Location>> LOCATIONS = Property.create("locations", List.class);
+    public static final Property<Location> HOME_LOCATION = Property.create("homeLocation", Location.class);
+    public static final Property<List<Issue>> ISSUES = Property.create("issues", List.class);
+
+
+    protected Object locations;
+    protected Object homeLocation;
+    protected Object issues;
+
+    public void addToLocations(Location obj) {
+        addToManyTarget("locations", obj, true);
+    }
+
+    public void removeFromLocations(Location obj) {
+        removeToManyTarget("locations", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<Location> getLocations() {
+        return (List<Location>)readProperty("locations");
+    }
+
+    public void setHomeLocation(Location homeLocation) {
+        setToOneTarget("homeLocation", homeLocation, true);
+    }
+
+    public Location getHomeLocation() {
+        return (Location)readProperty("homeLocation");
+    }
+
+    public void addToIssues(Issue obj) {
+        addToManyTarget("issues", obj, true);
+    }
+
+    public void removeFromIssues(Issue obj) {
+        removeToManyTarget("issues", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<Issue> getIssues() {
+        return (List<Issue>)readProperty("issues");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "locations":
+                return this.locations;
+            case "homeLocation":
+                return this.homeLocation;
+            case "issues":
+                return this.issues;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "locations":
+                this.locations = val;
+                break;
+            case "homeLocation":
+                this.homeLocation = val;
+                break;
+            case "issues":
+                this.issues = 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.locations);
+        out.writeObject(this.homeLocation);
+        out.writeObject(this.issues);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.locations = in.readObject();
+        this.homeLocation = in.readObject();
+        this.issues = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
index 8d58595..fd12258 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
@@ -83,4 +83,5 @@ public class CayenneProjects {
     public static final String HYBRID_DATA_OBJECT_PROJECT = "cayenne-hybrid-data-object.xml";
     public static final String JAVA8 = "cayenne-java8.xml";
     public static final String INHERITANCE_WITH_ENUM_PROJECT = "cayenne-inheritance-with-enum.xml";
+    public static final String CAY_2521 = "cayenne-cay-2521.xml";
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
index d74cffa..9017e99 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
@@ -19,6 +19,21 @@
 
 package org.apache.cayenne.unit.di.server;
 
+import java.net.URL;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.TreeMap;
+
 import org.apache.cayenne.access.DataDomain;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.DbGenerator;
@@ -43,21 +58,6 @@ import org.apache.cayenne.unit.UnitDbAdapter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.net.URL;
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.Types;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.TreeMap;
-
 /**
  * Default implementation of the AccessStack that has a single DataNode per DataMap.
  */
@@ -82,7 +82,7 @@ public class SchemaBuilder {
 			"qualified.map.xml", "quoted-identifiers.map.xml", "inheritance-single-table1.map.xml",
 			"inheritance-vertical.map.xml", "oneway-rels.map.xml", "unsupported-distinct-types.map.xml",
 			"array-type.map.xml", "cay-2032.map.xml", "weighted-sort.map.xml", "hybrid-data-object.map.xml",
-			"java8.map.xml", "inheritance-with-enum.map.xml" };
+			"java8.map.xml", "cay-2521.map.xml", "inheritance-with-enum.map.xml" };
 
 	// hardcoded dependent entities that should be excluded
 	// if LOBs are not supported
diff --git a/cayenne-server/src/test/resources/cay-2521.map.xml b/cayenne-server/src/test/resources/cay-2521.map.xml
new file mode 100644
index 0000000..5cf7a82
--- /dev/null
+++ b/cayenne-server/src/test/resources/cay-2521.map.xml
@@ -0,0 +1,80 @@
+<?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 https://cayenne.apache.org/schema/10/modelMap.xsd"
+	 project-version="10">
+	<property name="defaultPackage" value="org.apache.cayenne.testdo.cay_2521"/>
+	<db-entity name="ISSUE">
+		<db-attribute name="home_team_id" type="INTEGER" length="10"/>
+		<db-attribute name="id" type="INTEGER" isPrimaryKey="true" isGenerated="true" isMandatory="true" length="10"/>
+		<db-attribute name="location_id" type="INTEGER" length="10"/>
+	</db-entity>
+	<db-entity name="LOCATION">
+		<db-attribute name="id" type="INTEGER" isPrimaryKey="true" isGenerated="true" isMandatory="true" length="10"/>
+		<db-attribute name="name" type="VARCHAR" length="45"/>
+		<db-attribute name="team_id" type="INTEGER" length="10"/>
+	</db-entity>
+	<db-entity name="TEAM1">
+		<db-attribute name="home_location_id" type="INTEGER" length="10"/>
+		<db-attribute name="id" type="INTEGER" isPrimaryKey="true" isMandatory="true" length="10"/>
+	</db-entity>
+	<obj-entity name="Issue" className="org.apache.cayenne.testdo.cay_2521.Issue" dbEntityName="ISSUE"/>
+	<obj-entity name="Location" className="org.apache.cayenne.testdo.cay_2521.Location" dbEntityName="LOCATION">
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="name"/>
+	</obj-entity>
+	<obj-entity name="Team" className="org.apache.cayenne.testdo.cay_2521.Team" dbEntityName="TEAM1"/>
+	<db-relationship name="HOME_TEAM" source="ISSUE" target="TEAM1">
+		<db-attribute-pair source="home_team_id" target="id"/>
+	</db-relationship>
+	<db-relationship name="LOCATION" source="ISSUE" target="LOCATION">
+		<db-attribute-pair source="location_id" target="id"/>
+	</db-relationship>
+	<db-relationship name="ISSUES" source="LOCATION" target="ISSUE" toMany="true">
+		<db-attribute-pair source="id" target="location_id"/>
+	</db-relationship>
+	<db-relationship name="TEAM" source="LOCATION" target="TEAM1">
+		<db-attribute-pair source="team_id" target="id"/>
+	</db-relationship>
+	<db-relationship name="HOME_TEAM" source="LOCATION" target="TEAM1">
+		<db-attribute-pair source="id" target="home_location_id"/>
+	</db-relationship>
+	<db-relationship name="ISSUES" source="TEAM1" target="ISSUE" toMany="true">
+		<db-attribute-pair source="id" target="home_team_id"/>
+	</db-relationship>
+	<db-relationship name="LOCATIONS" source="TEAM1" target="LOCATION" toMany="true">
+		<db-attribute-pair source="id" target="team_id"/>
+	</db-relationship>
+	<db-relationship name="HOME_LOCATION" source="TEAM1" target="LOCATION">
+		<db-attribute-pair source="home_location_id" target="id"/>
+	</db-relationship>
+	<obj-relationship name="location" source="Issue" target="Location" deleteRule="Cascade" db-relationship-path="LOCATION"/>
+	<obj-relationship name="homeTeam" source="Issue" target="Team" deleteRule="Cascade" db-relationship-path="HOME_TEAM"/>
+	<obj-relationship name="issues" source="Location" target="Issue" deleteRule="Cascade" db-relationship-path="ISSUES"/>
+	<obj-relationship name="team" source="Location" target="Team" deleteRule="Cascade" db-relationship-path="TEAM"/>
+	<obj-relationship name="homeTeam" source="Location" target="Team" deleteRule="Cascade" db-relationship-path="HOME_TEAM"/>
+	<obj-relationship name="locations" source="Team" target="Location" deleteRule="Cascade" db-relationship-path="LOCATIONS"/>
+	<obj-relationship name="homeLocation" source="Team" target="Location" deleteRule="Cascade" db-relationship-path="HOME_LOCATION"/>
+	<obj-relationship name="issues" source="Team" target="Issue" deleteRule="Cascade" db-relationship-path="ISSUES"/>
+	<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>
diff --git a/cayenne-server/src/test/resources/cayenne-cay-2521.xml b/cayenne-server/src/test/resources/cayenne-cay-2521.xml
new file mode 100644
index 0000000..8db3486
--- /dev/null
+++ b/cayenne-server/src/test/resources/cayenne-cay-2521.xml
@@ -0,0 +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="cay-2521"/>
+</domain>