You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2012/10/11 19:15:53 UTC

svn commit: r1397175 - in /cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src: main/java/org/apache/cayenne/access/ test/java/org/apache/cayenne/access/

Author: aadamchik
Date: Thu Oct 11 17:15:53 2012
New Revision: 1397175

URL: http://svn.apache.org/viewvc?rev=1397175&view=rev
Log:
CAY-1746 Two InsertBatchQueries generated for a single DbEntity insert when this DbEntity has a flattened attribute and a flattened relationship

DbArcId implementation for tracking flattened DbEntity changes
(cherry picked from commit a52431bb10c4a49123cac8cabe9d124cd61f3c1d)

Added:
    cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/DbArcId.java
    cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/DbArcIdTest.java
Modified:
    cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/FlattenedArcKey.java
    cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/FlattenedArcKeyTest.java

Added: cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/DbArcId.java
URL: http://svn.apache.org/viewvc/cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/DbArcId.java?rev=1397175&view=auto
==============================================================================
--- cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/DbArcId.java (added)
+++ cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/DbArcId.java Thu Oct 11 17:15:53 2012
@@ -0,0 +1,70 @@
+package org.apache.cayenne.access;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.util.EqualsBuilder;
+import org.apache.cayenne.util.HashCodeBuilder;
+
+/**
+ * An id similar to ObjectId that identifies a DbEntity snapshot for implicit
+ * DbEntities of flattened attributes or relationships. Provides 'equals' and
+ * 'hashCode' implementations adequate for use as a map key.
+ * 
+ * @since 3.2
+ */
+final class DbArcId {
+
+    private int hashCode;
+
+    private DbEntity entity;
+    private ObjectId sourceId;
+    private String incomingArc;
+
+    DbArcId(DbEntity entity, ObjectId sourceId, String incomingArc) {
+        this.entity = entity;
+        this.sourceId = sourceId;
+        this.incomingArc = incomingArc;
+    }
+    
+    DbEntity getEntity() {
+        return entity;
+    }
+    
+    ObjectId getSourceId() {
+        return sourceId;
+    }
+    
+    String getIncominArc() {
+        return incomingArc;
+    }
+
+    @Override
+    public int hashCode() {
+
+        if (this.hashCode == 0) {
+            HashCodeBuilder builder = new HashCodeBuilder(3, 5);
+            builder.append(sourceId);
+            builder.append(incomingArc);
+            this.hashCode = builder.toHashCode();
+        }
+
+        return hashCode;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+
+        if (this == object) {
+            return true;
+        }
+
+        if (!(object instanceof DbArcId)) {
+            return false;
+        }
+
+        DbArcId id = (DbArcId) object;
+
+        return new EqualsBuilder().append(sourceId, id.sourceId)
+                .append(incomingArc, id.incomingArc).isEquals();
+    }
+}

Modified: cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/FlattenedArcKey.java
URL: http://svn.apache.org/viewvc/cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/FlattenedArcKey.java?rev=1397175&r1=1397174&r2=1397175&view=diff
==============================================================================
--- cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/FlattenedArcKey.java (original)
+++ cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/main/java/org/apache/cayenne/access/FlattenedArcKey.java Thu Oct 11 17:15:53 2012
@@ -41,6 +41,7 @@ import org.apache.cayenne.map.DbRelation
 import org.apache.cayenne.map.ObjRelationship;
 import org.apache.cayenne.query.Query;
 import org.apache.cayenne.query.SQLTemplate;
