cayenne-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ntimof...@apache.org
Subject [cayenne] branch master updated: CAY-2520 Split ObjectId into several specialized variants
Date Tue, 05 Feb 2019 14:34:28 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/master by this push:
     new 9e720f7  CAY-2520 Split ObjectId into several specialized variants
9e720f7 is described below

commit 9e720f759f92565c99880cc88bd3340f8ad27f3f
Author: Nikita Timofeev <stariy95@gmail.com>
AuthorDate: Tue Feb 5 17:34:15 2019 +0300

    CAY-2520 Split ObjectId into several specialized variants
---
 RELEASE-NOTES.txt                                  |   1 +
 UPGRADE.txt                                        |   3 +
 .../CayenneContextClientChannelEventsIT.java       |  24 +-
 .../java/org/apache/cayenne/CayenneContextIT.java  |  14 +-
 .../cayenne/CayenneContextMapRelationshipIT.java   |   4 +-
 .../cayenne/CayenneContextWithDataContextIT.java   |   6 +-
 .../test/java/org/apache/cayenne/ObjectIdTest.java |  21 +-
 .../cayenne/PersistentObjectInContextIT.java       |  12 +-
 .../cayenne/access/ClientServerChannelIT.java      |   4 +-
 .../apache/cayenne/query/ClientExpressionIT.java   |   8 +-
 .../apache/cayenne/query/ObjectIdQueryTest.java    |   2 +-
 .../cayenne/query/RelationshipQueryTest.java       |   2 +-
 .../apache/cayenne/remote/ClientChannelTest.java   |   6 +-
 .../cayenne/util/ObjectDetachOperationIT.java      |   4 +-
 .../cayenne/commitlog/CommitLogFilter_AllIT.java   |  14 +-
 .../commitlog/CommitLogFilter_All_FlattenedIT.java |   6 +-
 .../commitlog/CommitLogFilter_FilteredIT.java      |  12 +-
 .../apache/cayenne/lifecycle/id/EntityIdCoder.java |   6 +-
 .../cayenne/lifecycle/id/EntityIdCoderTest.java    |  12 +-
 .../apache/cayenne/lifecycle/id/IdCoderTest.java   |  10 +-
 ...tContextChangeLogSubListMessageFactoryTest.java |   2 +-
 .../java/org/apache/cayenne/CayenneContext.java    |   3 +-
 .../cayenne/CayenneContextGraphManagerTest.java    |   4 +-
 .../src/main/java/org/apache/cayenne/Cayenne.java  |  10 +-
 .../src/main/java/org/apache/cayenne/DataRow.java  |   2 +-
 .../src/main/java/org/apache/cayenne/ObjectId.java | 388 ++---------------
 .../java/org/apache/cayenne/ObjectIdCompound.java  | 255 +++++++++++
 .../java/org/apache/cayenne/ObjectIdNumber.java    | 126 ++++++
 .../java/org/apache/cayenne/ObjectIdSingle.java    | 132 ++++++
 .../main/java/org/apache/cayenne/ObjectIdTmp.java  | 124 ++++++
 .../org/apache/cayenne/access/DataContext.java     |  10 +-
 .../cayenne/access/DataDomainQueryAction.java      |   8 +-
 .../org/apache/cayenne/access/ObjectResolver.java  |   4 +-
 .../cayenne/util/ObjectContextQueryAction.java     |   8 +-
 .../org/apache/cayenne/util/SingleEntryMap.java    | 265 ++++++++++++
 .../org/apache/cayenne/CayenneDataObjectIT.java    |   2 +-
 .../cayenne/CayenneDataObjectInContextIT.java      |   4 +-
 .../test/java/org/apache/cayenne/CayenneIT.java    |  12 +-
 .../apache/cayenne/ContextStateRecorderTest.java   |   8 +-
 .../apache/cayenne/ObjectContextChangeLogTest.java |   2 +-
 .../org/apache/cayenne/ObjectIdRegressionTest.java |   4 +-
 .../test/java/org/apache/cayenne/ObjectIdTest.java | 167 +++++---
 .../org/apache/cayenne/PersistentObjectIT.java     |   2 +-
 .../DataContextEntityWithMeaningfulPKIT.java       |   2 +-
 .../apache/cayenne/access/DataContextExtrasIT.java |   4 +-
 .../org/apache/cayenne/access/DataContextIT.java   |   4 +-
 .../cayenne/access/DataContextObjectIdQueryIT.java |   6 +-
 .../DataContextObjectIdQuery_PolymorphicIT.java    |   6 +-
 .../access/DataContextPrefetchMultistepIT.java     |   4 +-
 .../org/apache/cayenne/access/DataRowStoreIT.java  |   6 +-
 .../org/apache/cayenne/access/DbArcIdTest.java     |  35 +-
 .../apache/cayenne/access/FlattenedArcKeyIT.java   |  12 +-
 .../org/apache/cayenne/access/JointPrefetchIT.java |   8 +-
 .../cayenne/access/NestedDataContextReadIT.java    |   5 +-
 .../org/apache/cayenne/access/NumericTypesIT.java  |   4 +-
 .../org/apache/cayenne/access/ObjectStoreIT.java   |   6 +-
 .../org/apache/cayenne/access/ObjectStoreTest.java |   4 +-
 .../apache/cayenne/access/QuotedIdentifiersIT.java |   4 +-
 .../org/apache/cayenne/exp/ExpressionTest.java     |   2 +-
 .../org/apache/cayenne/exp/parser/ASTListTest.java |   6 +-
 .../apache/cayenne/exp/parser/EvaluatorTest.java   |  18 +-
 .../apache/cayenne/query/ObjectIdQueryTest.java    |  24 +-
 .../cayenne/query/RelationshipQueryTest.java       |   4 +-
 .../org/apache/cayenne/query/SelectById_RunIT.java |   6 +-
 .../apache/cayenne/query/StatementFetchSizeIT.java |   2 +-
 .../reflect/LifecycleCallbackEventHandlerTest.java |  11 +-
 .../template/CayenneSQLTemplateProcessorTest.java  |   4 +-
 .../cayenne/util/ShallowMergeOperationIT.java      |   4 +-
 .../apache/cayenne/util/SingleEntryMapTest.java    | 471 +++++++++++++++++++++
 .../velocity/VelocitySQLTemplateProcessorTest.java |   6 +-
 70 files changed, 1740 insertions(+), 641 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index e2eeca3..fc6fbe7 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -19,6 +19,7 @@ CAY-2508 Create api to add aliases in expressions
 CAY-2510 Create builder to load custom modules into plugins and modeler
 CAY-2511 Contribute custom properties for attributes
 CAY-2517 EventManager: optimization of adding listeners
+CAY-2520 Split ObjectId into several specialized variants
 
 Bug Fixes:
 
diff --git a/UPGRADE.txt b/UPGRADE.txt
index c7eb958..c5de6e7 100644
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -7,6 +7,9 @@ IMPORTANT: be sure to read all notes for the intermediate releases between your
 
 UPGRADING TO 4.2.M1
 
+* Per CAY-2520 ObjectId can't be instantiated directly, ObjectId.of(..) methods should be used.
+    E.g. ObjectId.of("Artist", 1) instead of new ObjectId("Artist", 1).
+
 * Per CAY-2467 Property class is replaced with a type-aware Property API, it's mostly backwards compatible.
     To take advantage of this new API you should regenerate code via Modeler ("Tools" -> "Generate Classes") or cgen tools.
 
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextClientChannelEventsIT.java b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextClientChannelEventsIT.java
index 49ef1f5..11c1403 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextClientChannelEventsIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextClientChannelEventsIT.java
@@ -168,11 +168,11 @@ public class CayenneContextClientChannelEventsIT extends ClientCaseContextsSync
 
         ClientMtTable1 o1 = (ClientMtTable1) Cayenne.objectForQuery(
                 c1,
-                new ObjectIdQuery(new ObjectId("MtTable1", "TABLE1_ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable1", "TABLE1_ID", 1)));
 
         ClientMtTable1 o2 = (ClientMtTable1) Cayenne.objectForQuery(
                 c2,
-                new ObjectIdQuery(new ObjectId("MtTable1", "TABLE1_ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable1", "TABLE1_ID", 1)));
 
         assertEquals("g1", o1.getGlobalAttribute1());
         assertEquals("g1", o2.getGlobalAttribute1());
@@ -200,18 +200,18 @@ public class CayenneContextClientChannelEventsIT extends ClientCaseContextsSync
 
         ClientMtTable2 o1 = (ClientMtTable2) Cayenne.objectForQuery(
                 c1,
-                new ObjectIdQuery(new ObjectId("MtTable2", "TABLE2_ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable2", "TABLE2_ID", 1)));
 
         ClientMtTable2 o2 = (ClientMtTable2) Cayenne.objectForQuery(
                 c2,
-                new ObjectIdQuery(new ObjectId("MtTable2", "TABLE2_ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable2", "TABLE2_ID", 1)));
 
         assertEquals("g1", o1.getTable1().getGlobalAttribute1());
         assertEquals("g1", o2.getTable1().getGlobalAttribute1());
 
         ClientMtTable1 o1r = (ClientMtTable1) Cayenne.objectForQuery(
                 c1,
-                new ObjectIdQuery(new ObjectId("MtTable1", "TABLE1_ID", 2)));
+                new ObjectIdQuery(ObjectId.of("MtTable1", "TABLE1_ID", 2)));
         o1.setTable1(o1r);
         c1.commitChanges();
         
@@ -234,11 +234,11 @@ public class CayenneContextClientChannelEventsIT extends ClientCaseContextsSync
 
         ClientMtTable1 o1 = (ClientMtTable1) Cayenne.objectForQuery(
                 c1,
-                new ObjectIdQuery(new ObjectId("MtTable1", "TABLE1_ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable1", "TABLE1_ID", 1)));
 
         ClientMtTable1 o2 = (ClientMtTable1) Cayenne.objectForQuery(
                 c2,
-                new ObjectIdQuery(new ObjectId("MtTable1", "TABLE1_ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable1", "TABLE1_ID", 1)));
 
         assertEquals(1, o1.getTable2Array().size());
         assertEquals(1, o2.getTable2Array().size());
@@ -268,7 +268,7 @@ public class CayenneContextClientChannelEventsIT extends ClientCaseContextsSync
 
         ClientMtTable1 o1 = (ClientMtTable1) Cayenne.objectForQuery(
                 c1,
-                new ObjectIdQuery(new ObjectId("MtTable1", "TABLE1_ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable1", "TABLE1_ID", 1)));
 
         // do not resolve objects in question in the second context and see if the merge
         // causes any issues...
@@ -288,7 +288,7 @@ public class CayenneContextClientChannelEventsIT extends ClientCaseContextsSync
 
         ClientMtTable1 o2 = (ClientMtTable1) Cayenne.objectForQuery(
                 c2,
-                new ObjectIdQuery(new ObjectId("MtTable1", "TABLE1_ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable1", "TABLE1_ID", 1)));
         assertEquals(2, o2.getTable2Array().size());
     }
 