+import org.apache.cayenne.util.HashCodeBuilder;
 import org.apache.cayenne.util.Util;
 
 /**
@@ -50,60 +51,49 @@ import org.apache.cayenne.util.Util;
  */
 final class FlattenedArcKey {
 
-    ObjectId sourceId;
-    ObjectId destinationId;
     ObjRelationship relationship;
-    ObjRelationship reverseRelationship;
-    String compareToken;
+
+    DbArcId id1;
+    DbArcId id2;
 
     FlattenedArcKey(ObjectId sourceId, ObjectId destinationId,
             ObjRelationship relationship) {
 
-        this.sourceId = sourceId;
-        this.destinationId = destinationId;
         this.relationship = relationship;
-        this.reverseRelationship = relationship.getReverseRelationship();
 
-        // build a string token to make comparison (or at least hashcode) indepent from
-        // direction
-        String relName1 = relationship.getName();
-        if (reverseRelationship != null) {
-            String relName2 = reverseRelationship.getName();
-
-            // Find the lexically lesser name and use it as the name of the source, then
-            // use the second.
-            // If equal (the same name), it doesn't matter which order...
-            if (relName1.compareTo(relName2) <= 0) {
-                this.compareToken = relName1 + "." + relName2;
-            }
-            else {
-                this.compareToken = relName2 + "." + relName1;
-            }
+        List<DbRelationship> dbRelationships = relationship
+                .getDbRelationships();
+        if (dbRelationships.size() != 2) {
+            throw new CayenneRuntimeException(
+                    "Only single-step flattened relationships are supported in this operation, whereas the relationship '%s' has %s",
+                    relationship, dbRelationships.size());
         }
-        else {
-            this.compareToken = relName1;
+
+        DbRelationship r1 = dbRelationships.get(0);
+        DbRelationship r2 = dbRelationships.get(1).getReverseRelationship();
+
+        if (r2 == null) {
+            throw new IllegalStateException(
+                    "No reverse relationship for DbRelationship "
+                            + dbRelationships.get(1));
         }
+
+        id1 = new DbArcId((DbEntity) r1.getTargetEntity(), sourceId,
+                r1.getName());
+        id2 = new DbArcId(id1.getEntity(), destinationId, r2.getName());
     }
 
     /**
      * Returns a join DbEntity for the single-step flattened relationship.
      */
     DbEntity getJoinEntity() {
-        List<DbRelationship> relList = relationship.getDbRelationships();
-        if (relList.size() != 2) {
-            throw new CayenneRuntimeException(
-                    "Only single-step flattened relationships are supported in this operation: "
-                            + relationship);
-        }
-
-        DbRelationship firstDbRel = relList.get(0);
-        return (DbEntity) firstDbRel.getTargetEntity();
+        return id1.getEntity();
     }
 
     /**
-     * Returns a snapshot for join record for the single-step flattened relationship,
-     * generating value for the primary key column if it is not propagated via the
-     * relationships.
+     * Returns a snapshot for join record for the single-step flattened
+     * relationship, generating value for the primary key column if it is not
+     * propagated via the relationships.
      */
     Map<String, Object> buildJoinSnapshotForInsert(DataNode node) {
         Map<String, Object> snapshot = lazyJoinSnapshot();
@@ -119,7 +109,8 @@ final class FlattenedArcKey {
 
             DbAdapter adapter = node.getAdapter();
 
-            // skip db-generated... looks like we don't care about the actual PK value
+            // skip db-generated... looks like we don't care about the actual PK
+            // value
             // here, so no need to retrieve db-generated pk back to Java.
             if (adapter.supportsGeneratedKeys() && dbAttr.isGenerated()) {
                 continue;
@@ -132,11 +123,11 @@ final class FlattenedArcKey {
 
             // finally, use database generation mechanism
             try {
-                Object pkValue = adapter.getPkGenerator().generatePk(node, dbAttr);
+                Object pkValue = adapter.getPkGenerator().generatePk(node,
+                        dbAttr);
                 snapshot.put(dbAttrName, pkValue);
                 autoPkDone = true;
-            }
-            catch (Exception ex) {
+            } catch (Exception ex) {
                 throw new CayenneRuntimeException("Error generating PK: "
                         + ex.getMessage(), ex);
             }
@@ -146,9 +137,9 @@ final class FlattenedArcKey {
     }
 
     /**
-     * Returns pk snapshots for join records for the single-step flattened relationship.
-     * Multiple joins between the same pair of objects are theoretically possible, so the
-     * return value is a list.
+     * Returns pk snapshots for join records for the single-step flattened
+     * relationship. Multiple joins between the same pair of objects are
+     * theoretically possible, so the return value is a list.
      */
     List buildJoinSnapshotsForDelete(DataNode node) {
         Map snapshot = eagerJoinSnapshot();
@@ -169,24 +160,24 @@ final class FlattenedArcKey {
         }
 
         // ok, the key is not included in snapshot, must do the fetch...
-        // TODO: this should be optimized in the future, but now DeleteBatchQuery
+        // TODO: this should be optimized in the future, but now
+        // DeleteBatchQuery
         // expects a PK snapshot, so we must provide it.
 
         final boolean quotesNeeded;
         if (joinEntity.getDataMap() != null
                 && joinEntity.getDataMap().isQuotingSQLIdentifiers()) {
             quotesNeeded = true;
-        }
-        else {
+        } else {
             quotesNeeded = false;
         }
 
-        QuotingStrategy quoter = node.getAdapter().getQuotingStrategy(quotesNeeded);
+        QuotingStrategy quoter = node.getAdapter().getQuotingStrategy(
+                quotesNeeded);
 
         StringBuilder sql = new StringBuilder("SELECT ");
         Collection<DbAttribute> pk = joinEntity.getPrimaryKeys();
-        final List<DbAttribute> pkList = pk instanceof List
-                ? (List<DbAttribute>) pk
+        final List<DbAttribute> pkList = pk instanceof List ? (List<DbAttribute>) pk
                 : new ArrayList<DbAttribute>(pk);
 
         for (int i = 0; i < pkList.size(); i++) {
@@ -201,9 +192,11 @@ final class FlattenedArcKey {
             sql.append(quoter.quoteString(attribute.getName()));
 
             if (quotesNeeded) {
-                // since the name of the column can potentially be quoted and use reserved
+                // since the name of the column can potentially be quoted and
+                // use reserved
                 // keywords as name, let's specify
-                // generated column name parameters to ensure the query doesn't explode
+                // generated column name parameters to ensure the query doesn't
+                // explode
                 sql.append("' '").append(
                         TypesMapping.getJavaBySqlType(attribute.getType()));
                 sql.append("' '").append("pk").append(i);
@@ -212,30 +205,25 @@ final class FlattenedArcKey {
             sql.append("')");
         }
 
-        sql
-                .append(" FROM ")
-                .append(quoter.quoteFullyQualifiedName(joinEntity))
+        sql.append(" FROM ").append(quoter.quoteFullyQualifiedName(joinEntity))
                 .append(" WHERE ");
         int i = snapshot.size();
         for (Object key : snapshot.keySet()) {
-            sql
-                    .append(quoter.quoteString(String.valueOf(key)))
-                    .append(" #bindEqual($")
-                    .append(key)
-                    .append(")");
+            sql.append(quoter.quoteString(String.valueOf(key)))
+                    .append(" #bindEqual($").append(key).append(")");
 
             if (--i > 0) {
                 sql.append(" AND ");
             }
         }
 
-        SQLTemplate query = new SQLTemplate(joinEntity.getDataMap(), sql.toString(), true);
+        SQLTemplate query = new SQLTemplate(joinEntity.getDataMap(),
+                sql.toString(), true);
         query.setParameters(snapshot);
 
         final List[] result = new List[1];
 
-        node.performQueries(
-                Collections.singleton((Query) query),
+        node.performQueries(Collections.singleton((Query) query),
                 new DefaultOperationObserver() {
 
                     @Override
@@ -244,21 +232,22 @@ final class FlattenedArcKey {
                         if (quotesNeeded && !dataRows.isEmpty()) {
                             // decode results...
 
-                            List<DataRow> fixedRows = new ArrayList<DataRow>(dataRows
-                                    .size());
-                            for(Object o : dataRows) {
+                            List<DataRow> fixedRows = new ArrayList<DataRow>(
+                                    dataRows.size());
+                            for (Object o : dataRows) {
                                 DataRow row = (DataRow) o;
-                                
+
                                 DataRow fixedRow = new DataRow(2);
-                                
+
                                 for (int i = 0; i < pkList.size(); i++) {
                                     DbAttribute attribute = pkList.get(i);
-                                    fixedRow.put(attribute.getName(), row.get("pk" + i));
+                                    fixedRow.put(attribute.getName(),
+                                            row.get("pk" + i));
                                 }
-                                
+
                                 fixedRows.add(fixedRow);
                             }
-                            
+
                             dataRows = fixedRows;
                         }
 
@@ -268,8 +257,8 @@ final class FlattenedArcKey {
                     @Override
                     public void nextQueryException(Query query, Exception ex) {
                         throw new CayenneRuntimeException(
-                                "Raising from query exception.",
-                                Util.unwindException(ex));
+                                "Raising from query exception.", Util
+                                        .unwindException(ex));
                     }
 
                     @Override
@@ -283,14 +272,39 @@ final class FlattenedArcKey {
         return result[0];
     }
 
-    boolean isBidirectional() {
-        return reverseRelationship != null;
-    }
-
     @Override
     public int hashCode() {
-        // TODO: use hashcode builder to make a better hashcode.
-        return sourceId.hashCode() + destinationId.hashCode() + compareToken.hashCode();
+        // order ids in array for hashcode consistency purposes. The actual
+        // order direction is not important, as long as it
+        // is consistent across invocations
+
+        int compare = id1.getSourceId().getEntityName()
+                .compareTo(id2.getSourceId().getEntityName());
+
+        if (compare == 0) {
+            compare = id1.getIncominArc().compareTo(id2.getIncominArc());
+
+            if (compare == 0) {
+                // since ordering is mostly important for detecting equivalent
+                // FlattenedArc keys coming from 2 opposite directions, the name
+                // of ObjRelationship can be a good criteria
+
+                ObjRelationship or2 = relationship.getReverseRelationship();
+                compare = or2 != null ? relationship.getName().compareTo(
+                        or2.getName()) : 1;
+
+                // TODO: if(compare == 0) ??
+            }
+        }
+
+        DbArcId[] ordered;
+        if (compare < 0) {
+            ordered = new DbArcId[] { id1, id2 };
+        } else {
+            ordered = new DbArcId[] { id2, id1 };
+        }
+
+        return new HashCodeBuilder().append(ordered).toHashCode();
     }
 
     /**
@@ -307,30 +321,16 @@ final class FlattenedArcKey {
             return false;
         }
 
-        FlattenedArcKey update = (FlattenedArcKey) object;
+        FlattenedArcKey key = (FlattenedArcKey) object;
 
-        if (!this.compareToken.equals(update.compareToken)) {
-            return false;
-        }
-
-        boolean bidi = isBidirectional();
-        if (bidi != update.isBidirectional()) {
-            return false;
+        // ignore id order in comparison
+        if (id1.equals(key.id1)) {
+            return id2.equals(key.id2);
+        } else if (id1.equals(key.id2)) {
+            return id2.equals(key.id1);
         }
 
-        return (bidi) ? bidiEquals(update) : uniEquals(update);
-    }
-
-    private boolean bidiEquals(FlattenedArcKey update) {
-        return (sourceId.equals(update.sourceId) && destinationId
-                .equals(update.destinationId))
-                || (this.sourceId.equals(update.destinationId) && this.destinationId
-                        .equals(update.sourceId));
-    }
-
-    private boolean uniEquals(FlattenedArcKey update) {
-        return (this.sourceId.equals(update.sourceId) && this.destinationId
-                .equals(update.destinationId));
+        return false;
     }
 
     private Map eagerJoinSnapshot() {
@@ -345,17 +345,21 @@ final class FlattenedArcKey {
         DbRelationship firstDbRel = relList.get(0);
         DbRelationship secondDbRel = relList.get(1);
 
-        Map<String, ?> sourceId = this.sourceId.getIdSnapshot();
-        Map<String, ?> destinationId = this.destinationId.getIdSnapshot();
+        // here ordering of ids is determined by 'relationship', so use id1, id2
+        // instead of orderedIds
+        Map<String, ?> sourceId = id1.getSourceId().getIdSnapshot();
+        Map<String, ?> destinationId = id2.getSourceId().getIdSnapshot();
 
-        Map<String, Object> snapshot = new HashMap<String, Object>(sourceId.size()
-                + destinationId.size(), 1);
+        Map<String, Object> snapshot = new HashMap<String, Object>(
+                sourceId.size() + destinationId.size(), 1);
         for (DbJoin join : firstDbRel.getJoins()) {
-            snapshot.put(join.getTargetName(), sourceId.get(join.getSourceName()));
+            snapshot.put(join.getTargetName(),
+                    sourceId.get(join.getSourceName()));
         }
 
         for (DbJoin join : secondDbRel.getJoins()) {
-            snapshot.put(join.getSourceName(), destinationId.get(join.getTargetName()));
+            snapshot.put(join.getSourceName(),
+                    destinationId.get(join.getTargetName()));
         }
 
         return snapshot;
@@ -376,19 +380,24 @@ final class FlattenedArcKey {
         List<DbJoin> fromSourceJoins = firstDbRel.getJoins();
         List<DbJoin> toTargetJoins = secondDbRel.getJoins();
 
-        Map<String, Object> snapshot = new HashMap<String, Object>(fromSourceJoins.size()
-                + toTargetJoins.size(), 1);
+        Map<String, Object> snapshot = new HashMap<String, Object>(
+                fromSourceJoins.size() + toTargetJoins.size(), 1);
+
+        // here ordering of ids is determined by 'relationship', so use id1, id2
+        // instead of orderedIds
 
         for (int i = 0, numJoins = fromSourceJoins.size(); i < numJoins; i++) {
             DbJoin join = fromSourceJoins.get(i);
 
-            Object value = new PropagatedValueFactory(sourceId, join.getSourceName());
+            Object value = new PropagatedValueFactory(id1.getSourceId(),
+                    join.getSourceName());
             snapshot.put(join.getTargetName(), value);
         }
 
         for (int i = 0, numJoins = toTargetJoins.size(); i < numJoins; i++) {
             DbJoin join = toTargetJoins.get(i);
-            Object value = new PropagatedValueFactory(destinationId, join.getTargetName());
+            Object value = new PropagatedValueFactory(id2.getSourceId(),
+                    join.getTargetName());
             snapshot.put(join.getSourceName(), value);
         }
 

Added: cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/DbArcIdTest.java
URL: http://svn.apache.org/viewvc/cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/DbArcIdTest.java?rev=1397175&view=auto
==============================================================================
--- cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/DbArcIdTest.java (added)
+++ cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/DbArcIdTest.java Thu Oct 11 17:15:53 2012
@@ -0,0 +1,48 @@
+package org.apache.cayenne.access;
+
+import junit.framework.TestCase;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.map.DbEntity;
+
+public class DbArcIdTest extends TestCase {
+
+    public void testHashCode() {
+        
+        DbEntity e = new DbEntity("X");
+
+        DbArcId id1 = new DbArcId(e, new ObjectId("x", "k", "v"), "r1");
+        int h1 = id1.hashCode();
+        assertEquals(h1, id1.hashCode());
+        assertEquals(h1, id1.hashCode());
+
+        DbArcId id1_eq = new DbArcId(e, new ObjectId("x", "k", "v"), "r1");
+        assertEquals(h1, id1_eq.hashCode());
+
+        DbArcId id2 = new DbArcId(e, new ObjectId("x", "k", "v"), "r2");
+        assertFalse(h1 == id2.hashCode());
+
+        DbArcId id3 = new DbArcId(e, new ObjectId("y", "k", "v"), "r1");
+        assertFalse(h1 == id3.hashCode());
+    }
+
+    public void testEquals() {
+
+        DbEntity e = new DbEntity("X");
+        
+        DbArcId id1 = new DbArcId(e, new ObjectId("x", "k", "v"), "r1");
+        assertTrue(id1.equals(id1));
+
+        DbArcId id1_eq = new DbArcId(e, new ObjectId("x", "k", "v"), "r1");
+        assertTrue(id1.equals(id1_eq));
+        assertTrue(id1_eq.equals(id1));
+
+        DbArcId id2 = new DbArcId(e, new ObjectId("x", "k", "v"), "r2");
+        assertFalse(id1.equals(id2));
+
+        DbArcId id3 = new DbArcId(e, new ObjectId("y", "k", "v"), "r1");
+        assertFalse(id1.equals(id3));
+
+        assertFalse(id1.equals(new Object()));
+    }
+}

Modified: cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/FlattenedArcKeyTest.java
URL: http://svn.apache.org/viewvc/cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/FlattenedArcKeyTest.java?rev=1397175&r1=1397174&r2=1397175&view=diff
==============================================================================
--- cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/FlattenedArcKeyTest.java (original)
+++ cayenne/main/trunk/framework/cayenne-jdk1.5-unpublished/src/test/java/org/apache/cayenne/access/FlattenedArcKeyTest.java Thu Oct 11 17:15:53 2012
@@ -19,44 +19,76 @@
 
 package org.apache.cayenne.access;
 
-import junit.framework.TestCase;
-
 import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.map.MockObjRelationship;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.ObjRelationship;
+import org.apache.cayenne.testdo.relationship.FlattenedTest1;
+import org.apache.cayenne.testdo.relationship.FlattenedTest3;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+
+@UseServerRuntime(ServerCase.RELATIONSHIPS_PROJECT)
+public class FlattenedArcKeyTest extends ServerCase {
 
-/**
- */
-public class FlattenedArcKeyTest extends TestCase {
+    @Inject
+    private EntityResolver entityResolver;
 
     public void testAttributes() {
         ObjectId src = new ObjectId("X");
         ObjectId target = new ObjectId("Y");
-        MockObjRelationship r1 = new MockObjRelationship("r1");
-        r1.setReverseRelationship(new MockObjRelationship("r2"));
+        ObjRelationship r1 = (ObjRelationship) entityResolver.lookupObjEntity(
+                FlattenedTest3.class).getRelationship(
+                FlattenedTest3.TO_FT1_PROPERTY);
 
         FlattenedArcKey update = new FlattenedArcKey(src, target, r1);
 
-        assertSame(src, update.sourceId);
-        assertSame(target, update.destinationId);
+        assertSame(src, update.id1.getSourceId());
+        assertSame(target, update.id2.getSourceId());
         assertSame(r1, update.relationship);
-        assertSame(r1.getReverseRelationship(), update.reverseRelationship);
-        assertTrue(update.isBidirectional());
+    }
+    
+    public void testHashCode() {
+        ObjectId src = new ObjectId("X");
+        ObjectId target = new ObjectId("Y");
+        ObjRelationship r1 = (ObjRelationship) entityResolver.lookupObjEntity(
+                FlattenedTest3.class).getRelationship(
+                FlattenedTest3.TO_FT1_PROPERTY);
+
+        FlattenedArcKey update = new FlattenedArcKey(src, target, r1);
+        FlattenedArcKey update1 = new FlattenedArcKey(target, src,
+                r1.getReverseRelationship());
+
+        ObjRelationship r3 = (ObjRelationship) entityResolver.lookupObjEntity(
+                FlattenedTest1.class).getRelationship(
+                FlattenedTest1.FT3OVER_COMPLEX_PROPERTY);
+
+        FlattenedArcKey update2 = new FlattenedArcKey(target, src, r3);
+
+        int h = update.hashCode();
+        int h1 = update1.hashCode();
+        int h2 = update2.hashCode();
+        assertTrue(h == h1);
+        assertTrue(h == update.hashCode());
+        assertFalse(h == h2);
     }
 
     public void testEquals() {
         ObjectId src = new ObjectId("X");
         ObjectId target = new ObjectId("Y");
-        MockObjRelationship r1 = new MockObjRelationship("r1");
-        r1.setReverseRelationship(new MockObjRelationship("r2"));
+        ObjRelationship r1 = (ObjRelationship) entityResolver.lookupObjEntity(
+                FlattenedTest3.class).getRelationship(
+                FlattenedTest3.TO_FT1_PROPERTY);
 
         FlattenedArcKey update = new FlattenedArcKey(src, target, r1);
-        FlattenedArcKey update1 = new FlattenedArcKey(target, src, r1
-                .getReverseRelationship());
+        FlattenedArcKey update1 = new FlattenedArcKey(target, src,
+                r1.getReverseRelationship());
+
+        ObjRelationship r3 = (ObjRelationship) entityResolver.lookupObjEntity(
+                FlattenedTest1.class).getRelationship(
+                FlattenedTest1.FT3OVER_COMPLEX_PROPERTY);
 
-        FlattenedArcKey update2 = new FlattenedArcKey(
-                target,
-                src,
-                new MockObjRelationship("r3"));
+        FlattenedArcKey update2 = new FlattenedArcKey(target, src, r3);
 
         assertTrue(update.equals(update1));
         assertFalse(update.equals(update2));