@@ -305,18 +305,18 @@ public class CayenneContextClientChannelEventsIT extends ClientCaseContextsSync
 
         ClientMtTable4 o1 = (ClientMtTable4) Cayenne.objectForQuery(
                 c1,
-                new ObjectIdQuery(new ObjectId("MtTable4", "ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable4", "ID", 1)));
 
         ClientMtTable4 o2 = (ClientMtTable4) Cayenne.objectForQuery(
                 c2,
-                new ObjectIdQuery(new ObjectId("MtTable4", "ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable4", "ID", 1)));
 
         assertEquals(2, o1.getTable5s().size());
         assertEquals(2, o2.getTable5s().size());
 
         ClientMtTable5 o1r = (ClientMtTable5) Cayenne.objectForQuery(
                 c1,
-                new ObjectIdQuery(new ObjectId("MtTable5", "ID", 1)));
+                new ObjectIdQuery(ObjectId.of("MtTable5", "ID", 1)));
         o1.removeFromTable5s(o1r);
 
         c1.commitChanges();
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextIT.java b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextIT.java
index 33bd5e8..35e178d 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextIT.java
@@ -118,7 +118,7 @@ public class CayenneContextIT extends ClientCase {
 
 		// test that a command is being sent via connector on commit...
 
-		context.internalGraphManager().nodePropertyChanged(new ObjectId("MtTable1"), "x", "y", "z");
+		context.internalGraphManager().nodePropertyChanged(ObjectId.of("MtTable1"), "x", "y", "z");
 
 		context.commitChanges();
 		assertEquals(1, channel.getRequestObjects().size());
@@ -131,7 +131,7 @@ public class CayenneContextIT extends ClientCase {
 	@Test
 	public void testCommitChangesNew() {
 		final CompoundDiff diff = new CompoundDiff();
-		final Object newObjectId = new ObjectId("test", "key", "generated");
+		final Object newObjectId = ObjectId.of("test", "key", "generated");
 		eventManager = new DefaultEventManager(0);
 
 		// test that ids that are passed back are actually propagated to the
@@ -229,7 +229,7 @@ public class CayenneContextIT extends ClientCase {
 		// COMMITTED
 		Persistent committed = new MockPersistentObject();
 		committed.setPersistenceState(PersistenceState.COMMITTED);
-		committed.setObjectId(new ObjectId("test_entity", "key", "value1"));
+		committed.setObjectId(ObjectId.of("test_entity", "key", "value1"));
 		committed.setObjectContext(context);
 		context.deleteObjects(committed);
 		assertEquals(PersistenceState.DELETED, committed.getPersistenceState());
@@ -237,7 +237,7 @@ public class CayenneContextIT extends ClientCase {
 		// MODIFIED
 		Persistent modified = new MockPersistentObject();
 		modified.setPersistenceState(PersistenceState.MODIFIED);
-		modified.setObjectId(new ObjectId("test_entity", "key", "value2"));
+		modified.setObjectId(ObjectId.of("test_entity", "key", "value2"));
 		modified.setObjectContext(context);
 		context.deleteObjects(modified);
 		assertEquals(PersistenceState.DELETED, modified.getPersistenceState());
@@ -245,7 +245,7 @@ public class CayenneContextIT extends ClientCase {
 		// DELETED
 		Persistent deleted = new MockPersistentObject();
 		deleted.setPersistenceState(PersistenceState.DELETED);
-		deleted.setObjectId(new ObjectId("test_entity", "key", "value3"));
+		deleted.setObjectId(ObjectId.of("test_entity", "key", "value3"));
 		deleted.setObjectContext(context);
 		context.deleteObjects(deleted);
 		assertEquals(PersistenceState.DELETED, committed.getPersistenceState());
@@ -254,7 +254,7 @@ public class CayenneContextIT extends ClientCase {
 	@Test
 	public void testBeforePropertyReadShouldInflateHollow() {
 
-		ObjectId gid = new ObjectId("MtTable1", "a", "b");
+        ObjectId gid = ObjectId.of("MtTable1", "a", "b");
 		final ClientMtTable1 inflated = new ClientMtTable1();
 		inflated.setPersistenceState(PersistenceState.COMMITTED);
 		inflated.setObjectId(gid);
@@ -306,7 +306,7 @@ public class CayenneContextIT extends ClientCase {
 	@Test
 	public void testBeforeHollowDeleteShouldChangeStateToCommited() {
 
-		ObjectId gid = new ObjectId("MtTable1", "a", "b");
+        ObjectId gid = ObjectId.of("MtTable1", "a", "b");
 		final ClientMtTable1 inflated = new ClientMtTable1();
 		inflated.setPersistenceState(PersistenceState.COMMITTED);
 		inflated.setObjectId(gid);
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextMapRelationshipIT.java b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextMapRelationshipIT.java
index ce3b59f..8fdca31 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextMapRelationshipIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextMapRelationshipIT.java
@@ -69,7 +69,7 @@ public class CayenneContextMapRelationshipIT extends ClientCase {
     public void testReadToMany() throws Exception {
         createTwoMapToManysWithTargetsDataSet();
 
-        ObjectId id = new ObjectId("IdMapToMany", IdMapToMany.ID_PK_COLUMN, 1);
+        ObjectId id = ObjectId.of("IdMapToMany", IdMapToMany.ID_PK_COLUMN, 1);
         ClientIdMapToMany o1 = (ClientIdMapToMany) Cayenne.objectForQuery(
                 context,
                 new ObjectIdQuery(id));
@@ -89,7 +89,7 @@ public class CayenneContextMapRelationshipIT extends ClientCase {
     public void testAddToMany() throws Exception {
         createTwoMapToManysWithTargetsDataSet();
 
-        ObjectId id = new ObjectId("IdMapToMany", IdMapToMany.ID_PK_COLUMN, 1);
+        ObjectId id = ObjectId.of("IdMapToMany", IdMapToMany.ID_PK_COLUMN, 1);
         ClientIdMapToMany o1 = (ClientIdMapToMany) Cayenne.objectForQuery(
                 context,
                 new ObjectIdQuery(id));
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextWithDataContextIT.java b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextWithDataContextIT.java
index c910c30..96c517c 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextWithDataContextIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/CayenneContextWithDataContextIT.java
@@ -295,7 +295,7 @@ public class CayenneContextWithDataContextIT extends ClientCase {
     public void testCreateFault() throws Exception {
         tMtTable1.insert(1, "g1", "s1");
 
-        ObjectId id = new ObjectId("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 1);
+        ObjectId id = ObjectId.of("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 1);
 
         Object fault = clientContext.createFault(id);
         assertTrue(fault instanceof ClientMtTable1);
@@ -317,7 +317,7 @@ public class CayenneContextWithDataContextIT extends ClientCase {
     public void testCreateBadFault() throws Exception {
         tMtTable1.insert(1, "g1", "s1");
 
-        ObjectId id = new ObjectId("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 2);
+        ObjectId id = ObjectId.of("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 2);
 
         Object fault = clientContext.createFault(id);
         assertTrue(fault instanceof ClientMtTable1);
@@ -338,7 +338,7 @@ public class CayenneContextWithDataContextIT extends ClientCase {
     public void testPrefetchingToOne() throws Exception {
         createTwoMtTable1sAnd2sDataSet();
 
-        final ObjectId prefetchedId = new ObjectId(
+        final ObjectId prefetchedId = ObjectId.of(
                 "MtTable1",
                 MtTable1.TABLE1_ID_PK_COLUMN,
                 1);
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/ObjectIdTest.java b/cayenne-client/src/test/java/org/apache/cayenne/ObjectIdTest.java
index f4ef665..efa1cca 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/ObjectIdTest.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/ObjectIdTest.java
@@ -34,17 +34,17 @@ public class ObjectIdTest {
 
     @Test
     public void testHessianSerializabilityTemp() throws Exception {
-        ObjectId temp1 = new ObjectId("e");
+        ObjectId temp1 = ObjectId.of("e");
 
         // make sure hashcode is resolved
         int h = temp1.hashCode();
-        assertEquals(h, temp1.hashCode);
-        assertTrue(temp1.hashCode != 0);
+        assertEquals(h, temp1.hashCode());
+        assertTrue(temp1.hashCode() != 0);
 
         ObjectId temp2 = (ObjectId) HessianUtil.cloneViaClientServerSerialization(temp1, new EntityResolver());
 
         // make sure hashCode is reset to 0
-        assertTrue(temp2.hashCode == 0);
+        assertEquals(h, temp2.hashCode());
 
         assertTrue(temp1.isTemporary());
         assertNotSame(temp1, temp2);
@@ -53,17 +53,16 @@ public class ObjectIdTest {
 
     @Test
     public void testHessianSerializabilityPerm() throws Exception {
-        ObjectId perm1 = new ObjectId("e", "a", "b");
+        ObjectId perm1 = ObjectId.of("e", "a", "b");
 
         // make sure hashcode is resolved
         int h = perm1.hashCode();
-        assertEquals(h, perm1.hashCode);
-        assertTrue(perm1.hashCode != 0);
+        assertEquals(h, perm1.hashCode());
+        assertTrue(perm1.hashCode() != 0);
 
         ObjectId perm2 = (ObjectId) HessianUtil.cloneViaClientServerSerialization(perm1, new EntityResolver());
 
-        // make sure hashCode is reset to 0
-        assertTrue(perm2.hashCode == 0);
+        assertEquals(h, perm2.hashCode());
 
         assertFalse(perm2.isTemporary());
         assertNotSame(perm1, perm2);
@@ -74,8 +73,8 @@ public class ObjectIdTest {
     public void testHessianSerializabilityPerm1() throws Exception {
         // test serializing an id created with unmodifiable map
 
-        Map id = Collections.unmodifiableMap(Collections.singletonMap("a", "b"));
-        ObjectId perm1 = new ObjectId("e", id);
+        Map<String, Object> id = Collections.unmodifiableMap(Collections.singletonMap("a", "b"));
+        ObjectId perm1 = ObjectId.of("e", id);
         ObjectId perm2 = (ObjectId) HessianUtil.cloneViaClientServerSerialization(perm1, new EntityResolver());
 
         assertFalse(perm2.isTemporary());
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/PersistentObjectInContextIT.java b/cayenne-client/src/test/java/org/apache/cayenne/PersistentObjectInContextIT.java
index 209a8d6..108698d 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/PersistentObjectInContextIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/PersistentObjectInContextIT.java
@@ -75,10 +75,10 @@ public class PersistentObjectInContextIT extends ClientCase {
     public void testResolveToManyReverseResolved() throws Exception {
         createTwoMtTable1sAnd2sDataSet();
 
-        ObjectId gid = new ObjectId(
+        ObjectId gid = ObjectId.of(
                 "MtTable1",
                 MtTable1.TABLE1_ID_PK_COLUMN,
-                new Integer(1));
+                1);
         ClientMtTable1 t1 = (ClientMtTable1) Cayenne.objectForQuery(
                 context,
                 new ObjectIdQuery(gid));
@@ -100,10 +100,10 @@ public class PersistentObjectInContextIT extends ClientCase {
     public void testToOneRelationship() throws Exception {
         createTwoMtTable1sAnd2sDataSet();
 
-        ObjectId gid = new ObjectId(
+        ObjectId gid = ObjectId.of(
                 "MtTable2",
                 MtTable2.TABLE2_ID_PK_COLUMN,
-                new Integer(1));
+                1);
         ClientMtTable2 mtTable21 = (ClientMtTable2) Cayenne.objectForQuery(
                 context,
                 new ObjectIdQuery(gid));
@@ -119,10 +119,10 @@ public class PersistentObjectInContextIT extends ClientCase {
     public void testResolveToOneReverseResolved() throws Exception {
         createTwoMtTable1sAnd2sDataSet();
 
-        ObjectId gid = new ObjectId(
+        ObjectId gid = ObjectId.of(
                 "MtTable2",
                 MtTable2.TABLE2_ID_PK_COLUMN,
-                new Integer(1));
+                1);
         ClientMtTable2 mtTable21 = (ClientMtTable2) Cayenne.objectForQuery(
                 context,
                 new ObjectIdQuery(gid));
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/access/ClientServerChannelIT.java b/cayenne-client/src/test/java/org/apache/cayenne/access/ClientServerChannelIT.java
index 62ecbe6..66addea 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/access/ClientServerChannelIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/access/ClientServerChannelIT.java
@@ -125,7 +125,7 @@ public class ClientServerChannelIT extends ClientCase {
 
 		// introduce changes
 		clientServerChannel.onSync(serverContext
-				, new NodeCreateOperation(new ObjectId("MtTable1"))
+				, new NodeCreateOperation(ObjectId.of("MtTable1"))
 				, DataChannel.FLUSH_CASCADE_SYNC);
 
 		assertEquals(1, serverContext.performQuery(query).size());
@@ -150,7 +150,7 @@ public class ClientServerChannelIT extends ClientCase {
 		ClientMtTable1 clientObject = (ClientMtTable1) result;
 		assertNotNull(clientObject.getObjectId());
 
-		assertEquals(new ObjectId("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 55), clientObject.getObjectId());
+		assertEquals(ObjectId.of("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 55), clientObject.getObjectId());
 	}
 
 	@Test
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/query/ClientExpressionIT.java b/cayenne-client/src/test/java/org/apache/cayenne/query/ClientExpressionIT.java
index 1a345a6..f44b009 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/query/ClientExpressionIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/query/ClientExpressionIT.java
@@ -106,8 +106,8 @@ public class ClientExpressionIT extends ClientCase {
         assertEquals(t1.getObjectId(), values[0]);
         assertEquals(t2.getObjectId(), values[1]);
         
-        ObjectId t1Id = new ObjectId("MtTable1", "TABLE1_ID", 1);
-        ObjectId t2Id = new ObjectId("MtTable1", "TABLE1_ID", 2);
+        ObjectId t1Id = ObjectId.of("MtTable1", "TABLE1_ID", 1);
+        ObjectId t2Id = ObjectId.of("MtTable1", "TABLE1_ID", 2);
         t1.setObjectId(t1Id);
         t2.setObjectId(t2Id);
 
@@ -138,8 +138,8 @@ public class ClientExpressionIT extends ClientCase {
         assertEquals(t1.getObjectId(), values[0]);
         assertEquals(t2.getObjectId(), values[1]);
         
-        ObjectId t1Id = new ObjectId("MtTable1", "TABLE1_ID", 1);
-        ObjectId t2Id = new ObjectId("MtTable1", "TABLE1_ID", 2);
+        ObjectId t1Id = ObjectId.of("MtTable1", "TABLE1_ID", 1);
+        ObjectId t2Id = ObjectId.of("MtTable1", "TABLE1_ID", 2);
         t1.setObjectId(t1Id);
         t2.setObjectId(t2Id);
         
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java b/cayenne-client/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
index 7607e44..7acbd53 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
@@ -31,7 +31,7 @@ public class ObjectIdQueryTest {
 
     @Test
     public void testSerializabilityWithHessian() throws Exception {
-        ObjectId oid = new ObjectId("test", "a", "b");
+        ObjectId oid = ObjectId.of("test", "a", "b");
         ObjectIdQuery query = new ObjectIdQuery(oid);
 
         Object o = HessianUtil.cloneViaClientServerSerialization(query, new EntityResolver());
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/query/RelationshipQueryTest.java b/cayenne-client/src/test/java/org/apache/cayenne/query/RelationshipQueryTest.java
index c39447b..99f21bf 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/query/RelationshipQueryTest.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/query/RelationshipQueryTest.java
@@ -30,7 +30,7 @@ public class RelationshipQueryTest {
 
     @Test
     public void testSerializabilityWithHessian() throws Exception {
-        ObjectId oid = new ObjectId("test", "a", "b");
+        ObjectId oid = ObjectId.of("test", "a", "b");
         RelationshipQuery query = new RelationshipQuery(oid, "relX");
 
         RelationshipQuery q1 = (RelationshipQuery) HessianUtil.cloneViaClientServerSerialization(query,
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/remote/ClientChannelTest.java b/cayenne-client/src/test/java/org/apache/cayenne/remote/ClientChannelTest.java
index 3384b91..8b2d5c1 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/remote/ClientChannelTest.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/remote/ClientChannelTest.java
@@ -74,7 +74,7 @@ public class ClientChannelTest {
     public void testOnQuerySelect() {
 
         final MockPersistentObject o1 = new MockPersistentObject();
-        ObjectId oid1 = new ObjectId("test_entity");
+        ObjectId oid1 = ObjectId.of("test_entity");
         o1.setObjectId(oid1);
 
         ClientConnection connection = mock(ClientConnection.class);
@@ -135,7 +135,7 @@ public class ClientChannelTest {
         CayenneContext context = new CayenneContext();
         context.setEntityResolver(resolver);
 
-        ObjectId oid = new ObjectId("test_entity", "x", "y");
+        ObjectId oid = ObjectId.of("test_entity", "x", "y");
 
         MockPersistentObject o1 = new MockPersistentObject(oid);
         context.getGraphManager().registerNode(oid, o1);
@@ -176,7 +176,7 @@ public class ClientChannelTest {
         CayenneContext context = new CayenneContext();
         context.setEntityResolver(resolver);
 
-        ObjectId oid = new ObjectId("test_entity", "x", "y");
+        ObjectId oid = ObjectId.of("test_entity", "x", "y");
 
         MockPersistentObject o1 = new MockPersistentObject(oid);
         o1.setPersistenceState(PersistenceState.MODIFIED);
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/util/ObjectDetachOperationIT.java b/cayenne-client/src/test/java/org/apache/cayenne/util/ObjectDetachOperationIT.java
index 7bde106..6c01297 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/util/ObjectDetachOperationIT.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/util/ObjectDetachOperationIT.java
@@ -64,7 +64,7 @@ public class ObjectDetachOperationIT extends ClientCase {
         EntityResolver clientResolver = serverResover.getClientEntityResolver();
         ObjectDetachOperation op = new ObjectDetachOperation(clientResolver);
 
-        ObjectId oid = new ObjectId("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 456);
+        ObjectId oid = ObjectId.of("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 456);
         MtTable1 so = new MtTable1();
         so.setObjectId(oid);
         so.setGlobalAttribute1("gx");
@@ -96,7 +96,7 @@ public class ObjectDetachOperationIT extends ClientCase {
         EntityResolver clientResolver = serverResover.getClientEntityResolver();
         ObjectDetachOperation op = new ObjectDetachOperation(clientResolver);
 
-        ObjectId oid = new ObjectId("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 4);
+        ObjectId oid = ObjectId.of("MtTable1", MtTable1.TABLE1_ID_PK_COLUMN, 4);
         MtTable1 so = new MtTable1();
         so.setObjectId(oid);
         so.setPersistenceState(PersistenceState.HOLLOW);
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_AllIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_AllIT.java
index 62d8631..c73c143 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_AllIT.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_AllIT.java
@@ -115,7 +115,7 @@ public class CommitLogFilter_AllIT extends AuditableServerCase {
 				assertNotNull(changes);
 				assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange c = changes.getChanges().get(new ObjectId("Auditable1", Auditable1.ID_PK_COLUMN, 1));
+				ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
 				assertNotNull(c);
 				assertEquals(ObjectChangeType.UPDATE, c.getType());
 				assertEquals(1, c.getAttributeChanges().size());
@@ -151,7 +151,7 @@ public class CommitLogFilter_AllIT extends AuditableServerCase {
 				assertNotNull(changes);
 				assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange c = changes.getChanges().get(new ObjectId("Auditable1", Auditable1.ID_PK_COLUMN, 1));
+				ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
 				assertNotNull(c);
 				assertEquals(ObjectChangeType.DELETE, c.getType());
 				assertEquals(1, c.getAttributeChanges().size());
@@ -196,16 +196,16 @@ public class CommitLogFilter_AllIT extends AuditableServerCase {
 				assertEquals(4, changes.getUniqueChanges().size());
 
 				ObjectChange ac1c = changes.getChanges().get(
-						new ObjectId("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 1));
+						ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 1));
 				assertNotNull(ac1c);
 				assertEquals(ObjectChangeType.UPDATE, ac1c.getType());
 				ToOneRelationshipChange ac1c1 = ac1c.getToOneRelationshipChanges()
 						.get(AuditableChild1.PARENT.getName());
 				assertEquals(a1.getObjectId(), ac1c1.getOldValue());
-				assertEquals(null, ac1c1.getNewValue());
+                assertNull(ac1c1.getNewValue());
 
 				ObjectChange ac2c = changes.getChanges().get(
-						new ObjectId("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 2));
+						ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 2));
 				assertNotNull(ac2c);
 				assertEquals(ObjectChangeType.UPDATE, ac2c.getType());
 				ToOneRelationshipChange ac2c1 = ac2c.getToOneRelationshipChanges()
@@ -214,7 +214,7 @@ public class CommitLogFilter_AllIT extends AuditableServerCase {
 				assertEquals(a1.getObjectId(), ac2c1.getNewValue());
 
 				ObjectChange ac3c = changes.getChanges().get(
-						new ObjectId("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 3));
+						ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 3));
 				assertNotNull(ac3c);
 				assertEquals(ObjectChangeType.UPDATE, ac3c.getType());
 				ToOneRelationshipChange ac3c1 = ac3c.getToOneRelationshipChanges()
@@ -258,7 +258,7 @@ public class CommitLogFilter_AllIT extends AuditableServerCase {
 				assertNotNull(changes);
 				assertEquals(4, changes.getUniqueChanges().size());
 
-				ObjectChange a1c = changes.getChanges().get(new ObjectId("Auditable1", Auditable1.ID_PK_COLUMN, 1));
+				ObjectChange a1c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
 				assertNotNull(a1c);
 				assertEquals(ObjectChangeType.UPDATE, a1c.getType());
 				assertEquals(0, a1c.getAttributeChanges().size());
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
index caf4c0b..8542f96 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
@@ -77,7 +77,7 @@ public class CommitLogFilter_All_FlattenedIT extends FlattenedServerCase {
 				assertNotNull(changes);
 				assertEquals(3, changes.getUniqueChanges().size());
 
-				ObjectChange e3c = changes.getChanges().get(new ObjectId("E3", E3.ID_PK_COLUMN, 1));
+				ObjectChange e3c = changes.getChanges().get(ObjectId.of("E3", E3.ID_PK_COLUMN, 1));
 				assertNotNull(e3c);
 				assertEquals(ObjectChangeType.UPDATE, e3c.getType());
 				assertEquals(0, e3c.getAttributeChanges().size());
@@ -92,7 +92,7 @@ public class CommitLogFilter_All_FlattenedIT extends FlattenedServerCase {
 				assertEquals(1, e3c1.getRemoved().size());
 				assertTrue(e3c1.getRemoved().contains(e4_1.getObjectId()));
 				
-				ObjectChange e41c = changes.getChanges().get(new ObjectId("E4", E4.ID_PK_COLUMN, 11));
+				ObjectChange e41c = changes.getChanges().get(ObjectId.of("E4", E4.ID_PK_COLUMN, 11));
 				assertNotNull(e41c);
 				assertEquals(ObjectChangeType.UPDATE, e41c.getType());
 				assertEquals(0, e41c.getAttributeChanges().size());
@@ -106,7 +106,7 @@ public class CommitLogFilter_All_FlattenedIT extends FlattenedServerCase {
 				assertEquals(1, e41c1.getRemoved().size());
 				assertTrue(e41c1.getRemoved().contains(e3.getObjectId()));
 				
-				ObjectChange e42c = changes.getChanges().get(new ObjectId("E4", E4.ID_PK_COLUMN, 12));
+				ObjectChange e42c = changes.getChanges().get(ObjectId.of("E4", E4.ID_PK_COLUMN, 12));
 				assertNotNull(e42c);
 				assertEquals(ObjectChangeType.UPDATE, e42c.getType());
 				assertEquals(0, e42c.getAttributeChanges().size());
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_FilteredIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_FilteredIT.java
index eab650f..94335d1 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_FilteredIT.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_FilteredIT.java
@@ -109,7 +109,7 @@ public class CommitLogFilter_FilteredIT extends AuditableServerCase {
 				assertNotNull(changes);
 				assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange c = changes.getChanges().get(new ObjectId("Auditable2", Auditable2.ID_PK_COLUMN, 1));
+				ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable2", Auditable2.ID_PK_COLUMN, 1));
 				assertNotNull(c);
 				assertEquals(ObjectChangeType.UPDATE, c.getType());
 				assertEquals(1, c.getAttributeChanges().size());
@@ -144,7 +144,7 @@ public class CommitLogFilter_FilteredIT extends AuditableServerCase {
 				assertNotNull(changes);
 				assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange c = changes.getChanges().get(new ObjectId("Auditable2", Auditable2.ID_PK_COLUMN, 1));
+				ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable2", Auditable2.ID_PK_COLUMN, 1));
 				assertNotNull(c);
 				assertEquals(ObjectChangeType.DELETE, c.getType());
 				assertEquals(1, c.getAttributeChanges().size());
@@ -185,7 +185,7 @@ public class CommitLogFilter_FilteredIT extends AuditableServerCase {
 				assertNotNull(changes);
 				assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange a1c = changes.getChanges().get(new ObjectId("Auditable1", Auditable1.ID_PK_COLUMN, 1));
+				ObjectChange a1c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
 				assertNotNull(a1c);
 				assertEquals(ObjectChangeType.UPDATE, a1c.getType());
 				assertEquals(0, a1c.getAttributeChanges().size());
@@ -229,7 +229,7 @@ public class CommitLogFilter_FilteredIT extends AuditableServerCase {
 				assertSame(context, invocation.getArguments()[0]);
 
 				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNull(changes.getChanges().get(new ObjectId("Auditable3", Auditable3.ID_PK_COLUMN, 1)));
+				assertNull(changes.getChanges().get(ObjectId.of("Auditable3", Auditable3.ID_PK_COLUMN, 1)));
 
 				return null;
 			}
@@ -261,7 +261,7 @@ public class CommitLogFilter_FilteredIT extends AuditableServerCase {
 				assertSame(context, invocation.getArguments()[0]);
 
 				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNull(changes.getChanges().get(new ObjectId("Auditable3", Auditable3.ID_PK_COLUMN, 1)));
+				assertNull(changes.getChanges().get(ObjectId.of("Auditable3", Auditable3.ID_PK_COLUMN, 1)));
 
 				return null;
 			}
@@ -293,7 +293,7 @@ public class CommitLogFilter_FilteredIT extends AuditableServerCase {
 				assertSame(context, invocation.getArguments()[0]);
 
 				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNull(changes.getChanges().get(new ObjectId("Auditable4", Auditable4.ID_PK_COLUMN, 11)));
+				assertNull(changes.getChanges().get(ObjectId.of("Auditable4", Auditable4.ID_PK_COLUMN, 11)));
 
 				return null;
 			}
diff --git a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/id/EntityIdCoder.java b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/id/EntityIdCoder.java
index 76ee168..f305ba2 100644
--- a/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/id/EntityIdCoder.java
+++ b/cayenne-lifecycle/src/main/java/org/apache/cayenne/lifecycle/id/EntityIdCoder.java
@@ -145,7 +145,7 @@ public class EntityIdCoder {
 
         if (stringId.startsWith(TEMP_ID_PREFIX)) {
             String idValues = stringId.substring(entityName.length() + 1 + TEMP_PREFIX_LENGTH);
-            return new ObjectId(entityName, decodeTemp(idValues));
+            return ObjectId.of(entityName, decodeTemp(idValues));
         }
 
         String idValues = stringId.substring(entityName.length() + 1);
@@ -160,7 +160,7 @@ public class EntityIdCoder {
                 // unexpected
                 throw new CayenneRuntimeException("Unsupported encoding", e);
             }
-            return new ObjectId(entityName, entry.getKey(), entry.getValue().fromStringId(decoded));
+            return ObjectId.of(entityName, entry.getKey(), entry.getValue().fromStringId(decoded));
         }
 
         Map<String, Object> idMap = new HashMap<>(idSize);
@@ -185,7 +185,7 @@ public class EntityIdCoder {
             idMap.put(entry.getKey(), entry.getValue().fromStringId(decoded));
         }
 
-        return new ObjectId(entityName, idMap);
+        return ObjectId.of(entityName, idMap);
     }
 
     private byte[] decodeTemp(String byteString) {
diff --git a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/EntityIdCoderTest.java b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/EntityIdCoderTest.java
index 2bc0297..cc47c4b 100644
--- a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/EntityIdCoderTest.java
+++ b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/EntityIdCoderTest.java
@@ -64,7 +64,7 @@ public class EntityIdCoderTest {
         EntityIdCoder coder = new EntityIdCoder(e1);
 
         byte[] key = new byte[] { 2, 2, 10, 100 };
-        ObjectId encoded = new ObjectId("E1", key);
+        ObjectId encoded = ObjectId.of("E1", key);
 
         String string = coder.toStringId(encoded);
         assertEquals(".E1:02020A64", string);
@@ -87,7 +87,7 @@ public class EntityIdCoderTest {
         when(entity.getDbEntityName()).thenReturn(dbEntity.getName());
         when(entity.getDbEntity()).thenReturn(dbEntity);
 
-        ObjectId id = new ObjectId("x", "ID", 3);
+        ObjectId id = ObjectId.of("x", "ID", 3);
 
         EntityIdCoder coder = new EntityIdCoder(entity);
         assertEquals("x:3", coder.toStringId(id));
@@ -109,7 +109,7 @@ public class EntityIdCoderTest {
         when(entity.getDbEntityName()).thenReturn(dbEntity.getName());
         when(entity.getDbEntity()).thenReturn(dbEntity);
 
-        ObjectId id = new ObjectId("x", "ID", 3L);
+        ObjectId id = ObjectId.of("x", "ID", 3L);
 
         EntityIdCoder coder = new EntityIdCoder(entity);
         assertEquals("x:3", coder.toStringId(id));
@@ -133,7 +133,7 @@ public class EntityIdCoderTest {
 
         EntityIdCoder coder = new EntityIdCoder(entity);
 
-        ObjectId id = new ObjectId("x", "ID", "AbC");
+        ObjectId id = ObjectId.of("x", "ID", "AbC");
         assertEquals("x:AbC", coder.toStringId(id));
 
         ObjectId parsedId = coder.toObjectId("x:AbC");
@@ -155,7 +155,7 @@ public class EntityIdCoderTest {
 
         EntityIdCoder coder = new EntityIdCoder(entity);
 
-        ObjectId id = new ObjectId("x", "ID", "Ab:C");
+        ObjectId id = ObjectId.of("x", "ID", "Ab:C");
         assertEquals("x:Ab%3AC", coder.toStringId(id));
 
         ObjectId parsedId = coder.toObjectId("x:Ab%3AC");
@@ -191,7 +191,7 @@ public class EntityIdCoderTest {
         idMap.put("ID", "X;Y");
         idMap.put("ABC", 6783463L);
         idMap.put("ZZZ", "'_'");
-        ObjectId id = new ObjectId("x", idMap);
+        ObjectId id = ObjectId.of("x", idMap);
         assertEquals("x:6783463:X%3BY:%27_%27", coder.toStringId(id));
 
         ObjectId parsedId = coder.toObjectId("x:6783463:X%3BY:%27_%27");
diff --git a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/IdCoderTest.java b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/IdCoderTest.java
index c3a4876..de4539f 100644
--- a/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/IdCoderTest.java
+++ b/cayenne-lifecycle/src/test/java/org/apache/cayenne/lifecycle/id/IdCoderTest.java
@@ -46,14 +46,14 @@ public class IdCoderTest {
         IdCoder handler = new IdCoder(runtime.getChannel().getEntityResolver());
 
         E1 e1 = new E1();
-        e1.setObjectId(new ObjectId("E1", "ID", 5));
+        e1.setObjectId(ObjectId.of("E1", "ID", 5));
         assertEquals("E1:5", handler.getStringId(e1));
     }
 
     @Test
     public void testGetStringId_ObjectId() {
         IdCoder handler = new IdCoder(runtime.getChannel().getEntityResolver());
-        assertEquals("E1:5", handler.getStringId(new ObjectId("E1", "ID", 5)));
+        assertEquals("E1:5", handler.getStringId(ObjectId.of("E1", "ID", 5)));
     }
 
     @Test
@@ -63,7 +63,7 @@ public class IdCoderTest {
         byte[] key = new byte[] { 1, 2, 10, 100 };
 
         E1 e1 = new E1();
-        e1.setObjectId(new ObjectId("E1", key));
+        e1.setObjectId(ObjectId.of("E1", key));
 
         assertEquals(".E1:01020A64", handler.getStringId(e1));
     }
@@ -75,7 +75,7 @@ public class IdCoderTest {
         byte[] key = new byte[] { 1, (byte) 0xD7, 10, 100 };
 
         ObjectId decoded = handler.getObjectId(".E1:01D70A64");
-        assertEquals(new ObjectId("E1", key), decoded);
+        assertEquals(ObjectId.of("E1", key), decoded);
     }
 
     @Test
@@ -83,7 +83,7 @@ public class IdCoderTest {
         IdCoder handler = new IdCoder(runtime.getChannel().getEntityResolver());
 
         byte[] key = new byte[] { 5, 2, 11, 99 };
-        ObjectId id = new ObjectId("E1", key);
+        ObjectId id = ObjectId.of("E1", key);
         id.getReplacementIdMap().put("ID", 6);
 
         E1 e1 = new E1();
diff --git a/cayenne-protostuff/src/test/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactoryTest.java b/cayenne-protostuff/src/test/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactoryTest.java
index 7fe3dd2..277494f 100644
--- a/cayenne-protostuff/src/test/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactoryTest.java
+++ b/cayenne-protostuff/src/test/java/org/apache/cayenne/ObjectContextChangeLogSubListMessageFactoryTest.java
@@ -42,7 +42,7 @@ public class ObjectContextChangeLogSubListMessageFactoryTest extends ProtostuffP
     @Test
     public void testGetDiffsSerializable() throws Exception {
         ObjectContextChangeLog recorder = new ObjectContextChangeLog();
-        recorder.addOperation(new NodeCreateOperation(new ObjectId("test")));
+        recorder.addOperation(new NodeCreateOperation(ObjectId.of("test")));
         CompoundDiff diff = (CompoundDiff) recorder.getDiffs();
 
         byte[] data = serializationService.serialize(diff);
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContext.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContext.java
index 3f111db..29c5c06 100644
--- a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContext.java
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContext.java
@@ -325,8 +325,7 @@ public class CayenneContext extends BaseContext {
          */
         ObjectId id = object.getObjectId();
         if (id == null) {
-            id = new ObjectId(entityName);
-            object.setObjectId(id);
+            object.setObjectId(ObjectId.of(entityName));
         }
 
         injectInitialValue(object);
diff --git a/cayenne-rop-server/src/test/java/org/apache/cayenne/CayenneContextGraphManagerTest.java b/cayenne-rop-server/src/test/java/org/apache/cayenne/CayenneContextGraphManagerTest.java
index 1adfb52..21d8051 100644
--- a/cayenne-rop-server/src/test/java/org/apache/cayenne/CayenneContextGraphManagerTest.java
+++ b/cayenne-rop-server/src/test/java/org/apache/cayenne/CayenneContextGraphManagerTest.java
@@ -43,7 +43,7 @@ public class CayenneContextGraphManagerTest {
     @Test
     public void testRegisterNode() {
 
-        ObjectId id = new ObjectId("E1", "ID", 500);
+        ObjectId id = ObjectId.of("E1", "ID", 500);
         Persistent object = mock(Persistent.class);
 
         graphManager.registerNode(id, object);
@@ -53,7 +53,7 @@ public class CayenneContextGraphManagerTest {
     @Test
     public void testUnregisterNode() {
 
-        ObjectId id = new ObjectId("E1", "ID", 500);
+        ObjectId id = ObjectId.of("E1", "ID", 500);
         Persistent object = mock(Persistent.class);
 
         graphManager.registerNode(id, object);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/Cayenne.java b/cayenne-server/src/main/java/org/apache/cayenne/Cayenne.java
index 8098bf5..9dc11d5 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/Cayenne.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/Cayenne.java
@@ -354,7 +354,7 @@ public class Cayenne {
      */
     @SuppressWarnings("unchecked")
 	public static <T> T objectForPK(ObjectContext context, Class<T> dataObjectClass, int pk) {
-        return (T) objectForPK(context, buildId(context, dataObjectClass, Integer.valueOf(pk)));
+        return (T) objectForPK(context, buildId(context, dataObjectClass, pk));
     }
 
     /**
@@ -392,7 +392,7 @@ public class Cayenne {
             throw new CayenneRuntimeException("Non-existent ObjEntity for class: %s", dataObjectClass);
         }
 
-        return (T) objectForPK(context, new ObjectId(entity.getName(), pk));
+        return (T) objectForPK(context, ObjectId.of(entity.getName(), pk));
     }
 
     /**
@@ -441,7 +441,7 @@ public class Cayenne {
             throw new IllegalArgumentException("Null ObjEntity name.");
         }
 
-        return objectForPK(context, new ObjectId(objEntityName, pk));
+        return objectForPK(context, ObjectId.of(objEntityName, pk));
     }
 
     /**
@@ -495,7 +495,7 @@ public class Cayenne {
         }
 
         String attr = pkAttributes.iterator().next();
-        return new ObjectId(objEntityName, attr, pk);
+        return ObjectId.of(objEntityName, attr, pk);
     }
 
     static ObjectId buildId(ObjectContext context, Class<?> dataObjectClass, Object pk) {
@@ -518,7 +518,7 @@ public class Cayenne {
         }
 
         String attr = pkAttributes.iterator().next();
-        return new ObjectId(entity.getName(), attr, pk);
+        return ObjectId.of(entity.getName(), attr, pk);
     }
 
     protected Cayenne() {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/DataRow.java b/cayenne-server/src/main/java/org/apache/cayenne/DataRow.java
index e618182..ed1cf57 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/DataRow.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/DataRow.java
@@ -126,7 +126,7 @@ public class DataRow extends HashMap<String, Object> {
         }
 
         Map<String, Object> target = relationship.targetPkSnapshotWithSrcSnapshot(this);
-        return (target != null) ? new ObjectId(entityName, target) : null;
+        return (target != null) ? ObjectId.of(entityName, target) : null;
     }
 
     @Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/ObjectId.java b/cayenne-server/src/main/java/org/apache/cayenne/ObjectId.java
index f7d6b41..bd51310 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/ObjectId.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/ObjectId.java
@@ -19,368 +19,72 @@
 
 package org.apache.cayenne;
 
-import org.apache.cayenne.util.EqualsBuilder;
-import org.apache.cayenne.util.HashCodeBuilder;
-import org.apache.cayenne.util.IDUtil;
-import org.apache.cayenne.util.Util;
-
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 /**
+ * <p>
  * A portable global identifier for persistent objects. ObjectId can be
  * temporary (used for transient or new uncommitted objects) or permanent (used
- * for objects that have been already stored in DB). A temporary ObjectId stores
- * object entity name and a pseudo-unique binary key; permanent id stores a map
- * of values from an external persistent store (aka "primary key").
- * 
+ * for objects that have been already stored in DB).
+ * <p>
+ * A temporary ObjectId stores object entity name and a pseudo-unique binary key;
+ * permanent id stores a map of values from an external persistent store (aka "primary key").
  */
-public class ObjectId implements Serializable {
-
-	private static final long serialVersionUID = -2265029098344119323L;
-	
-	protected String entityName;
-	protected Map<String, Object> objectIdKeys;
-
-	private String singleKey;
-	private Object singleValue;
-
-	// key which is used for temporary ObjectIds only
-	protected byte[] key;
-
-	protected Map<String, Object> replacementIdMap;
-
-	// hash code is transient to make sure id is portable across VM
-	transient int hashCode;
-
-	// exists for deserialization with Hessian and similar
-	@SuppressWarnings("unused")
-	private ObjectId() {
-	}
-
-	/**
-	 * Creates a TEMPORARY ObjectId. Assigns a generated unique key.
-	 * 
-	 * @since 1.2
-	 */
-	public ObjectId(String entityName) {
-		this(entityName, IDUtil.pseudoUniqueByteSequence8());
-	}
-
-	/**
-	 * Creates a TEMPORARY id with a specified entity name and a binary key. It
-	 * is a caller responsibility to provide a globally unique binary key.
-	 * 
-	 * @since 1.2
-	 */
-	public ObjectId(String entityName, byte[] key) {
-		this.entityName = entityName;
-		this.key = key;
-	}
-
-	/**
-	 * Creates a portable permanent ObjectId.
-	 * 
-	 * @param entityName
-	 *            The entity name which this object id is for
-	 * @param key
-	 *            A key describing this object id, usually the attribute name
-	 *            for the primary key
-	 * @param value
-	 *            The unique value for this object id
-	 * @since 1.2
-	 */
-	public ObjectId(String entityName, String key, int value) {
-		this(entityName, key, Integer.valueOf(value));
-	}
-
-	/**
-	 * Creates a portable permanent ObjectId.
-	 * 
-	 * @param entityName
-	 *            The entity name which this object id is for
-	 * @param key
-	 *            A key describing this object id, usually the attribute name
-	 *            for the primary key
-	 * @param value
-	 *            The unique value for this object id
-	 * @since 1.2
-	 */
-	public ObjectId(String entityName, String key, Object value) {
-		this.entityName = entityName;
-
-		this.singleKey = key;
-		this.singleValue = value;
-	}
-
-	/**
-	 * Creates a portable permanent ObjectId as a compound primary key.
-	 * 
-	 * @param entityName
-	 *            The entity name which this object id is for
-	 * @param idMap
-	 *            Keys are usually the attribute names for each part of the
-	 *            primary key. Values are unique when taken as a whole.
-	 * @since 1.2
-	 */
-	public ObjectId(String entityName, Map<String, ?> idMap) {
-		this.entityName = entityName;
-
-		if (idMap == null || idMap.size() == 0) {
-
-		} else if (idMap.size() == 1) {
-			Map.Entry<String, ?> e = idMap.entrySet().iterator().next();
-			this.singleKey = String.valueOf(e.getKey());
-			this.singleValue = e.getValue();
-		} else {
-
-			// we have to create a copy of the map, otherwise we may run into
-			// serialization
-			// problems with hessian
-			this.objectIdKeys = new HashMap<>(idMap);
-		}
-	}
-
-	/**
-	 * Is this is temporary object id (used for objects which are not yet
-	 * persisted to the data store).
-	 */
-	public boolean isTemporary() {
-		return key != null;
-	}
-
-	/**
-	 * @since 1.2
-	 */
-	public String getEntityName() {
-		return entityName;
-	}
-
-	/**
-	 * Get the binary temporary object id. Null if this object id is permanent
-	 * (persisted to the data store).
-	 */
-	public byte[] getKey() {
-		return key;
-	}
-
-	/**
-	 * Returns an unmodifiable Map of persistent id values, essentially a
-	 * primary key map. For temporary id returns replacement id, if it was
-	 * already created. Otherwise returns an empty map.
-	 */
-	public Map<String, Object> getIdSnapshot() {
-		if (isTemporary()) {
-			return (replacementIdMap == null) ? Collections.<String, Object>emptyMap() : Collections.unmodifiableMap(replacementIdMap);
-		}
-
-		if (singleKey != null) {
-			return Collections.singletonMap(singleKey, singleValue);
-		}
-
-		return objectIdKeys != null ? Collections.unmodifiableMap(objectIdKeys) : Collections.<String, Object>emptyMap();
-	}
-
-	@Override
-	public boolean equals(Object object) {
-		if (this == object) {
-			return true;
-		}
-
-		if (!(object instanceof ObjectId)) {
-			return false;
-		}
-
-		ObjectId id = (ObjectId) object;
-
-		if (!Util.nullSafeEquals(entityName, id.entityName)) {
-			return false;
-		}
-
-		if (isTemporary()) {
-			return new EqualsBuilder().append(key, id.key).isEquals();
-		}
-
-		if (singleKey != null) {
-			return Util.nullSafeEquals(singleKey, id.singleKey) && valueEquals(singleValue, id.singleValue);
-		}
-
-		if (id.objectIdKeys == null) {
-			return objectIdKeys == null;
-		}
-
-		if (id.objectIdKeys.size() != objectIdKeys.size()) {
-			return false;
-		}
-
-		for (Map.Entry<String, ?> entry : objectIdKeys.entrySet()) {
-			String entryKey = entry.getKey();
-			Object entryValue = entry.getValue();
-
-			if (entryValue == null) {
-				if (id.objectIdKeys.get(entryKey) != null || !id.objectIdKeys.containsKey(entryKey)) {
-					return false;
-				}
-			} else {
-				if (!valueEquals(entryValue, id.objectIdKeys.get(entryKey))) {
-					return false;
-				}
-			}
-		}
-
-		return true;
-	}
-
-	private final boolean valueEquals(Object o1, Object o2) {
-		if (o1 == o2) {
-			return true;
-		}
-
-		if (o1 == null) {
-			return false;
-		}
-
-		if (o1 instanceof Number) {
-			return o2 instanceof Number && ((Number) o1).longValue() == ((Number) o2).longValue();
-		}
-
-		if (o1.getClass().isArray()) {
-			return new EqualsBuilder().append(o1, o2).isEquals();
-		}
-
-		return Util.nullSafeEquals(o1, o2);
-	}
-
-	@Override
-	public int hashCode() {
-
-		if (this.hashCode == 0) {
-
-			HashCodeBuilder builder = new HashCodeBuilder(3, 5);
-			builder.append(entityName.hashCode());
-
-			if (key != null) {
-				builder.append(key);
-			} else if (singleKey != null) {
-				builder.append(singleKey.hashCode());
-
-				// must reconcile all possible numeric types
-				if (singleValue instanceof Number) {
-					builder.append(((Number) singleValue).longValue());
-				} else {
-					builder.append(singleValue);
-				}
-			} else if (objectIdKeys != null) {
-				int len = objectIdKeys.size();
-
-				// handle multiple keys - must sort the keys to use with
-				// HashCodeBuilder
-
-				String[] keys = objectIdKeys.keySet().toArray(new String[0]);
-				Arrays.sort(keys);
-
-				for (int i = 0; i < len; i++) {
-					// HashCodeBuilder will take care of processing object if it
-					// happens to be a primitive array such as byte[]
+public interface ObjectId extends Serializable {
 
-					// also we don't have to append the key hashcode, its index
-					// will
-					// work
-					builder.append(i);
+    static ObjectId of(String entityName) {
+        return new ObjectIdTmp(entityName);
+    }
 
-					Object value = objectIdKeys.get(keys[i]);
-					// must reconcile all possible numeric types
-					if (value instanceof Number) {
-						builder.append(((Number) value).longValue());
-					} else {
-						builder.append(value);
-					}
-				}
-			}
+    static ObjectId of(String entityName, byte[] tmpKey) {
+        return new ObjectIdTmp(entityName, tmpKey);
+    }
 
-			this.hashCode = builder.toHashCode();
-			assert hashCode != 0 : "Generated zero hashCode";
-		}
+    static ObjectId of(String entityName, String keyName, Object value) {
+        if(value instanceof Number) {
+            return new ObjectIdNumber(entityName, keyName, (Number)value);
+        }
+        return new ObjectIdSingle(entityName, keyName, value);
+    }
 
-		return hashCode;
-	}
+    static ObjectId of(String entityName, ObjectId objectId) {
+        if(objectId instanceof ObjectIdNumber) {
+            ObjectIdNumber id = (ObjectIdNumber) objectId;
+            return new ObjectIdNumber(entityName, id.getKeyName(), id.getValue());
+        }
 
-	/**
-	 * Returns a non-null mutable map that can be used to append replacement id
-	 * values. This allows to incrementally build a replacement GlobalID.
-	 * 
-	 * @since 1.2
-	 */
-	public Map<String, Object> getReplacementIdMap() {
-		if (replacementIdMap == null) {
-			replacementIdMap = new HashMap<>();
-		}
+        if(objectId instanceof ObjectIdSingle) {
+            ObjectIdSingle id = (ObjectIdSingle) objectId;
+            return new ObjectIdSingle(entityName, id.getKeyName(), id.getValue());
+        }
 
-		return replacementIdMap;
-	}
+        if(objectId instanceof ObjectIdTmp) {
+            return of(entityName, objectId.getKey());
+        }
 
-	/**
-	 * Creates and returns a replacement ObjectId. No validation of ID is done.
-	 * 
-	 * @since 1.2
-	 */
-	public ObjectId createReplacementId() {
-		// merge existing and replaced ids to handle a replaced subset of
-		// a compound primary key
-		Map<String, Object> newIdMap = new HashMap<>(getIdSnapshot());
-		if (replacementIdMap != null) {
-			newIdMap.putAll(replacementIdMap);
-		}
-		return new ObjectId(getEntityName(), newIdMap);
-	}
+        return of(entityName, objectId.getIdSnapshot());
+    }
 
-	/**
-	 * Returns true if there is full or partial replacement id attached to this
-	 * id. This method is preferrable to "!getReplacementIdMap().isEmpty()" as
-	 * it avoids unneeded replacement id map creation.
-	 */
-	public boolean isReplacementIdAttached() {
-		return replacementIdMap != null && !replacementIdMap.isEmpty();
-	}
+    static ObjectId of(String entityName, Map<String, ?> values) {
+        if(values.size() == 1) {
+            Map.Entry<String, ?> entry = values.entrySet().iterator().next();
+            return of(entityName, entry.getKey(), entry.getValue());
+        }
+        return new ObjectIdCompound(entityName, values);
+    }
 
-	/**
-	 * A standard toString method used for debugging. It is guaranteed to
-	 * produce the same string if two ObjectIds are equal.
-	 */
-	@Override
-	public String toString() {
+    boolean isTemporary();
 
-		StringBuilder buffer = new StringBuilder();
+    String getEntityName();
 
-		buffer.append("<ObjectId:").append(entityName);
+    byte[] getKey();
 
-		if (isTemporary()) {
-			buffer.append(", TEMP:");
-			for (byte aKey : key) {
-				IDUtil.appendFormattedByte(buffer, aKey);
-			}
-		} else if (singleKey != null) {
-			buffer.append(", ").append(String.valueOf(singleKey)).append("=").append(singleValue);
-		} else if (objectIdKeys != null) {
+    Map<String, Object> getIdSnapshot();
 
-			// ensure consistent order of the keys, so that toString could be
-			// used as a
-			// unique key, just like id itself
+    Map<String, Object> getReplacementIdMap();
 
-			List<String> keys = new ArrayList<>(objectIdKeys.keySet());
-			Collections.sort(keys);
-			for (String key : keys) {
-				buffer.append(", ");
-				buffer.append(String.valueOf(key)).append("=").append(objectIdKeys.get(key));
-			}
-		}
+    ObjectId createReplacementId();
 
-		buffer.append(">");
-		return buffer.toString();
-	}
+    boolean isReplacementIdAttached();
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdCompound.java b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdCompound.java
new file mode 100644
index 0000000..e05886f
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdCompound.java
@@ -0,0 +1,255 @@
+/*****************************************************************
+ *   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;
+
+import org.apache.cayenne.util.HashCodeBuilder;
+import org.apache.cayenne.util.Util;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Compound {@link ObjectId}
+ * @since 4.2
+ */
+class ObjectIdCompound implements ObjectId {
+
+	private static final long serialVersionUID = -2265029098344119323L;
+	
+	protected final String entityName;
+	protected final Map<String, Object> objectIdKeys;
+
+	protected Map<String, Object> replacementIdMap;
+
+	// hash code is transient to make sure id is portable across VM
+	private transient int hashCode;
+
+	// exists for deserialization with Hessian and similar
+	@SuppressWarnings("unused")
+	private ObjectIdCompound() {
+		entityName = null;
+		objectIdKeys = Collections.emptyMap();
+	}
+
+	/**
+	 * Creates a portable permanent ObjectId as a compound primary key.
+	 * 
+	 * @param entityName
+	 *            The entity name which this object id is for
+	 * @param idMap
+	 *            Keys are usually the attribute names for each part of the
+	 *            primary key. Values are unique when taken as a whole.
+	 * @since 1.2
+	 */
+	ObjectIdCompound(String entityName, Map<String, ?> idMap) {
+		this.entityName = entityName;
+
+		if (idMap == null || idMap.size() == 0) {
+			this.objectIdKeys = Collections.emptyMap();
+			return;
+		}
+		this.objectIdKeys = wrapIdMap(idMap);
+	}
+
+	@SuppressWarnings("unchecked")
+	private Map<String, Object> wrapIdMap(Map<String, ?> m) {
+		if(m.getClass() == HashMap.class) {
+			return (Map<String, Object>)m;
+		} else {
+			// we have to create a copy of the map, otherwise we may run into serialization problems with hessian
+			return new HashMap<>(m);
+		}
+	}
+
+	/**
+	 * Is this is temporary object id (used for objects which are not yet
+	 * persisted to the data store).
+	 */
+	@Override
+	public boolean isTemporary() {
+		return false;
+	}
+
+	/**
+	 * @since 1.2
+	 */
+	@Override
+	public String getEntityName() {
+		return entityName;
+	}
+
+	/**
+	 * Get the binary temporary object id. Null if this object id is permanent
+	 * (persisted to the data store).
+	 */
+	@Override
+	public byte[] getKey() {
+		return null;
+	}
+
+	/**
+	 * Returns an unmodifiable Map of persistent id values, essentially a
+	 * primary key map. For temporary id returns replacement id, if it was
+	 * already created. Otherwise returns an empty map.
+	 */
+	@Override
+	public Map<String, Object> getIdSnapshot() {
+		return Collections.unmodifiableMap(objectIdKeys);
+	}
+
+	@Override
+	public boolean equals(Object object) {
+		if (this == object) {
+			return true;
+		}
+
+		if (!(object instanceof ObjectIdCompound)) {
+			return false;
+		}
+
+		ObjectIdCompound id = (ObjectIdCompound) object;
+		if (!Util.nullSafeEquals(entityName, id.entityName)) {
+			return false;
+		}
+
+		if (id.objectIdKeys.size() != objectIdKeys.size()) {
+			return false;
+		}
+
+		for (Map.Entry<String, ?> entry : objectIdKeys.entrySet()) {
+			String entryKey = entry.getKey();
+			Object entryValue = entry.getValue();
+
+			if (entryValue == null
+					&& (id.objectIdKeys.get(entryKey) != null || !id.objectIdKeys.containsKey(entryKey))) {
+				return false;
+			} else if (!valueEquals(entryValue, id.objectIdKeys.get(entryKey))) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	private boolean valueEquals(Object o1, Object o2) {
+		if (o1 == o2) {
+			return true;
+		}
+
+		if (o2 == null) {
+			return false;
+		}
+
+		if (o1 instanceof Number) {
+			return o2 instanceof Number && ((Number) o1).longValue() == ((Number) o2).longValue();
+		}
+
+		return Util.nullSafeEquals(o1, o2);
+	}
+
+	@Override
+	public int hashCode() {
+		if(hashCode != 0) {
+			return hashCode;
+		}
+
+		HashCodeBuilder builder = new HashCodeBuilder().append(entityName.hashCode());
+
+		// handle multiple keys - must sort the keys to use with HashCodeBuilder
+		String[] keys = objectIdKeys.keySet().toArray(new String[0]);
+		Arrays.sort(keys);
+		for (int i = 0; i < keys.length; i++) {
+			// HashCodeBuilder will take care of processing object if it
+			// happens to be a primitive array such as byte[]
+
+			// also we don't have to append the key hashcode, its index will work
+			builder.append(i);
+
+			Object value = objectIdKeys.get(keys[i]);
+			// must reconcile all possible numeric types
+			if (value instanceof Number) {
+				builder.append(((Number) value).longValue());
+			} else {
+				builder.append(value);
+			}
+		}
+		return hashCode = builder.toHashCode();
+	}
+
+	/**
+	 * Returns a non-null mutable map that can be used to append replacement id
+	 * values. This allows to incrementally build a replacement GlobalID.
+	 * 
+	 * @since 1.2
+	 */
+	@Override
+	public Map<String, Object> getReplacementIdMap() {
+		if (replacementIdMap == null) {
+			replacementIdMap = new HashMap<>();
+		}
+
+		return replacementIdMap;
+	}
+
+	/**
+	 * Creates and returns a replacement ObjectId. No validation of ID is done.
+	 * 
+	 * @since 1.2
+	 */
+	@Override
+	public ObjectId createReplacementId() {
+		if(replacementIdMap == null) {
+			return this;
+		}
+		// merge existing and replaced ids to handle a replaced subset of a compound primary key
+		Map<String, Object> newIdMap = new HashMap<>(objectIdKeys);
+		newIdMap.putAll(replacementIdMap);
+		return ObjectId.of(entityName, newIdMap);
+	}
+
+	/**
+	 * Returns true if there is full or partial replacement id attached to this
+	 * id. This method is preferable to "!getReplacementIdMap().isEmpty()" as
+	 * it avoids unneeded replacement id map creation.
+	 */
+	@Override
+	public boolean isReplacementIdAttached() {
+		return replacementIdMap != null && !replacementIdMap.isEmpty();
+	}
+
+	/**
+	 * A standard toString method used for debugging. It is guaranteed to
+	 * produce the same string if two ObjectIds are equal.
+	 */
+	@Override
+	public String toString() {
+		StringBuilder buffer = new StringBuilder().append("<ObjectId:").append(entityName);
+		// ensure consistent order of the keys, so that toString could be
+		// used as a unique key, just like id itself
+		String[] keys = objectIdKeys.keySet().toArray(new String[0]);
+		Arrays.sort(keys);
+		for (String key : keys) {
+			buffer.append(", ").append(key).append("=").append(objectIdKeys.get(key));
+		}
+		return buffer.append(">").toString();
+	}
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdNumber.java b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdNumber.java
new file mode 100644
index 0000000..d835c60
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdNumber.java
@@ -0,0 +1,126 @@
+/*****************************************************************
+ *   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;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.cayenne.util.SingleEntryMap;
+
+/**
+ * Optimized implementation of {@link ObjectId} for single numeric PK
+ * @since 4.2
+ */
+class ObjectIdNumber implements ObjectId {
+
+    private static final long serialVersionUID = 3968183354758914938L;
+
+    // this two fields can be kept somewhere else as ID index shared by all IDs
+    private final String entityName;
+    private final String keyName;
+
+    private final Number value;
+
+    private SingleEntryMap<String, Object> replacementId;
+
+    // exists for deserialization with Hessian and similar
+    @SuppressWarnings("unused")
+    private ObjectIdNumber() {
+        this.entityName = "";
+        this.keyName = "";
+        this.value = 0L;
+    }
+
+    ObjectIdNumber(String entityName, String keyName, Number value) {
+        this.entityName = entityName;
+        this.keyName = keyName;
+        this.value = value;
+    }
+
+    @Override
+    public boolean isTemporary() {
+        return false;
+    }
+
+    @Override
+    public String getEntityName() {
+        return entityName;
+    }
+
+    @Override
+    public byte[] getKey() {
+        return null;
+    }
+
+    @Override
+    public Map<String, Object> getIdSnapshot() {
+        return Collections.singletonMap(keyName, value);
+    }
+
+    @Override
+    public Map<String, Object> getReplacementIdMap() {
+        if(replacementId == null) {
+            replacementId = new SingleEntryMap<>(keyName);
+        }
+        return replacementId;
+    }
+
+    @Override
+    public ObjectId createReplacementId() {
+        Object newValue = replacementId == null ? null : replacementId.getValue();
+        return newValue == null ? this : ObjectId.of(entityName, keyName, newValue);
+    }
+
+    @Override
+    public boolean isReplacementIdAttached() {
+        return replacementId != null && !replacementId.isEmpty();
+    }
+
+    @Override
+    public String toString() {
+        return "<ObjectId:" + entityName + ", " + keyName + "=" + value + ">";
+    }
+
+    String getKeyName() {
+        return keyName;
+    }
+
+    Number getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        ObjectIdNumber that = (ObjectIdNumber) o;
+        return value.longValue() == that.value.longValue() && entityName.equals(that.entityName);
+    }
+
+    @Override
+    public int hashCode() {
+        long longValue = value.longValue();
+        return 31 * entityName.hashCode() + (int) (longValue ^ (longValue >>> 32));
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdSingle.java b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdSingle.java
new file mode 100644
index 0000000..2199126
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdSingle.java
@@ -0,0 +1,132 @@
+/*****************************************************************
+ *   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;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.cayenne.util.HashCodeBuilder;
+import org.apache.cayenne.util.SingleEntryMap;
+import org.apache.cayenne.util.Util;
+
+/**
+ * {@link ObjectId} with single non-numeric value
+ * @since 4.2
+ */
+class ObjectIdSingle implements ObjectId {
+
+    private static final long serialVersionUID = 3968183354758914938L;
+
+    private final String entityName;
+    private final String keyName;
+    private final Object value;
+    private transient int hashCode;
+
+    private SingleEntryMap<String, Object> replacementId;
+
+    // exists for deserialization with Hessian and similar
+    @SuppressWarnings("unused")
+    private ObjectIdSingle() {
+        this.entityName = "";
+        this.keyName = "";
+        this.value = null;
+    }
+
+    ObjectIdSingle(String entityName, String keyName, Object value) {
+        this.entityName = entityName;
+        this.keyName = keyName;
+        this.value = value;
+    }
+
+    @Override
+    public boolean isTemporary() {
+        return false;
+    }
+
+    @Override
+    public String getEntityName() {
+        return entityName;
+    }
+
+    @Override
+    public byte[] getKey() {
+        return null;
+    }
+
+    @Override
+    public Map<String, Object> getIdSnapshot() {
+        return Collections.singletonMap(keyName, value);
+    }
+
+    @Override
+    public Map<String, Object> getReplacementIdMap() {
+        if(replacementId == null) {
+            replacementId = new SingleEntryMap<>(keyName);
+        }
+        return replacementId;
+    }
+
+    @Override
+    public ObjectId createReplacementId() {
+        Object newValue = replacementId == null ? null : replacementId.getValue();
+        return newValue == null ? this : ObjectId.of(entityName, keyName, newValue);
+    }
+
+    @Override
+    public boolean isReplacementIdAttached() {
+        return replacementId != null && !replacementId.isEmpty();
+    }
+
+    @Override
+    public String toString() {
+        return "<ObjectId:" + entityName + ", " + keyName + "=" + value + ">";
+    }
+
+    String getKeyName() {
+        return keyName;
+    }
+
+    Object getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        ObjectIdSingle that = (ObjectIdSingle) o;
+        if(!Util.nullSafeEquals(entityName, that.entityName)) {
+            return false;
+        }
+        return Util.nullSafeEquals(value, that.value);
+    }
+
+    @Override
+    public int hashCode() {
+        if(hashCode == 0) {
+            hashCode = new HashCodeBuilder().append(entityName).append(value).toHashCode();
+        }
+        return hashCode;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdTmp.java b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdTmp.java
new file mode 100644
index 0000000..05964b8
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdTmp.java
@@ -0,0 +1,124 @@
+/*****************************************************************
+ *   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;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.cayenne.util.IDUtil;
+
+/**
+ * Tmp {@link ObjectId}
+ * @since 4.2
+ */
+class ObjectIdTmp implements ObjectId {
+
+    private static final long serialVersionUID = 6566399722364067372L;
+
+    private final String entityName;
+    private final byte[] id;
+
+    private Map<String, Object> replacementId;
+
+    // exists for deserialization with Hessian and similar
+    @SuppressWarnings("unused")
+    private ObjectIdTmp() {
+        entityName = null;
+        id = null;
+    }
+
+    ObjectIdTmp(String entityName, byte[] id) {
+        this.id = id;
+        this.entityName = entityName;
+    }
+
+    ObjectIdTmp(String entityName) {
+        this(entityName, IDUtil.pseudoUniqueByteSequence8());
+    }
+
+    @Override
+    public boolean isTemporary() {
+        return true;
+    }
+
+    @Override
+    public String getEntityName() {
+        return entityName;
+    }
+
+    @Override
+    public byte[] getKey() {
+        return id;
+    }
+
+    @Override
+    public Map<String, Object> getIdSnapshot() {
+        if(replacementId != null) {
+            return Collections.unmodifiableMap(replacementId);
+        }
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public Map<String, Object> getReplacementIdMap() {
+        if(replacementId == null) {
+            replacementId = new HashMap<>();
+        }
+        return replacementId;
+    }
+
+    @Override
+    public ObjectId createReplacementId() {
+        return ObjectId.of(entityName, replacementId);
+    }
+
+    @Override
+    public boolean isReplacementIdAttached() {
+        return replacementId != null && !replacementId.isEmpty();
+    }
+
+    @Override
+    public String toString() {
+        return "<ObjectId:" + entityName + ",TEMP:" + hashCode() + ">";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        ObjectIdTmp that = (ObjectIdTmp) o;
+        if (!Arrays.equals(id, that.id)) {
+            return false;
+        }
+        return entityName.equals(that.entityName);
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * entityName.hashCode() + Arrays.hashCode(id);
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContext.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContext.java
index 27460d2..57993f4 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContext.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContext.java
@@ -501,12 +501,8 @@ public class DataContext extends BaseContext {
         // this will initialize to-many lists
         descriptor.injectValueHolders(object);
 
-        ObjectId id = new ObjectId(entityName);
-
-        // note that the order of initialization of persistence artifacts below
-        // is
-        // important - do not change it lightly
-        object.setObjectId(id);
+        // NOTE: the order of initialization of persistence artifacts below is important - do not change it lightly
+        object.setObjectId(ObjectId.of(entityName));
 
         injectInitialValue(object);
 
@@ -548,7 +544,7 @@ public class DataContext extends BaseContext {
                         + "Try using 'localObjects()' instead.");
             }
         } else {
-            persistent.setObjectId(new ObjectId(entity.getName()));
+            persistent.setObjectId(ObjectId.of(entity.getName()));
         }
 
         ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(entity.getName());
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
index 57458a9..ad4f342 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java
@@ -189,22 +189,22 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver {
 
 		EntityInheritanceTree inheritanceTree = domain.getEntityResolver().getInheritanceTree(superOid.getEntityName());
 		if (!inheritanceTree.getChildren().isEmpty()) {
-			row = polymorphicRowFromCache(inheritanceTree, superOid.getIdSnapshot());
+			row = polymorphicRowFromCache(inheritanceTree, superOid);
 		}
 
 		return row;
 	}
     
-	private DataRow polymorphicRowFromCache(EntityInheritanceTree superNode, Map<String, ?> idSnapshot) {
+	private DataRow polymorphicRowFromCache(EntityInheritanceTree superNode, ObjectId superOid) {
 
 		for (EntityInheritanceTree child : superNode.getChildren()) {
-			ObjectId id = new ObjectId(child.getEntity().getName(), idSnapshot);
+			ObjectId id = ObjectId.of(child.getEntity().getName(), superOid);
 			DataRow row = cache.getCachedSnapshot(id);
 			if (row != null) {
 				return row;
 			}
 			
-			row = polymorphicRowFromCache(child, idSnapshot);
+			row = polymorphicRowFromCache(child, superOid);
 			if (row != null) {
 				return row;
 			}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java
index d658abf..088d341 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectResolver.java
@@ -244,7 +244,7 @@ class ObjectResolver {
 			}
 
 			// PUT without a prefix
-			return new ObjectId(name, attribute.getName(), val);
+			return ObjectId.of(name, attribute.getName(), val);
 		}
 
 		// ... handle generic case - PK.size > 1
@@ -268,7 +268,7 @@ class ObjectResolver {
 			idMap.put(attribute.getName(), val);
 		}
 
-		return new ObjectId(name, idMap);
+		return ObjectId.of(name, idMap);
 	}
 
 	interface DescriptorResolutionStrategy {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java
index 85f01e0..0a49a6b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java
@@ -218,22 +218,22 @@ public abstract class ObjectContextQueryAction {
 
 		EntityInheritanceTree inheritanceTree = actingContext.getEntityResolver().getInheritanceTree(superOid.getEntityName());
 		if (!inheritanceTree.getChildren().isEmpty()) {
-			object = polymorphicObjectFromCache(inheritanceTree, superOid.getIdSnapshot());
+			object = polymorphicObjectFromCache(inheritanceTree, superOid);
 		}
 
 		return object;
 	}
     
-	private Object polymorphicObjectFromCache(EntityInheritanceTree superNode, Map<String, ?> idSnapshot) {
+	private Object polymorphicObjectFromCache(EntityInheritanceTree superNode, ObjectId superOid) {
 
 		for (EntityInheritanceTree child : superNode.getChildren()) {
-			ObjectId id = new ObjectId(child.getEntity().getName(), idSnapshot);
+			ObjectId id = ObjectId.of(child.getEntity().getName(), superOid);
 			Object object = actingContext.getGraphManager().getNode(id);
 			if (object != null) {
 				return object;
 			}
 			
-			object = polymorphicObjectFromCache(child, idSnapshot);
+			object = polymorphicObjectFromCache(child, superOid);
 			if (object != null) {
 				return object;
 			}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/SingleEntryMap.java b/cayenne-server/src/main/java/org/apache/cayenne/util/SingleEntryMap.java
new file mode 100644
index 0000000..726840f
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/SingleEntryMap.java
@@ -0,0 +1,265 @@
+/*****************************************************************
+ *   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.util;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import static java.util.Collections.*;
+
+/**
+ * Optimized mutable single-entry map.
+ * <p>
+ * This implementation is compatible with general {@link Map} contract, including {@link Map#equals(Object)},
+ * {@link Map#hashCode()} and {@link java.util.AbstractMap#toString()} implementations.
+ * <p>
+ * This Map can store only one key that is defined at creation time and can't be changed.
+ * This map will throw {@link IllegalArgumentException} on any put operation with the wrong key
+ * and return {@code null} on get.
+ * <p>
+ * This map will be effectively empty after putting null value.
+ *
+ * @since 4.2
+ */
+public class SingleEntryMap<K, V> implements Map<K, V>, Map.Entry<K, V>, Serializable {
+
+    private static final long serialVersionUID = -3848347748971431847L;
+
+    private final K key;
+    private V value;
+
+    /**
+     * Create empty map
+     *
+     * @param key that can be stored in this map, can't be null
+     */
+    public SingleEntryMap(K key) {
+        this(key, null);
+    }
+
+    /**
+     * Create map with single key-value entry
+     *
+     * @param key that can be stored in this map, can't be null
+     * @param value to store, if null map will be empty.
+     */
+    public SingleEntryMap(K key, V value) {
+        this.key = Objects.requireNonNull(key);
+        this.value = value;
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        return this.value == null ? emptySet() : singleton(this);
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return this.value != null && this.key.equals(key);
+    }
+
+    @Override
+    public int size() {
+        return this.value == null ? 0 : 1;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return this.value == null;
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return value != null && value.equals(this.value);
+    }
+
+    @Override
+    public V get(Object key) {
+        return this.key.equals(key) ? this.value : null;
+    }
+
+    @Override
+    public V put(K key, V value) {
+        if(this.key.equals(key)) {
+            return setValue(value);
+        }
+        throw new IllegalArgumentException("This map supports only key '" + this.key + "'");
+    }
+
+    @Override
+    public V remove(Object key) {
+        return this.key.equals(key) ? setValue(null) : null;
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> map) {
+        map.forEach(this::put);
+    }
+
+    @Override
+    public void clear() {
+        value = null;
+    }
+
+    @Override
+    public Set<K> keySet() {
+        return value == null ? emptySet() : singleton(key);
+    }
+
+    @Override
+    public Collection<V> values() {
+        return value == null ? emptySet() : singleton(value);
+    }
+
+    @Override
+    public K getKey() {
+        return key;
+    }
+
+    @Override
+    public V getValue() {
+        return value;
+    }
+
+    @Override
+    public V setValue(V value) {
+        V oldValue = this.value;
+        this.value = value;
+        return oldValue;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof Map)) {
+            return false;
+        }
+        Map<?,?> m = (Map<?,?>) o;
+        return m.size() == size() && (value == null || value.equals(m.get(key)));
+    }
+
+    @Override
+    public int hashCode() {
+        return value == null ? 0 : key.hashCode() ^ value.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return value == null ? "{}" : "{" + key + "=" + value + "}";
+    }
+
+    /* below is a set of methods with default implementation in Map interface */
+
+    @Override
+    public void forEach(BiConsumer<? super K, ? super V> action) {
+        if(value != null) {
+            action.accept(key, value);
+        }
+    }
+
+    @Override
+    public V getOrDefault(Object key, V defaultValue) {
+        return this.key.equals(key) && value != null ? value : defaultValue;
+    }
+
+    @Override
+    public V putIfAbsent(K key, V value) {
+        if(this.key.equals(key)) {
+            if (this.value == null) {
+                this.value = value;
+                return null;
+            }
+            return this.value;
+        }
+        throw new IllegalArgumentException("This map supports only key '" + this.key + "'");
+    }
+
+    @Override
+    public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
+        if(this.key.equals(key)) {
+            if(value == null) {
+                value = mappingFunction.apply(key);
+            }
+            return value;
+        }
+        throw new IllegalArgumentException("This map supports only key '" + this.key + "'");
+    }
+
+    @Override
+    public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+        if (value != null && this.key.equals(key)) {
+            return value = remappingFunction.apply(key, value);
+        }
+        return null;
+    }
+
+    @Override
+    public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+        if(this.key.equals(key)) {
+            return value = remappingFunction.apply(key, value);
+        }
+        throw new IllegalArgumentException("This map supports only key '" + this.key + "'");
+    }
+
+
+    @Override
+    public V merge(K key, V newValue, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
+        if(this.key.equals(key)) {
+            return value = value == null ? newValue : remappingFunction.apply(value, newValue);
+        }
+        throw new IllegalArgumentException("This map supports only key '" + this.key + "'");
+    }
+
+    @Override
+    public V replace(K key, V value) {
+        if(this.key.equals(key) && this.value != null) {
+            V oldValue = this.value;
+            this.value = value;
+            return oldValue;
+        }
+        return null;
+    }
+
+    @Override
+    public boolean replace(K key, V oldValue, V newValue) {
+        if(this.key.equals(key) && value != null && value.equals(oldValue)) {
+            value = newValue;
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean remove(Object key, Object value) {
+        if(this.key.equals(key) && this.value != null && this.value.equals(value)) {
+            this.value = null;
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/CayenneDataObjectIT.java b/cayenne-server/src/test/java/org/apache/cayenne/CayenneDataObjectIT.java
index 01c39b5..f625ee6 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/CayenneDataObjectIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/CayenneDataObjectIT.java
@@ -45,7 +45,7 @@ public class CayenneDataObjectIT extends ServerCase {
 	@Test
 	public void testSetObjectId() throws Exception {
 		CayenneDataObject object = new CayenneDataObject();
-		ObjectId oid = new ObjectId("T");
+        ObjectId oid = ObjectId.of("T");
 
 		assertNull(object.getObjectId());
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/CayenneDataObjectInContextIT.java b/cayenne-server/src/test/java/org/apache/cayenne/CayenneDataObjectInContextIT.java
index a99918c..76751cf 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/CayenneDataObjectInContextIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/CayenneDataObjectInContextIT.java
@@ -299,10 +299,10 @@ public class CayenneDataObjectInContextIT extends ServerCase {
         assertEquals(PersistenceState.NEW, object.getPersistenceState());
 
         // do a manual id substitution
-        object.setObjectId(new ObjectId(
+        object.setObjectId(ObjectId.of(
                 "Artist",
                 Artist.ARTIST_ID_PK_COLUMN,
-                new Integer(3)));
+                3));
 
         context.commitChanges();
         assertEquals(PersistenceState.COMMITTED, object.getPersistenceState());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java b/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java
index fde78e9..449a51c 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/CayenneIT.java
@@ -152,8 +152,7 @@ public class CayenneIT extends ServerCase {
     public void testObjectForQuery() throws Exception {
         createOneArtist();
 
-        ObjectId id = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, new Integer(
-                33002));
+        ObjectId id = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 33002);
 
         assertNull(context.getGraphManager().getNode(id));
 
@@ -180,8 +179,7 @@ public class CayenneIT extends ServerCase {
     @Test
     public void testObjectForQueryNoObject() throws Exception {
 
-        ObjectId id = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, new Integer(
-                44001));
+        ObjectId id = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 44001);
 
         Object object = Cayenne.objectForQuery(context, new ObjectIdQuery(id));
         assertNull(object);
@@ -204,16 +202,14 @@ public class CayenneIT extends ServerCase {
         assertSame(o1, Cayenne.objectForPK(context, o1.getObjectId()));
         assertSame(o2, Cayenne.objectForPK(context, o2.getObjectId()));
 
-        assertNull(Cayenne.objectForPK(context, new ObjectId("Artist", new byte[] {
-                1, 2, 3
-        })));
+        assertNull(Cayenne.objectForPK(context, ObjectId.of("Artist", new byte[] {1, 2, 3})));
     }
 
     @Test
     public void testObjectForPKObjectId() throws Exception {
         createOneArtist();
 
-        Object object = Cayenne.objectForPK(context, new ObjectId(
+        Object object = Cayenne.objectForPK(context, ObjectId.of(
                 "Artist",
                 Artist.ARTIST_ID_PK_COLUMN,
                 33002));
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/ContextStateRecorderTest.java b/cayenne-server/src/test/java/org/apache/cayenne/ContextStateRecorderTest.java
index 2aa24c1..230fcc7 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/ContextStateRecorderTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/ContextStateRecorderTest.java
@@ -59,7 +59,7 @@ public class ContextStateRecorderTest {
 		assertTrue(recorder.dirtyNodes(PersistenceState.HOLLOW).isEmpty());
 
 		MockPersistentObject modified = new MockPersistentObject();
-		modified.setObjectId(new ObjectId("MockPersistentObject", "key", "value1"));
+		modified.setObjectId(ObjectId.of("MockPersistentObject", "key", "value1"));
 		modified.setPersistenceState(PersistenceState.MODIFIED);
 		
 		when(mockGraphManager.getNode(modified.getObjectId())).thenReturn(modified);
@@ -73,7 +73,7 @@ public class ContextStateRecorderTest {
 		assertTrue(recorder.dirtyNodes(PersistenceState.HOLLOW).isEmpty());
 
 		MockPersistentObject deleted = new MockPersistentObject();
-		deleted.setObjectId(new ObjectId("MockPersistentObject", "key", "value2"));
+		deleted.setObjectId(ObjectId.of("MockPersistentObject", "key", "value2"));
 		deleted.setPersistenceState(PersistenceState.DELETED);
 		when(mockGraphManager.getNode(deleted.getObjectId())).thenReturn(deleted);
 		recorder.nodeRemoved(deleted.getObjectId());
@@ -94,7 +94,7 @@ public class ContextStateRecorderTest {
 
 		// introduce a fake dirty object
 		MockPersistentObject object = new MockPersistentObject();
-		object.setObjectId(new ObjectId("MockPersistentObject", "key", "value"));
+		object.setObjectId(ObjectId.of("MockPersistentObject", "key", "value"));
 		object.setPersistenceState(PersistenceState.MODIFIED);
 
 		when(mockGraphManager.getNode(object.getObjectId())).thenReturn(object);
@@ -116,7 +116,7 @@ public class ContextStateRecorderTest {
 
 		// introduce a fake dirty object
 		MockPersistentObject object = new MockPersistentObject();
-		object.setObjectId(new ObjectId("MockPersistentObject", "key", "value"));
+		object.setObjectId(ObjectId.of("MockPersistentObject", "key", "value"));
 		object.setPersistenceState(PersistenceState.MODIFIED);
 		recorder.nodePropertyChanged(object.getObjectId(), "xyz", "a", "b");
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/ObjectContextChangeLogTest.java b/cayenne-server/src/test/java/org/apache/cayenne/ObjectContextChangeLogTest.java
index aef1c9e..e0e9af7 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/ObjectContextChangeLogTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/ObjectContextChangeLogTest.java
@@ -75,7 +75,7 @@ public class ObjectContextChangeLogTest {
     @Test
     public void testGetDiffsSerializable() throws Exception {
         ObjectContextChangeLog recorder = new ObjectContextChangeLog();
-        recorder.addOperation(new NodeCreateOperation(new ObjectId("test")));
+        recorder.addOperation(new NodeCreateOperation(ObjectId.of("test")));
         CompoundDiff diff = (CompoundDiff) recorder.getDiffs();
 
         Object clone = Util.cloneViaSerialization(diff);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/ObjectIdRegressionTest.java b/cayenne-server/src/test/java/org/apache/cayenne/ObjectIdRegressionTest.java
index 38ca185..327649f 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/ObjectIdRegressionTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/ObjectIdRegressionTest.java
@@ -34,13 +34,13 @@ public class ObjectIdRegressionTest {
 
         int size = 100000;
 
-        new ObjectId("Artist");
+        ObjectId.of("Artist");
         ObjectId[] pool = new ObjectId[size];
 
         long t0 = System.currentTimeMillis();
         // fill in
         for (int i = 0; i < size; i++) {
-            pool[i] = new ObjectId("Artist");
+            pool[i] = ObjectId.of("Artist");
         }
 
         long t1 = System.currentTimeMillis();
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/ObjectIdTest.java b/cayenne-server/src/test/java/org/apache/cayenne/ObjectIdTest.java
index 2ed72ad..dba358b 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/ObjectIdTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/ObjectIdTest.java
@@ -27,24 +27,19 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 
 public class ObjectIdTest {
 
     @Test
     public void testConstructor() {
-        ObjectId temp1 = new ObjectId("e");
+        ObjectId temp1 = ObjectId.of("e");
         assertEquals("e", temp1.getEntityName());
         assertTrue(temp1.isTemporary());
         assertNotNull(temp1.getKey());
 
         byte[] key = new byte[] { 1, 2, 3 };
-        ObjectId temp2 = new ObjectId("e1", key);
+        ObjectId temp2 = ObjectId.of("e1", key);
         assertEquals("e1", temp2.getEntityName());
         assertTrue(temp2.isTemporary());
         assertSame(key, temp2.getKey());
@@ -52,7 +47,7 @@ public class ObjectIdTest {
 
     @Test
     public void testSerializabilityTemp() throws Exception {
-        ObjectId temp1 = new ObjectId("e");
+        ObjectId temp1 = ObjectId.of("e");
         ObjectId temp2 = Util.cloneViaSerialization(temp1);
 
         assertTrue(temp1.isTemporary());
@@ -62,17 +57,16 @@ public class ObjectIdTest {
 
     @Test
     public void testSerializabilityPerm() throws Exception {
-        ObjectId perm1 = new ObjectId("e", "a", "b");
+        ObjectId perm1 = ObjectId.of("e", "a", "b");
 
         // make sure hashcode is resolved
         int h = perm1.hashCode();
-        assertEquals(h, perm1.hashCode);
-        assertTrue(perm1.hashCode != 0);
+        assertEquals(h, perm1.hashCode());
+        assertTrue(perm1.hashCode() != 0);
 
         ObjectId perm2 = Util.cloneViaSerialization(perm1);
 
-        // make sure hashCode is reset to 0
-        assertTrue(perm2.hashCode == 0);
+        assertEquals(h, perm1.hashCode());
 
         assertFalse(perm2.isTemporary());
         assertNotSame(perm1, perm2);
@@ -80,31 +74,55 @@ public class ObjectIdTest {
     }
 
     @Test
-    public void testEquals0() {
-        ObjectId oid1 = new ObjectId("TE");
+    public void testEqualsTmoKey() {
+        ObjectId oid1 = ObjectId.of("TE");
         assertEquals(oid1, oid1);
         assertEquals(oid1.hashCode(), oid1.hashCode());
     }
 
     @Test
-    public void testEquals1() {
-        ObjectId oid1 = new ObjectId("T", "a", "b");
-        ObjectId oid2 = new ObjectId("T", "a", "b");
+    public void testEqualsSingleValueKeyStr() {
+        ObjectId oid1 = ObjectId.of("T", "a", "b");
+        ObjectId oid2 = ObjectId.of("T", "a", "b");
         assertEquals(oid1, oid2);
         assertEquals(oid1.hashCode(), oid2.hashCode());
     }
 
     @Test
-    public void testEquals2() {
+    public void testNotEqualsSingleValueKeyStr() {
+        ObjectId oid1 = ObjectId.of("T", "a", "a");
+        ObjectId oid2 = ObjectId.of("T", "a", "b");
+        assertNotEquals(oid1, oid2);
+        assertNotEquals(oid1.hashCode(), oid2.hashCode());
+    }
+
+    @Test
+    public void testEqualsSingleValueKeyNumeric() {
+        ObjectId oid1 = ObjectId.of("T", "a", 42);
+        ObjectId oid2 = ObjectId.of("T", "a", BigDecimal.valueOf(42));
+        assertEquals(oid1, oid2);
+        assertEquals(oid1.hashCode(), oid2.hashCode());
+    }
+
+    @Test
+    public void testNotEqualsSingleValueKeyNumeric() {
+        ObjectId oid1 = ObjectId.of("T", "a", 41);
+        ObjectId oid2 = ObjectId.of("T", "a", BigDecimal.valueOf(42));
+        assertNotEquals(oid1, oid2);
+        assertNotEquals(oid1.hashCode(), oid2.hashCode());
+    }
+
+    @Test
+    public void testEqualsCompoundKeyNoValues() {
         Map<String, Object> hm = new HashMap<>();
-        ObjectId oid1 = new ObjectId("T", hm);
-        ObjectId oid2 = new ObjectId("T", hm);
+        ObjectId oid1 = ObjectId.of("T", hm);
+        ObjectId oid2 = ObjectId.of("T", hm);
         assertEquals(oid1, oid2);
         assertEquals(oid1.hashCode(), oid2.hashCode());
     }
 
     @Test
-    public void testEquals3() {
+    public void testEqualsSingleKeyFromMap() {
         String pknm = "xyzabc";
 
         Map<String, Object> hm1 = new HashMap<>();
@@ -113,8 +131,8 @@ public class ObjectIdTest {
         Map<String, Object> hm2 = new HashMap<>();
         hm2.put(pknm, "123");
 
-        ObjectId oid1 = new ObjectId("T", hm1);
-        ObjectId oid2 = new ObjectId("T", hm2);
+        ObjectId oid1 = ObjectId.of("T", hm1);
+        ObjectId oid2 = ObjectId.of("T", hm2);
         assertEquals(oid1, oid2);
         assertEquals(oid1.hashCode(), oid2.hashCode());
     }
@@ -123,7 +141,7 @@ public class ObjectIdTest {
      * This is a test case reproducing conditions for the bug "8458963".
      */
     @Test
-    public void testEquals5() {
+    public void testNotEqualsCompoundKey() {
 
         Map<String, Object> hm1 = new HashMap<>();
         hm1.put("key1", 1);
@@ -133,16 +151,16 @@ public class ObjectIdTest {
         hm2.put("key1", 11);
         hm2.put("key2", 1);
 
-        ObjectId ref = new ObjectId("T", hm1);
-        ObjectId oid = new ObjectId("T", hm2);
-        assertFalse(ref.equals(oid));
+        ObjectId ref = ObjectId.of("T", hm1);
+        ObjectId oid = ObjectId.of("T", hm2);
+        assertNotEquals(ref, oid);
     }
 
     /**
      * Multiple key objectId
      */
     @Test
-    public void testEquals6() {
+    public void testEqualsCompoundKey1() {
 
         Map<String, Object> hm1 = new HashMap<>();
         hm1.put("key1", 1);
@@ -152,9 +170,9 @@ public class ObjectIdTest {
         hm2.put("key1", 1);
         hm2.put("key2", 2);
 
-        ObjectId ref = new ObjectId("T", hm1);
-        ObjectId oid = new ObjectId("T", hm2);
-        assertTrue(ref.equals(oid));
+        ObjectId ref = ObjectId.of("T", hm1);
+        ObjectId oid = ObjectId.of("T", hm2);
+        assertEquals(ref, oid);
         assertEquals(ref.hashCode(), oid.hashCode());
     }
 
@@ -163,24 +181,43 @@ public class ObjectIdTest {
      * different order...
      */
     @Test
-    public void testEquals7() {
+    public void testEqualsCompoundKey2() {
 
         // create maps with guaranteed iteration order
-
-        @SuppressWarnings("unchecked")
         Map<String, Object> hm1 = new LinkedHashMap<>();
         hm1.put("KEY1", 1);
         hm1.put("KEY2", 2);
 
-        @SuppressWarnings("unchecked")
-        Map<String, Object> hm2 = new LinkedHashMap();
+        Map<String, Object> hm2 = new LinkedHashMap<>();
         // put same keys but in different order
         hm2.put("KEY2", 2);
         hm2.put("KEY1", 1);
 
-        ObjectId ref = new ObjectId("T", hm1);
-        ObjectId oid = new ObjectId("T", hm2);
-        assertTrue(ref.equals(oid));
+        ObjectId ref = ObjectId.of("T", hm1);
+        ObjectId oid = ObjectId.of("T", hm2);
+        assertEquals(ref, oid);
+        assertEquals(ref.hashCode(), oid.hashCode());
+    }
+
+    /**
+     * Test different numeric types.
+     */
+    @Test
+    public void testEqualsCompoundKey3() {
+
+        // create maps with guaranteed iteration order
+        Map<String, Object> hm1 = new LinkedHashMap<>();
+        hm1.put("KEY1", 1);
+        hm1.put("KEY2", 2);
+
+        Map<String, Object> hm2 = new LinkedHashMap<>();
+        // put same keys but in different order
+        hm2.put("KEY2", new BigDecimal(2.00));
+        hm2.put("KEY1", 1L);
+
+        ObjectId ref = ObjectId.of("T", hm1);
+        ObjectId oid = ObjectId.of("T", hm2);
+        assertEquals(ref, oid);
         assertEquals(ref.hashCode(), oid.hashCode());
     }
 
@@ -193,16 +230,16 @@ public class ObjectIdTest {
         Map<String, Object> hm2 = new HashMap<>();
         hm2.put("key1", new byte[] { 3, 4, 10, -1 });
 
-        ObjectId ref = new ObjectId("T", hm1);
-        ObjectId oid = new ObjectId("T", hm2);
+        ObjectId ref = ObjectId.of("T", hm1);
+        ObjectId oid = ObjectId.of("T", hm2);
         assertEquals(ref.hashCode(), oid.hashCode());
-        assertTrue(ref.equals(oid));
+        assertEquals(ref, oid);
     }
 
     @Test
     public void testEqualsNull() {
-        ObjectId o = new ObjectId("T", "ARTIST_ID", new Integer(42));
-        assertFalse(o.equals(null));
+        ObjectId o = ObjectId.of("T", "ARTIST_ID", 42);
+        assertNotNull(o);
     }
 
     @Test
@@ -218,33 +255,33 @@ public class ObjectIdTest {
         Map<String, Object> hm2 = new HashMap<>();
         hm2.put(pknm, "123");
 
-        ObjectId oid1 = new ObjectId("T", hm1);
-        ObjectId oid2 = new ObjectId("T", hm2);
+        ObjectId oid1 = ObjectId.of("T", hm1);
+        ObjectId oid2 = ObjectId.of("T", hm2);
 
         map.put(oid1, o1);
         assertSame(o1, map.get(oid2));
     }
 
     @Test
-    public void testNotEqual1() {
+    public void testNotEqualTmpKey() {
 
-        ObjectId oid1 = new ObjectId("T1");
-        ObjectId oid2 = new ObjectId("T2");
-        assertFalse(oid1.equals(oid2));
+        ObjectId oid1 = ObjectId.of("T1");
+        ObjectId oid2 = ObjectId.of("T2");
+        assertNotEquals(oid1, oid2);
     }
 
     @Test
-    public void testNotEqual2() {
+    public void testNotEqualSingleValueKey() {
 
         Map<String, Object> hm1 = new HashMap<>();
         hm1.put("pk1", "123");
 
         Map<String, Object> hm2 = new HashMap<>();
-        hm2.put("pk2", "123");
+        hm2.put("pk1", "124");
 
-        ObjectId oid1 = new ObjectId("T", hm1);
-        ObjectId oid2 = new ObjectId("T", hm2);
-        assertFalse(oid1.equals(oid2));
+        ObjectId oid1 = ObjectId.of("T", hm1);
+        ObjectId oid2 = ObjectId.of("T", hm2);
+        assertNotEquals(oid1, oid2);
     }
 
     /**
@@ -255,20 +292,18 @@ public class ObjectIdTest {
 
         // create maps with guaranteed iteration order
 
-        @SuppressWarnings("unchecked")
-        Map<String, Object> hm1 = new LinkedHashMap();
+        Map<String, Object> hm1 = new LinkedHashMap<>();
         hm1.put("KEY1", 1);
         hm1.put("KEY2", 2);
 
-        @SuppressWarnings("unchecked")
-        Map<String, Object> hm2 = new LinkedHashMap();
+        Map<String, Object> hm2 = new LinkedHashMap<>();
         // put same keys but in different order
         hm2.put("KEY2", new BigDecimal(2.00));
         hm2.put("KEY1", 1L);
 
-        ObjectId ref = new ObjectId("T", hm1);
-        ObjectId oid = new ObjectId("T", hm2);
-        assertTrue(ref.equals(oid));
+        ObjectId ref = ObjectId.of("T", hm1);
+        ObjectId oid = ObjectId.of("T", hm2);
+        assertEquals(ref, oid);
         assertEquals(ref.hashCode(), oid.hashCode());
     }
 
@@ -277,13 +312,13 @@ public class ObjectIdTest {
         Map<String, Object> m1 = new HashMap<>();
         m1.put("a", "1");
         m1.put("b", "2");
-        ObjectId i1 = new ObjectId("e1", m1);
+        ObjectId i1 = ObjectId.of("e1", m1);
 
         Map<String, Object> m2 = new HashMap<>();
         m2.put("b", "2");
         m2.put("a", "1");
 
-        ObjectId i2 = new ObjectId("e1", m2);
+        ObjectId i2 = ObjectId.of("e1", m2);
 
         assertEquals(i1, i2);
         assertEquals(i1.toString(), i2.toString());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/PersistentObjectIT.java b/cayenne-server/src/test/java/org/apache/cayenne/PersistentObjectIT.java
index 4b240b3..8cad206 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/PersistentObjectIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/PersistentObjectIT.java
@@ -52,7 +52,7 @@ public class PersistentObjectIT extends ServerCase {
 
     @Test
     public void testObjectID() {
-        ObjectId id = new ObjectId("test");
+        ObjectId id = ObjectId.of("test");
 
         PersistentObject object = new MockPersistentObject();
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java
index ee5c87b..31136a0 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java
@@ -56,7 +56,7 @@ public class DataContextEntityWithMeaningfulPKIT extends ServerCase {
         obj.setPkAttribute(1000);
         obj.setDescr("aaa-aaa");
         context.commitChanges();
-        ObjectId objId = new ObjectId("MeaningfulPKTest1", MeaningfulPKTest1.PK_ATTRIBUTE_PK_COLUMN, 1000);
+        ObjectId objId = ObjectId.of("MeaningfulPKTest1", MeaningfulPKTest1.PK_ATTRIBUTE_PK_COLUMN, 1000);
         ObjectIdQuery q = new ObjectIdQuery(objId, true, ObjectIdQuery.CACHE_REFRESH);
         @SuppressWarnings("unchecked")
         List<DataRow> result = (List<DataRow>)context.performQuery(q);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextExtrasIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextExtrasIT.java
index bb840f1..2e9a148 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextExtrasIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextExtrasIT.java
@@ -121,7 +121,7 @@ public class DataContextExtrasIT extends ServerCase {
         assertEquals(PersistenceState.NEW, object.getPersistenceState());
 
         // do a manual ID substitution
-        ObjectId manualId = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, 77777);
+        ObjectId manualId = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 77777);
         object.setObjectId(manualId);
 
         context.commitChanges();
@@ -159,7 +159,7 @@ public class DataContextExtrasIT extends ServerCase {
     @Test
     public void testResolveFaultFailure() {
 
-        Persistent o1 = context.findOrCreateObject(new ObjectId(
+        Persistent o1 = context.findOrCreateObject(ObjectId.of(
                 "Artist",
                 Artist.ARTIST_ID_PK_COLUMN,
                 234));
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java
index c489d45..eaf4b02 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java
@@ -219,7 +219,7 @@ public class DataContextIT extends ServerCase {
 		// the
 		// CAY-96 bug report)
 
-		ObjectId eId = new ObjectId("Exhibit", Exhibit.EXHIBIT_ID_PK_COLUMN, 2);
+        ObjectId eId = ObjectId.of("Exhibit", Exhibit.EXHIBIT_ID_PK_COLUMN, 2);
 		Exhibit e = (Exhibit) context.performQuery(new ObjectIdQuery(eId)).get(0);
 
 		assertTrue(e.readPropertyDirectly(Exhibit.TO_GALLERY.getName()) instanceof Fault);
@@ -690,7 +690,7 @@ public class DataContextIT extends ServerCase {
 
 		// testing this...
 		context.deleteObjects(hollow);
-		assertSame(hollow, context.getGraphManager().getNode(new ObjectId("Artist", "ARTIST_ID", 33001)));
+		assertSame(hollow, context.getGraphManager().getNode(ObjectId.of("Artist", "ARTIST_ID", 33001)));
 		assertEquals("artist1", hollow.getArtistName());
 
 		assertEquals(PersistenceState.DELETED, hollow.getPersistenceState());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQueryIT.java
index 5cf05a4..611dd06 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQueryIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQueryIT.java
@@ -56,7 +56,7 @@ public class DataContextObjectIdQueryIT extends ServerCase {
                 "UPDATE ARTIST SET DATE_OF_BIRTH = NULL"));
 
         long id = Cayenne.longPKForObject(a);
-        ObjectIdQuery query = new ObjectIdQuery(new ObjectId(
+        ObjectIdQuery query = new ObjectIdQuery(ObjectId.of(
                 "Artist",
                 Artist.ARTIST_ID_PK_COLUMN,
                 id), false, ObjectIdQuery.CACHE_REFRESH);
@@ -79,7 +79,7 @@ public class DataContextObjectIdQueryIT extends ServerCase {
                 "UPDATE ARTIST SET ARTIST_NAME = 'Y'"));
 
         long id = Cayenne.longPKForObject(a);
-        ObjectIdQuery query = new ObjectIdQuery(new ObjectId(
+        ObjectIdQuery query = new ObjectIdQuery(ObjectId.of(
                 "Artist",
                 Artist.ARTIST_ID_PK_COLUMN,
                 id), false, ObjectIdQuery.CACHE);
@@ -106,7 +106,7 @@ public class DataContextObjectIdQueryIT extends ServerCase {
                 Artist.class,
                 "UPDATE ARTIST SET DATE_OF_BIRTH = NULL"));
 
-        ObjectIdQuery query = new ObjectIdQuery(new ObjectId(
+        ObjectIdQuery query = new ObjectIdQuery(ObjectId.of(
                 "Artist",
                 Artist.ARTIST_ID_PK_COLUMN,
                 44l), false, ObjectIdQuery.CACHE_REFRESH);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQuery_PolymorphicIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQuery_PolymorphicIT.java
index fca2715..a963f95 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQuery_PolymorphicIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextObjectIdQuery_PolymorphicIT.java
@@ -62,7 +62,7 @@ public class DataContextObjectIdQuery_PolymorphicIT extends PeopleProjectCase {
 
 		tPerson.insert(1, "P1", "EM");
 
-		final ObjectIdQuery q1 = new ObjectIdQuery(new ObjectId("AbstractPerson", "PERSON_ID", 1), false,
+		final ObjectIdQuery q1 = new ObjectIdQuery(ObjectId.of("AbstractPerson", "PERSON_ID", 1), false,
 				ObjectIdQuery.CACHE);
 
 		AbstractPerson ap1 = (AbstractPerson) Cayenne.objectForQuery(context1, q1);
@@ -93,7 +93,7 @@ public class DataContextObjectIdQuery_PolymorphicIT extends PeopleProjectCase {
 
 
 		final ObjectIdQuery q1 = new ObjectIdQuery(
-				new ObjectId("AbstractPerson", "PERSON_ID", Cayenne.intPKForObject(e)),
+				ObjectId.of("AbstractPerson", "PERSON_ID", Cayenne.intPKForObject(e)),
 				false,
 				ObjectIdQuery.CACHE);
 
@@ -114,7 +114,7 @@ public class DataContextObjectIdQuery_PolymorphicIT extends PeopleProjectCase {
 
 		tPerson.insert(1, "P1", "EM");
 
-		final ObjectIdQuery q1 = new ObjectIdQuery(new ObjectId("AbstractPerson", "PERSON_ID", 1), false,
+		final ObjectIdQuery q1 = new ObjectIdQuery(ObjectId.of("AbstractPerson", "PERSON_ID", 1), false,
 				ObjectIdQuery.CACHE);
 
 		AbstractPerson ap1 = (AbstractPerson) Cayenne.objectForQuery(context1, q1);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
index ca3dc95..9bc3792 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPrefetchMultistepIT.java
@@ -125,12 +125,12 @@ public class DataContextPrefetchMultistepIT extends ServerCase {
         Map<String, Object> id1 = new HashMap<>();
         id1.put("ARTIST_ID", 11);
         id1.put("EXHIBIT_ID", 2);
-        ObjectId oid1 = new ObjectId("ArtistExhibit", id1);
+        ObjectId oid1 = ObjectId.of("ArtistExhibit", id1);
 
         Map<String, Object> id2 = new HashMap<>();
         id2.put("ARTIST_ID", 101);
         id2.put("EXHIBIT_ID", 2);
-        ObjectId oid2 = new ObjectId("ArtistExhibit", id2);
+        ObjectId oid2 = ObjectId.of("ArtistExhibit", id2);
 
         assertNull(context.getGraphManager().getNode(oid1));
         assertNull(context.getGraphManager().getNode(oid2));
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataRowStoreIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataRowStoreIT.java
index d2180b2..9000ad8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataRowStoreIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataRowStoreIT.java
@@ -83,15 +83,15 @@ public class DataRowStoreIT extends ServerCase {
         assertEquals(2, cache.maximumSize());
         assertEquals(0, cache.size());
 
-        ObjectId key1 = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, 1);
+        ObjectId key1 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 1);
         Map<ObjectId, DataRow> diff1 = new HashMap<>();
         diff1.put(key1, new DataRow(1));
 
-        ObjectId key2 = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
+        ObjectId key2 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
         Map<ObjectId, DataRow> diff2 = new HashMap<>();
         diff2.put(key2, new DataRow(1));
 
-        ObjectId key3 = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, 3);
+        ObjectId key3 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 3);
         Map<ObjectId, DataRow> diff3 = new HashMap<>();
         diff3.put(key3, new DataRow(1));
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DbArcIdTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DbArcIdTest.java
index e9f256e..7e8e7ea 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DbArcIdTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DbArcIdTest.java
@@ -23,53 +23,52 @@ import org.apache.cayenne.map.DbRelationship;
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotEquals;
 
 public class DbArcIdTest {
 
     @Test
     public void testHashCode() {
 
-        DbArcId id1 = new DbArcId(new ObjectId("x", "k", "v"),
+        DbArcId id1 = new DbArcId(ObjectId.of("x", "k", "v"),
                 new DbRelationship("r1"));
         int h1 = id1.hashCode();
         assertEquals(h1, id1.hashCode());
         assertEquals(h1, id1.hashCode());
 
-        DbArcId id1_eq = new DbArcId(new ObjectId("x", "k", "v"),
+        DbArcId id1_eq = new DbArcId(ObjectId.of("x", "k", "v"),
                 new DbRelationship("r1"));
         assertEquals(h1, id1_eq.hashCode());
 
-        DbArcId id2 = new DbArcId(new ObjectId("x", "k", "v"),
+        DbArcId id2 = new DbArcId(ObjectId.of("x", "k", "v"),
                 new DbRelationship("r2"));
-        assertFalse(h1 == id2.hashCode());
+        assertNotEquals(h1, id2.hashCode());
 
-        DbArcId id3 = new DbArcId(new ObjectId("y", "k", "v"),
+        DbArcId id3 = new DbArcId(ObjectId.of("y", "k", "v"),
                 new DbRelationship("r1"));
-        assertFalse(h1 == id3.hashCode());
+        assertNotEquals(h1, id3.hashCode());
     }
 
     @Test
     public void testEquals() {
 
-        DbArcId id1 = new DbArcId(new ObjectId("x", "k", "v"),
+        DbArcId id1 = new DbArcId(ObjectId.of("x", "k", "v"),
                 new DbRelationship("r1"));
-        assertTrue(id1.equals(id1));
+        assertEquals(id1, id1);
 
-        DbArcId id1_eq = new DbArcId(new ObjectId("x", "k", "v"),
+        DbArcId id1_eq = new DbArcId(ObjectId.of("x", "k", "v"),
                 new DbRelationship("r1"));
-        assertTrue(id1.equals(id1_eq));
-        assertTrue(id1_eq.equals(id1));
+        assertEquals(id1, id1_eq);
+        assertEquals(id1_eq, id1);
 
-        DbArcId id2 = new DbArcId(new ObjectId("x", "k", "v"),
+        DbArcId id2 = new DbArcId(ObjectId.of("x", "k", "v"),
                 new DbRelationship("r2"));
-        assertFalse(id1.equals(id2));
+        assertNotEquals(id1, id2);
 
-        DbArcId id3 = new DbArcId(new ObjectId("y", "k", "v"),
+        DbArcId id3 = new DbArcId(ObjectId.of("y", "k", "v"),
                 new DbRelationship("r1"));
-        assertFalse(id1.equals(id3));
+        assertNotEquals(id1, id3);
 
-        assertFalse(id1.equals(new Object()));
+        assertNotEquals(id1, new Object());
     }
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/FlattenedArcKeyIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/FlattenedArcKeyIT.java
index 6afa006..9e7ef01 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/FlattenedArcKeyIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/FlattenedArcKeyIT.java
@@ -42,8 +42,8 @@ public class FlattenedArcKeyIT extends ServerCase {
 
     @Test
     public void testAttributes() {
-        ObjectId src = new ObjectId("X");
-        ObjectId target = new ObjectId("Y");
+        ObjectId src = ObjectId.of("X");
+        ObjectId target = ObjectId.of("Y");
         ObjRelationship r1 = entityResolver.getObjEntity(FlattenedTest3.class).getRelationship(
                 FlattenedTest3.TO_FT1.getName());
 
@@ -56,8 +56,8 @@ public class FlattenedArcKeyIT extends ServerCase {
 
     @Test
     public void testHashCode() {
-        ObjectId src = new ObjectId("X");
-        ObjectId target = new ObjectId("Y");
+        ObjectId src = ObjectId.of("X");
+        ObjectId target = ObjectId.of("Y");
         ObjRelationship r1 = entityResolver.getObjEntity(FlattenedTest3.class).getRelationship(
                 FlattenedTest3.TO_FT1.getName());
 
@@ -79,8 +79,8 @@ public class FlattenedArcKeyIT extends ServerCase {
 
     @Test
     public void testEquals() {
-        ObjectId src = new ObjectId("X");
-        ObjectId target = new ObjectId("Y");
+        ObjectId src = ObjectId.of("X");
+        ObjectId target = ObjectId.of("Y");
         ObjRelationship r1 = entityResolver.getObjEntity(FlattenedTest3.class).getRelationship(
                 FlattenedTest3.TO_FT1.getName());
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/JointPrefetchIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/JointPrefetchIT.java
index 7f202ed..cff3b69 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/JointPrefetchIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/JointPrefetchIT.java
@@ -374,7 +374,7 @@ public class JointPrefetchIT extends ServerCase {
 
         // sanity check...
         DataObject g1 = (DataObject) context.getGraphManager().getNode(
-                new ObjectId("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33001));
+                ObjectId.of("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33001));
         assertNull(g1);
 
         final List<Artist> objects = q.select(context);
@@ -393,11 +393,11 @@ public class JointPrefetchIT extends ServerCase {
 
             // however both galleries must be in memory...
             DataObject g11 = (DataObject) context.getGraphManager().getNode(
-                    new ObjectId("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33001));
+                    ObjectId.of("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33001));
             assertNotNull(g11);
             assertEquals(PersistenceState.COMMITTED, g11.getPersistenceState());
             DataObject g2 = (DataObject) context.getGraphManager().getNode(
-                    new ObjectId("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33002));
+                    ObjectId.of("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33002));
             assertNotNull(g2);
             assertEquals(PersistenceState.COMMITTED, g2.getPersistenceState());
         });
@@ -443,7 +443,7 @@ public class JointPrefetchIT extends ServerCase {
                 .select(context);
         queryInterceptor.runWithQueriesBlocked(() -> {
             DataObject g1 = (DataObject) context.getGraphManager().getNode(
-                    new ObjectId("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33001)
+                    ObjectId.of("Gallery", Gallery.GALLERY_ID_PK_COLUMN, 33001)
             );
             assertNotNull(g1);
             assertEquals("G1", g1.readProperty("galleryName"));
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
index 3b50fb3..6137dd3 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
@@ -41,7 +41,6 @@ import org.junit.Before;
 import org.junit.Test;
 
 import java.sql.Types;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 
@@ -284,10 +283,10 @@ public class NestedDataContextReadIT extends ServerCase {
 
         final ObjectContext child = runtime.newContext(context);
 
-        final ObjectId prefetchedId = new ObjectId(
+        final ObjectId prefetchedId = ObjectId.of(
                 "Artist",
                 Artist.ARTIST_ID_PK_COLUMN,
-                new Integer(33001));
+                33001);
 
         SelectQuery<Painting> q = new SelectQuery<>(Painting.class);
         q.addOrdering(Painting.PAINTING_TITLE.asc());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
index 24196e8..eb3010e 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
@@ -285,7 +285,7 @@ public class NumericTypesIT extends ServerCase {
         object.setDecimalPK(new BigDecimal("1.25"));
         context.commitChanges();
 
-        ObjectId syntheticId = new ObjectId(
+        ObjectId syntheticId = ObjectId.of(
                 "DecimalPKTestEntity",
                 "DECIMAL_PK",
                 new BigDecimal("1.25"));
@@ -305,7 +305,7 @@ public class NumericTypesIT extends ServerCase {
         object.setDecimalPK(1.25);
         context.commitChanges();
 
-        ObjectId syntheticId = new ObjectId("DecimalPKTest1", "DECIMAL_PK", 1.25);
+        ObjectId syntheticId = ObjectId.of("DecimalPKTest1", "DECIMAL_PK", 1.25);
         assertSame(object, context.getGraphManager().getNode(syntheticId));
     }
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/ObjectStoreIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/ObjectStoreIT.java
index 8c768e8..2a48aa8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/ObjectStoreIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/ObjectStoreIT.java
@@ -53,19 +53,19 @@ public class ObjectStoreIT extends ServerCase {
         assertEquals(0, context.getObjectStore().registeredObjectsCount());
 
         DataObject o1 = new MockDataObject();
-        o1.setObjectId(new ObjectId("T", "key1", "v1"));
+        o1.setObjectId(ObjectId.of("T", "key1", "v1"));
         context.getObjectStore().registerNode(o1.getObjectId(), o1);
         assertEquals(1, context.getObjectStore().registeredObjectsCount());
 
         // test object with same id
         DataObject o2 = new MockDataObject();
-        o2.setObjectId(new ObjectId("T", "key1", "v1"));
+        o2.setObjectId(ObjectId.of("T", "key1", "v1"));
         context.getObjectStore().registerNode(o2.getObjectId(), o2);
         assertEquals(1, context.getObjectStore().registeredObjectsCount());
 
         // test new object
         DataObject o3 = new MockDataObject();
-        o3.setObjectId(new ObjectId("T", "key3", "v3"));
+        o3.setObjectId(ObjectId.of("T", "key3", "v3"));
         context.getObjectStore().registerNode(o3.getObjectId(), o3);
         assertEquals(2, context.getObjectStore().registeredObjectsCount());
     }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/ObjectStoreTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/ObjectStoreTest.java
index b8097c2..633cdc6 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/ObjectStoreTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/ObjectStoreTest.java
@@ -48,7 +48,7 @@ public class ObjectStoreTest {
     @Test
     public void testRegisterNode() {
 
-        ObjectId id = new ObjectId("E1", "ID", 500);
+        ObjectId id = ObjectId.of("E1", "ID", 500);
         Persistent object = mock(Persistent.class);
 
         objectStore.registerNode(id, object);
@@ -58,7 +58,7 @@ public class ObjectStoreTest {
     @Test
     public void testUnregisterNode() {
 
-        ObjectId id = new ObjectId("E1", "ID", 500);
+        ObjectId id = ObjectId.of("E1", "ID", 500);
         Persistent object = mock(Persistent.class);
 
         objectStore.registerNode(id, object);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/QuotedIdentifiersIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/QuotedIdentifiersIT.java
index 98f8b44..04df13a 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/QuotedIdentifiersIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/QuotedIdentifiersIT.java
@@ -153,12 +153,12 @@ public class QuotedIdentifiersIT extends ServerCase {
         List<QuoteAdress> objects7 = quoteAdress1.select(context);
         assertEquals(1, objects7.size());
 
-        ObjectIdQuery queryObjectId = new ObjectIdQuery(new ObjectId("QuoteAdress", QuoteAdress.GROUP.getName(), "324"));
+        ObjectIdQuery queryObjectId = new ObjectIdQuery(ObjectId.of("QuoteAdress", QuoteAdress.GROUP.getName(), "324"));
 
         List objects8 = context.performQuery(queryObjectId);
         assertEquals(1, objects8.size());
 
-        ObjectIdQuery queryObjectId2 = new ObjectIdQuery(new ObjectId("Quote_Person", "GROUP", "1111"));
+        ObjectIdQuery queryObjectId2 = new ObjectIdQuery(ObjectId.of("Quote_Person", "GROUP", "1111"));
         List objects9 = context.performQuery(queryObjectId2);
         assertEquals(1, objects9.size());
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/ExpressionTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/ExpressionTest.java
index 707144e..56e8bdb 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/ExpressionTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/ExpressionTest.java
@@ -114,7 +114,7 @@ public class ExpressionTest {
 	public void testAppendAsEJBQL_PersistentParamater() throws IOException {
 
 		Artist a = new Artist();
-		ObjectId aId = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, 1);
+        ObjectId aId = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 1);
 		a.setObjectId(aId);
 
 		Expression e = ExpressionFactory.matchExp("artist", a);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTListTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTListTest.java
index 0df2523..f5bba0a 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTListTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTListTest.java
@@ -36,7 +36,7 @@ public class ASTListTest {
 
     @Test
 	public void testConstructorWithCollection() {
-		ObjectId objectId = new ObjectId("Artist", "ARTIST_ID", 1);
+		ObjectId objectId = ObjectId.of("Artist", "ARTIST_ID", 1);
 		Persistent artist = mock(Persistent.class);
 		when(artist.getObjectId()).thenReturn(objectId);
 
@@ -51,7 +51,7 @@ public class ASTListTest {
 
 	@Test
 	public void testEquals() throws Exception {
-		ObjectId objectId = new ObjectId("Artist", "ARTIST_ID", 1);
+		ObjectId objectId = ObjectId.of("Artist", "ARTIST_ID", 1);
 		Persistent artist = mock(Persistent.class);
 		when(artist.getObjectId()).thenReturn(objectId);
 
@@ -68,7 +68,7 @@ public class ASTListTest {
 
 	@Test
 	public void testHashCode() throws Exception {
-		ObjectId objectId = new ObjectId("Artist", "ARTIST_ID", 1);
+		ObjectId objectId = ObjectId.of("Artist", "ARTIST_ID", 1);
 		Persistent artist = mock(Persistent.class);
 		when(artist.getObjectId()).thenReturn(objectId);
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/EvaluatorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/EvaluatorTest.java
index ea86e5d..18f9a05 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/EvaluatorTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/EvaluatorTest.java
@@ -211,14 +211,14 @@ public class EvaluatorTest {
     @Test
     public void testEvaluator_Persistent() {
 
-        ObjectId lhsId = new ObjectId("X", "k", 3);
+        ObjectId lhsId = ObjectId.of("X", "k", 3);
         Persistent lhs = mock(Persistent.class);
         when(lhs.getObjectId()).thenReturn(lhsId);
 
         Evaluator e = Evaluator.evaluator(lhs);
         assertNotNull(e);
 
-        ObjectId rhsId1 = new ObjectId("X", "k", 3);
+        ObjectId rhsId1 = ObjectId.of("X", "k", 3);
         Persistent rhs1 = mock(Persistent.class);
         when(rhs1.getObjectId()).thenReturn(rhsId1);
 
@@ -226,7 +226,7 @@ public class EvaluatorTest {
         assertTrue(e.eq(lhs, rhsId1));
         assertTrue(e.eq(lhs, 3));
 
-        ObjectId rhsId2 = new ObjectId("X", "k", 4);
+        ObjectId rhsId2 = ObjectId.of("X", "k", 4);
         Persistent rhs2 = mock(Persistent.class);
         when(rhs2.getObjectId()).thenReturn(rhsId2);
 
@@ -238,14 +238,14 @@ public class EvaluatorTest {
     @Test
     public void testEvaluator_Persistent_StringId() {
 
-        ObjectId lhsId = new ObjectId("X", "k", "A");
+        ObjectId lhsId = ObjectId.of("X", "k", "A");
         Persistent lhs = mock(Persistent.class);
         when(lhs.getObjectId()).thenReturn(lhsId);
 
         Evaluator e = Evaluator.evaluator(lhs);
         assertNotNull(e);
 
-        ObjectId rhsId1 = new ObjectId("X", "k", "A");
+        ObjectId rhsId1 = ObjectId.of("X", "k", "A");
         Persistent rhs1 = mock(Persistent.class);
         when(rhs1.getObjectId()).thenReturn(rhsId1);
 
@@ -253,7 +253,7 @@ public class EvaluatorTest {
         assertTrue(e.eq(lhs, rhsId1));
         assertTrue(e.eq(lhs, "A"));
 
-        ObjectId rhsId2 = new ObjectId("X", "k", "B");
+        ObjectId rhsId2 = ObjectId.of("X", "k", "B");
         Persistent rhs2 = mock(Persistent.class);
         when(rhs2.getObjectId()).thenReturn(rhsId2);
 
@@ -268,7 +268,7 @@ public class EvaluatorTest {
         Map<String, Object> lhsIdMap = new HashMap<>();
         lhsIdMap.put("a", 1);
         lhsIdMap.put("b", "B");
-        ObjectId lhsId = new ObjectId("X", lhsIdMap);
+        ObjectId lhsId = ObjectId.of("X", lhsIdMap);
         Persistent lhs = mock(Persistent.class);
         when(lhs.getObjectId()).thenReturn(lhsId);
 
@@ -278,7 +278,7 @@ public class EvaluatorTest {
         Map<String, Object> rhsId1Map = new HashMap<>();
         rhsId1Map.put("a", 1);
         rhsId1Map.put("b", "B");
-        ObjectId rhsId1 = new ObjectId("X", rhsId1Map);
+        ObjectId rhsId1 = ObjectId.of("X", rhsId1Map);
         Persistent rhs1 = mock(Persistent.class);
         when(rhs1.getObjectId()).thenReturn(rhsId1);
 
@@ -289,7 +289,7 @@ public class EvaluatorTest {
         Map<String, Object> rhsId2Map = new HashMap<>();
         rhsId2Map.put("a", 1);
         rhsId2Map.put("b", "BX");
-        ObjectId rhsId2 = new ObjectId("X", rhsId2Map);
+        ObjectId rhsId2 = ObjectId.of("X", rhsId2Map);
         Persistent rhs2 = mock(Persistent.class);
         when(rhs2.getObjectId()).thenReturn(rhsId2);
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
index 072dd26..b7ad6e8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectIdQueryTest.java
@@ -23,18 +23,14 @@ import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.util.Util;
 import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 
 public class ObjectIdQueryTest {
 
     @Test
     public void testConstructorObjectId() {
 
-        ObjectId oid = new ObjectId("MockDataObject", "a", "b");
+        ObjectId oid = ObjectId.of("MockDataObject", "a", "b");
         ObjectIdQuery query = new ObjectIdQuery(oid);
 
         assertSame(oid, query.getObjectId());
@@ -42,7 +38,7 @@ public class ObjectIdQueryTest {
 
     @Test
     public void testSerializability() throws Exception {
-        ObjectId oid = new ObjectId("test", "a", "b");
+        ObjectId oid = ObjectId.of("test", "a", "b");
         ObjectIdQuery query = new ObjectIdQuery(oid);
 
         Object o = Util.cloneViaSerialization(query);
@@ -57,10 +53,10 @@ public class ObjectIdQueryTest {
      */
     @Test
     public void testEquals() throws Exception {
-        ObjectIdQuery q1 = new ObjectIdQuery(new ObjectId("abc", "a", 1));
-        ObjectIdQuery q2 = new ObjectIdQuery(new ObjectId("abc", "a", 1));
-        ObjectIdQuery q3 = new ObjectIdQuery(new ObjectId("abc", "a", 3));
-        ObjectIdQuery q4 = new ObjectIdQuery(new ObjectId("123", "a", 1));
+        ObjectIdQuery q1 = new ObjectIdQuery(ObjectId.of("abc", "a", 1));
+        ObjectIdQuery q2 = new ObjectIdQuery(ObjectId.of("abc", "a", 1));
+        ObjectIdQuery q3 = new ObjectIdQuery(ObjectId.of("abc", "a", 3));
+        ObjectIdQuery q4 = new ObjectIdQuery(ObjectId.of("123", "a", 1));
 
         assertTrue(q1.equals(q2));
         assertEquals(q1.hashCode(), q2.hashCode());
@@ -74,7 +70,7 @@ public class ObjectIdQueryTest {
 
     @Test
     public void testMetadata() {
-        ObjectIdQuery q1 = new ObjectIdQuery(new ObjectId("abc", "a", 1), true, ObjectIdQuery.CACHE_REFRESH);
+        ObjectIdQuery q1 = new ObjectIdQuery(ObjectId.of("abc", "a", 1), true, ObjectIdQuery.CACHE_REFRESH);
 
         assertTrue(q1.isFetchAllowed());
         assertTrue(q1.isFetchMandatory());
@@ -82,7 +78,7 @@ public class ObjectIdQueryTest {
         QueryMetadata md1 = q1.getMetaData(null);
         assertTrue(md1.isFetchingDataRows());
 
-        ObjectIdQuery q2 = new ObjectIdQuery(new ObjectId("abc", "a", 1), false, ObjectIdQuery.CACHE);
+        ObjectIdQuery q2 = new ObjectIdQuery(ObjectId.of("abc", "a", 1), false, ObjectIdQuery.CACHE);
 
         assertTrue(q2.isFetchAllowed());
         assertFalse(q2.isFetchMandatory());
@@ -90,7 +86,7 @@ public class ObjectIdQueryTest {
         QueryMetadata md2 = q2.getMetaData(null);
         assertFalse(md2.isFetchingDataRows());
 
-        ObjectIdQuery q3 = new ObjectIdQuery(new ObjectId("abc", "a", 1), false, ObjectIdQuery.CACHE_NOREFRESH);
+        ObjectIdQuery q3 = new ObjectIdQuery(ObjectId.of("abc", "a", 1), false, ObjectIdQuery.CACHE_NOREFRESH);
 
         assertFalse(q3.isFetchAllowed());
         assertFalse(q3.isFetchMandatory());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/RelationshipQueryTest.java b/cayenne-server/src/test/java/org/apache/cayenne/query/RelationshipQueryTest.java
index 8171dc6..ffca4ef 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/RelationshipQueryTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/RelationshipQueryTest.java
@@ -32,7 +32,7 @@ public class RelationshipQueryTest {
     @Test
     public void testConstructorObjectId() {
 
-        ObjectId oid = new ObjectId("MockDataObject", "a", "b");
+        ObjectId oid = ObjectId.of("MockDataObject", "a", "b");
         RelationshipQuery query = new RelationshipQuery(oid, "relX");
         assertSame(oid, query.getObjectId());
         assertSame("relX", query.getRelationshipName());
@@ -40,7 +40,7 @@ public class RelationshipQueryTest {
 
     @Test
     public void testSerializability() throws Exception {
-        ObjectId oid = new ObjectId("test", "a", "b");
+        ObjectId oid = ObjectId.of("test", "a", "b");
         RelationshipQuery query = new RelationshipQuery(oid, "relX");
 
         RelationshipQuery q1 = (RelationshipQuery) Util.cloneViaSerialization(query);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
index 89b889c..2755abe 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectById_RunIT.java
@@ -116,12 +116,12 @@ public class SelectById_RunIT extends ServerCase {
 	public void testObjectIdPk() throws Exception {
 		createTwoArtists();
 
-		ObjectId oid3 = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, 3);
+		ObjectId oid3 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 3);
 		Artist a3 = SelectById.query(Artist.class, oid3).selectOne(context);
 		assertNotNull(a3);
 		assertEquals("artist3", a3.getArtistName());
 
-		ObjectId oid2 = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
+		ObjectId oid2 = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
 		Artist a2 = SelectById.query(Artist.class, oid2).selectOne(context);
 		assertNotNull(a2);
 		assertEquals("artist2", a2.getArtistName());
@@ -170,7 +170,7 @@ public class SelectById_RunIT extends ServerCase {
 		assertNotEquals(md1.getCacheKey(), md4.getCacheKey());
 
 		SelectById<Painting> q5 = SelectById
-				.query(Painting.class, new ObjectId("Painting", Painting.PAINTING_ID_PK_COLUMN, 4)).localCache();
+				.query(Painting.class, ObjectId.of("Painting", Painting.PAINTING_ID_PK_COLUMN, 4)).localCache();
 		QueryMetadata md5 = q5.getMetaData(resolver);
 		assertNotNull(md5);
 		assertNotNull(md5.getCacheKey());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/StatementFetchSizeIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/StatementFetchSizeIT.java
index 764070e..241a464 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/StatementFetchSizeIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/StatementFetchSizeIT.java
@@ -63,7 +63,7 @@ public class StatementFetchSizeIT extends ServerCase {
                 .getStatementFetchSize());
         context.performQuery(ejbql);
 
-        ObjectId id = new ObjectId("Artist", Artist.ARTIST_ID_PK_COLUMN, 1);
+        ObjectId id = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 1);
         RelationshipQuery relationshipQuery = new RelationshipQuery(
                 id,
                 Artist.PAINTING_ARRAY.getName(),
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/reflect/LifecycleCallbackEventHandlerTest.java b/cayenne-server/src/test/java/org/apache/cayenne/reflect/LifecycleCallbackEventHandlerTest.java
index 72c8360..04afa29 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/reflect/LifecycleCallbackEventHandlerTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/reflect/LifecycleCallbackEventHandlerTest.java
@@ -20,7 +20,6 @@ package org.apache.cayenne.reflect;
 
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.PersistentObject;
-import org.apache.cayenne.map.EntityResolver;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -39,7 +38,7 @@ public class LifecycleCallbackEventHandlerTest {
         map.addDefaultListener(l1, "callback");
 
         C1 c1 = new C1();
-        c1.setObjectId(new ObjectId("bogus"));
+        c1.setObjectId(ObjectId.of("bogus"));
 
         assertEquals(0, l1.entities.size());
         map.performCallbacks(c1);
@@ -58,7 +57,7 @@ public class LifecycleCallbackEventHandlerTest {
         map.addDefaultListener(l2, "callback");
 
         C1 c1 = new C1();
-        c1.setObjectId(new ObjectId("bogus"));
+        c1.setObjectId(ObjectId.of("bogus"));
 
         map.performCallbacks(c1);
         assertEquals(1, l1.callbackTimes.size());
@@ -76,7 +75,7 @@ public class LifecycleCallbackEventHandlerTest {
         map.addListener(C1.class, "c1Callback");
 
         C3 subclass = new C3();
-        subclass.setObjectId(new ObjectId("bogusSubclass"));
+        subclass.setObjectId(ObjectId.of("bogusSubclass"));
 
         assertEquals(0, subclass.callbacks.size());
         map.performCallbacks(subclass);
@@ -90,7 +89,7 @@ public class LifecycleCallbackEventHandlerTest {
         map.addListener(C1.class, "c1Callback");
 
         C4 subclass = new C4();
-        subclass.setObjectId(new ObjectId("bogus"));
+        subclass.setObjectId(ObjectId.of("bogus"));
 
         assertEquals(0, subclass.callbacks.size());
         map.performCallbacks(subclass);
@@ -106,7 +105,7 @@ public class LifecycleCallbackEventHandlerTest {
         map.addListener(C1.class, "c1Callback");
 
         C2 c = new C2();
-        c.setObjectId(new ObjectId("bogus"));
+        c.setObjectId(ObjectId.of("bogus"));
 
         assertTrue(c.callbacks.isEmpty());
         map.performCallbacks(c);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/template/CayenneSQLTemplateProcessorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/template/CayenneSQLTemplateProcessorTest.java
index 9e17849..e08810d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/template/CayenneSQLTemplateProcessorTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/template/CayenneSQLTemplateProcessorTest.java
@@ -156,7 +156,7 @@ public class CayenneSQLTemplateProcessorTest {
         String sqlTemplate = "SELECT * FROM ME WHERE COLUMN1 = #bind($helper.cayenneExp($a, 'db:ID_COLUMN'))";
 
         DataObject dataObject = new CayenneDataObject();
-        dataObject.setObjectId(new ObjectId("T", "ID_COLUMN", 5));
+        dataObject.setObjectId(ObjectId.of("T", "ID_COLUMN", 5));
 
         Map<String, Object> map = Collections.singletonMap("a", dataObject);
 
@@ -176,7 +176,7 @@ public class CayenneSQLTemplateProcessorTest {
         Map<String, Object> idMap = new HashMap<>();
         idMap.put("ID_COLUMN1", 3);
         idMap.put("ID_COLUMN2", "aaa");
-        ObjectId id = new ObjectId("T", idMap);
+        ObjectId id = ObjectId.of("T", idMap);
         DataObject dataObject = new CayenneDataObject();
         dataObject.setObjectId(id);
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/util/ShallowMergeOperationIT.java b/cayenne-server/src/test/java/org/apache/cayenne/util/ShallowMergeOperationIT.java
index 111caee..3cf1261 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/util/ShallowMergeOperationIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/util/ShallowMergeOperationIT.java
@@ -108,13 +108,13 @@ public class ShallowMergeOperationIT extends ServerCase {
         int modifiedId = 33003;
         final Artist modified = (Artist) Cayenne.objectForQuery(
                 context,
-                new ObjectIdQuery(new ObjectId(
+                new ObjectIdQuery(ObjectId.of(
                         "Artist",
                         Artist.ARTIST_ID_PK_COLUMN,
                         modifiedId)));
         final Artist peerModified = (Artist) Cayenne.objectForQuery(
                 childContext,
-                new ObjectIdQuery(new ObjectId(
+                new ObjectIdQuery(ObjectId.of(
                         "Artist",
                         Artist.ARTIST_ID_PK_COLUMN,
                         modifiedId)));
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/util/SingleEntryMapTest.java b/cayenne-server/src/test/java/org/apache/cayenne/util/SingleEntryMapTest.java
new file mode 100644
index 0000000..f271ea0
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/util/SingleEntryMapTest.java
@@ -0,0 +1,471 @@
+/*****************************************************************
+ *   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.util;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class SingleEntryMapTest {
+
+    private SingleEntryMap<String, Integer> map;
+
+    @Before
+    public void createMap() {
+        map = new SingleEntryMap<>("test");
+    }
+
+    @Test
+    public void constructor() {
+        assertEquals(0, map.size());
+        assertTrue(map.isEmpty());
+        assertNull(map.get("test"));
+        assertTrue(map.keySet().isEmpty());
+        assertTrue(map.values().isEmpty());
+
+        assertEquals("test", map.getKey());
+        assertNull(map.getValue());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void constructorWithNullKey() {
+        new SingleEntryMap<>(null);
+    }
+
+    @Test
+    public void constructorWithValue() {
+        Map<String, Integer> mapWithValue = new SingleEntryMap<>("test", 123);
+        assertEquals(1, mapWithValue.size());
+        assertEquals(123, (int)mapWithValue.get("test"));
+
+        mapWithValue.put("test", null);
+        assertNull(mapWithValue.get("test"));
+
+        mapWithValue.put("test", 321);
+        assertEquals(321, (int)mapWithValue.get("test"));
+    }
+
+    @Test
+    public void constructorWithNullValue() {
+        Map<String, Integer> mapWithValue = new SingleEntryMap<>("test", null);
+        assertEquals(0, mapWithValue.size());
+        assertNull(mapWithValue.get("test"));
+
+        mapWithValue.put("test", 321);
+        assertEquals(321, (int)mapWithValue.get("test"));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void constructorWithNullKeyAndValue() {
+        new SingleEntryMap<>(null, 123);
+    }
+
+    @Test
+    public void entrySet() {
+        assertTrue(map.entrySet().isEmpty());
+
+        map.setValue(123);
+        assertEquals(1, map.entrySet().size());
+        assertEquals("test", map.entrySet().iterator().next().getKey());
+        assertEquals(123, (int)map.entrySet().iterator().next().getValue());
+        assertEquals(1, map.entrySet().size());
+    }
+
+    @Test
+    public void containsKey() {
+        assertFalse(map.containsKey("test"));
+        assertFalse(map.containsKey("test1"));
+
+        map.put("test", 123);
+        assertTrue(map.containsKey("test"));
+        assertFalse(map.containsKey("test1"));
+
+        map.put("test", null);
+        assertFalse(map.containsKey("test"));
+        assertFalse(map.containsKey("test1"));
+    }
+
+    @Test
+    public void size() {
+        assertEquals(0, map.size());
+
+        map.put("test", 123);
+        assertEquals(1, map.size());
+
+        map.put("test", null);
+        assertEquals(0, map.size());
+    }
+
+    @Test
+    public void isEmpty() {
+        assertTrue(map.isEmpty());
+
+        map.put("test", 123);
+        assertFalse(map.isEmpty());
+
+        map.put("test", null);
+        assertTrue(map.isEmpty());
+    }
+
+    @Test
+    public void containsValue() {
+        assertFalse(map.containsValue(123));
+
+        map.put("test", 123);
+        assertTrue(map.containsValue(123));
+
+        map.put("test", null);
+        assertFalse(map.containsValue(123));
+    }
+
+    @Test
+    public void get() {
+        assertNull(map.get("test"));
+        assertNull(map.get("test2"));
+
+        map.put("test", 123);
+        assertEquals(123, (int)map.get("test"));
+        assertNull(map.get("test2"));
+
+        map.put("test", null);
+        assertNull(map.get("test"));
+        assertNull(map.get("test2"));
+    }
+
+    @Test
+    public void put() {
+        assertNull(map.put("test", 123));
+        assertEquals(123, (int)map.put("test", 321));
+        assertEquals(321, (int)map.put("test", null));
+        assertNull(map.put("test", 123));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void putWrongKey() {
+        map.put("test2", 321);
+    }
+
+    @Test
+    public void remove() {
+        assertNull(map.remove("test"));
+        assertNull(map.remove("test2"));
+
+        map.put("test", 123);
+        assertEquals(123, (int)map.remove("test"));
+
+        assertNull(map.remove("test"));
+    }
+
+    @Test
+    public void putAll() {
+        assertNull(map.get("test"));
+        assertNull(map.get("test2"));
+
+        Map<String, Integer> map2 = Collections.singletonMap("test", 123);
+
+        map.putAll(map2);
+        assertEquals(123, (int)map.get("test"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void putAllWrongKey() {
+        assertNull(map.get("test"));
+        assertNull(map.get("test2"));
+
+        Map<String, Integer> map2 = Collections.singletonMap("test2", 123);
+        map.putAll(map2);
+    }
+
+    @Test
+    public void clear() {
+        assertEquals(0, map.size());
+
+        map.clear();
+        assertEquals(0, map.size());
+
+        map.put("test", 123);
+        assertEquals(1, map.size());
+
+        map.clear();
+        assertEquals(0, map.size());
+        assertNull(map.get("test"));
+    }
+
+    @Test
+    public void keySet() {
+        assertTrue(map.keySet().isEmpty());
+
+        map.put("test", 123);
+        assertEquals(1, map.keySet().size());
+        assertEquals("test", map.keySet().iterator().next());
+        assertEquals("test", map.keySet().iterator().next());
+
+        map.clear();
+        assertTrue(map.keySet().isEmpty());
+    }
+
+    @Test
+    public void values() {
+        assertTrue(map.values().isEmpty());
+
+        map.put("test", 123);
+        assertEquals(1, map.keySet().size());
+        assertEquals(123, (int)map.values().iterator().next());
+        assertEquals(123, (int)map.values().iterator().next());
+
+        map.clear();
+        assertTrue(map.values().isEmpty());
+    }
+
+    @Test
+    public void getKey() {
+        assertEquals("test", map.getKey());
+
+        map.put("test", 123);
+        assertEquals("test", map.getKey());
+    }
+
+    @Test
+    public void getValue() {
+        assertNull(map.getValue());
+
+        map.put("test", 123);
+        assertEquals(123, (int)map.getValue());
+
+        map.put("test", null);
+        assertNull(map.getValue());
+
+        map.put("test", 321);
+        assertEquals(321, (int)map.getValue());
+    }
+
+    @Test
+    public void setValue() {
+        assertNull(map.getValue());
+
+        map.setValue(123);
+        assertEquals(123, (int)map.getValue());
+
+        map.setValue(null);
+        assertNull(map.getValue());
+
+        map.setValue(321);
+        assertEquals(321, (int)map.getValue());
+    }
+
+    @Test
+    public void testEquals() {
+        assertEquals(map, emptyMap());
+        assertNotEquals(map, singletonMap("test", null));
+
+        map.put("test", 123);
+        assertEquals(map, singletonMap("test", 123));
+        assertNotEquals(map, singletonMap("test", null));
+        assertNotEquals(map, singletonMap("test2", 123));
+        assertNotEquals(map, singletonMap("test", 124));
+
+        map.put("test", 321);
+        Map<String, Integer> other = new HashMap<>();
+        other.put("test", 321);
+        assertEquals(map, other);
+
+        assertEquals(map, map);
+        assertNotEquals(map, new ArrayList<>());
+    }
+
+    @Test
+    public void testHashCode() {
+        assertEquals(emptyMap().hashCode(), map.hashCode());
+        assertEquals(map.hashCode(), map.hashCode());
+        assertNotEquals(singletonMap("test", null).hashCode(), map.hashCode());
+
+        map.put("test", 123);
+        assertEquals(singletonMap("test", 123).hashCode(), map.hashCode());
+        assertNotEquals(singletonMap("test", null).hashCode(), map.hashCode());
+        assertNotEquals(singletonMap("test2", 123).hashCode(), map.hashCode());
+        assertNotEquals(singletonMap("test", 124).hashCode(), map.hashCode());
+        assertEquals(map.hashCode(), map.hashCode());
+
+        map.put("test", 321);
+        Map<String, Integer> other = new HashMap<>();
+        other.put("test", 321);
+        assertEquals(other.hashCode(), map.hashCode());
+        assertEquals(map.hashCode(), map.hashCode());
+    }
+
+    @Test
+    public void testToString() {
+        assertEquals(emptyMap().toString(), map.toString());
+        assertNotEquals(singletonMap("test", null).toString(), map.toString());
+
+        map.put("test", 123);
+        assertEquals(singletonMap("test", 123).toString(), map.toString());
+        assertNotEquals(singletonMap("test", null).toString(), map.toString());
+        assertNotEquals(singletonMap("test2", 123).toString(), map.toString());
+        assertNotEquals(singletonMap("test", 124).toString(), map.toString());
+
+        map.put("test", 321);
+        Map<String, Integer> other = new HashMap<>();
+        other.put("test", 321);
+        assertEquals(other.toString(), map.toString());
+    }
+
+    @Test
+    public void forEach() {
+        map.forEach((k, v) -> fail("Unexpected value in map: " + k + "=" + v));
+
+        map.put("test", 123);
+
+        AtomicInteger counter = new AtomicInteger();
+        map.forEach((k, v) -> {
+            assertEquals("test", k);
+            assertEquals(123, (int)v);
+            counter.incrementAndGet();
+        });
+
+        assertEquals(1, counter.get());
+    }
+
+    @Test
+    public void getOrDefault() {
+        assertEquals(321, (int)map.getOrDefault("test", 321));
+        assertEquals(321, (int)map.getOrDefault("test2", 321));
+
+        map.put("test", 123);
+        assertEquals(123, (int)map.getOrDefault("test", 321));
+        assertEquals(321, (int)map.getOrDefault("test2", 321));
+    }
+
+    @Test
+    public void putIfAbsent() {
+        assertNull(map.putIfAbsent("test", 123));
+        assertEquals(123, (int)map.putIfAbsent("test", 321));
+        assertEquals(123, (int)map.putIfAbsent("test", 456));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void putIfAbsentWrongKey() {
+        map.putIfAbsent("test2", 321);
+    }
+
+    @Test
+    public void computeIfAbsent() {
+        assertEquals(123, (int)map.computeIfAbsent("test", k -> 123));
+        assertEquals(123, (int)map.computeIfAbsent("test", k -> 321));
+        assertEquals(123, (int)map.computeIfAbsent("test", k -> 456));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void computeIfAbsentWrongKey() {
+        map.computeIfAbsent("test2", k -> 123);
+    }
+
+    @Test
+    public void computeIfPresent() {
+        assertNull(map.computeIfPresent("test", (k, v) -> v + 1));
+
+        map.put("test", 123);
+        assertEquals(Integer.valueOf(124), map.computeIfPresent("test", (k, v) -> v + 1));
+        assertNull(map.computeIfPresent("test2", (k, v) -> v + 1));
+        assertNull(map.computeIfPresent("test3", (k, v) -> 321));
+    }
+
+    @Test
+    public void compute() {
+        assertEquals(123, (int)map.compute("test", (k, v) -> v == null ? 123 : v + 1));
+        assertEquals(124, (int)map.compute("test", (k, v) -> v == null ? 123 : v + 1));
+        assertEquals(125, (int)map.compute("test", (k, v) -> v == null ? 123 : v + 1));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void computeWrongKey() {
+        map.compute("test2", (k, v) -> 123);
+    }
+
+    @Test
+    public void merge() {
+        assertEquals(1, (int)map.merge("test", 1, Integer::sum));
+        assertEquals(2, (int)map.merge("test", 1, Integer::sum));
+        assertEquals(3, (int)map.merge("test", 1, Integer::sum));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void mergeWrongKey() {
+        map.merge("test2", 123, (oldV, newV) -> oldV + newV);
+    }
+
+    @Test
+    public void replace() {
+        assertNull(map.replace("test", 123));
+        assertNull(map.replace("test", 321));
+        assertNull(map.replace("test2", 123));
+
+        map.put("test", 123);
+        assertEquals(123, (int)map.replace("test", 321));
+        assertEquals(321, (int)map.replace("test", 123));
+        assertNull(map.replace("test2", 123));
+    }
+
+    @Test
+    public void replaceWithValue() {
+        assertFalse(map.replace("test", 321, 123));
+        assertFalse(map.replace("test", null, 123));
+        assertFalse(map.replace("test2", null, 123));
+
+        map.put("test", 123);
+
+        assertTrue(map.replace("test", 123, 321));
+        assertTrue(map.replace("test", 321, 456));
+        assertFalse(map.replace("test", 321, 456));
+        assertFalse(map.replace("test", null, 123));
+        assertFalse(map.replace("test2", null, 123));
+        assertFalse(map.replace("test2", 456, 123));
+    }
+
+    @Test
+    public void removeWithValue() {
+        assertFalse(map.remove("test", null));
+        assertFalse(map.remove("test", 123));
+        assertFalse(map.remove("test2", null));
+        assertFalse(map.remove("test2", 123));
+
+        map.put("test", 123);
+
+        assertFalse(map.remove("test", null));
+        assertFalse(map.remove("test", 321));
+        assertFalse(map.remove("test2", null));
+        assertFalse(map.remove("test2", 123));
+
+        assertTrue(map.remove("test", 123));
+        assertFalse(map.remove("test", 123));
+    }
+}
\ No newline at end of file
diff --git a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
index 2b8a5e5..b4500c8 100644
--- a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
+++ b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateProcessorTest.java
@@ -154,7 +154,7 @@ public class VelocitySQLTemplateProcessorTest {
 		String sqlTemplate = "SELECT * FROM ME WHERE COLUMN1 = #bind($helper.cayenneExp($a, 'db:ID_COLUMN'))";
 
 		DataObject dataObject = new CayenneDataObject();
-		dataObject.setObjectId(new ObjectId("T", "ID_COLUMN", 5));
+		dataObject.setObjectId(ObjectId.of("T", "ID_COLUMN", 5));
 
 		Map<String, Object> map = Collections.<String, Object> singletonMap("a", dataObject);
 
@@ -172,9 +172,9 @@ public class VelocitySQLTemplateProcessorTest {
 				+ "AND COLUMN2 #bindNotEqual($helper.cayenneExp($a, 'db:ID_COLUMN2'))";
 
 		Map<String, Object> idMap = new HashMap<>();
-		idMap.put("ID_COLUMN1", new Integer(3));
+		idMap.put("ID_COLUMN1", 3);
 		idMap.put("ID_COLUMN2", "aaa");
-		ObjectId id = new ObjectId("T", idMap);
+        ObjectId id = ObjectId.of("T", idMap);
 		DataObject dataObject = new CayenneDataObject();
 		dataObject.setObjectId(id);
 


Mime
View raw message