You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2026/09/10 00:30:42 UTC

(cayenne) 01/04: CAY-3017 ObjectContext should not be a DataChannel

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

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

commit 405f05be44041060efc06f6753b81b84192780ac
Author: Andrus Adamchik <an...@objectstyle.com>
AuthorDate: Wed Sep 9 17:18:18 2026 -0400

    CAY-3017 ObjectContext should not be a DataChannel
---
 RELEASE-NOTES.txt                                  |  1 +
 UPGRADE.md                                         |  5 ++
 .../java/org/apache/cayenne/ObjectContext.java     | 19 ++---
 .../org/apache/cayenne/access/DataContext.java     | 93 ++++++++--------------
 .../apache/cayenne/access/DataContextChannel.java  | 83 +++++++++++++++++++
 .../cayenne/access/DataContextMergeHandler.java    | 10 ++-
 .../cayenne/access/DataContextQueryAction.java     |  2 +-
 .../org/apache/cayenne/access/ObjectStore.java     |  8 +-
 .../org/apache/cayenne/access/ToManyHolder.java    |  3 +-
 .../java/org/apache/cayenne/access/ToOneFault.java |  2 +-
 .../flush/operation/GraphBasedDbRowOpSorter.java   |  2 +-
 .../cayenne/ashwood/AshwoodEntitySorter.java       |  2 +-
 .../apache/cayenne/commitlog/CommitLogFilter.java  |  2 +-
 .../configuration/ObjectContextFactory.java        | 14 ++--
 .../configuration/runtime/DataContextFactory.java  | 77 ++++++------------
 .../org/apache/cayenne/graph/ChildDiffLoader.java  |  2 +-
 .../org/apache/cayenne/runtime/CayenneRuntime.java | 19 ++++-
 .../org/apache/cayenne/tx/TransactionFilter.java   |  2 +-
 .../java/org/apache/cayenne/util/EventUtil.java    | 22 +++++
 .../access/DataContextDataChannelEventsIT.java     |  3 +-
 .../access/DataContextResolveRelationshipIT.java   |  6 +-
 .../cayenne/access/DataContextSerializationIT.java |  6 +-
 .../cayenne/access/NestedDataContextReadIT.java    |  6 +-
 .../DataContextFactory_DedicatedCacheIT.java       |  2 +-
 .../runtime/DataContextFactory_ValidationIT.java   |  2 +-
 .../apache/cayenne/runtime/CayenneRuntimeTest.java | 27 ++++---
 26 files changed, 246 insertions(+), 174 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 7026bd377..454e0bd68 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -24,6 +24,7 @@ CAY-3013 MySQL: JDBC-level streaming for iterators
 CAY-3014 Remove QueryChain
 CAY-3015 Get rid of RefreshQuery
 CAY-3016 Get rid of RelationshipQuery
+CAY-3017 ObjectContext should not be a DataChannel
 
 Bug Fixes:
 
diff --git a/UPGRADE.md b/UPGRADE.md
index d585aaf95..3980f428d 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -136,6 +136,11 @@ Expression caseWhenExp = caseWhen(
           .select(context);
   ```
 
+*  Per [CAY-3017](https://issues.apache.org/jira/browse/CAY-3017) `ObjectContext` no longer extends `DataChannel`. This 
+  doesn't change the overall architecture (there are still "channels" downstream from an ObjectContext). But the channel 
+  methods are no longer accessible on the context itself. Application code wasn't ever not supposed to use them, as 
+  everything you need to do, you can do via the ObjectContext API.
+
 *  The `org.apache.cayenne.query.ParameterizedQuery` interface was removed, together with the `createQuery(Map)`
   methods of `SQLTemplate`, `ProcedureQuery` and `ObjectSelect` that implemented it. Applying parameters to a mapped
   query is now the job of the query descriptor - override `QueryDescriptor.buildQuery(Map)` if you have a custom
diff --git a/cayenne/src/main/java/org/apache/cayenne/ObjectContext.java b/cayenne/src/main/java/org/apache/cayenne/ObjectContext.java
index 3e92a79ca..f15fffb27 100644
--- a/cayenne/src/main/java/org/apache/cayenne/ObjectContext.java
+++ b/cayenne/src/main/java/org/apache/cayenne/ObjectContext.java
@@ -31,10 +31,15 @@ import java.util.List;
 /**
  * A Cayenne object facade to a persistent store. Instances of ObjectContext are
  * used in the application code to access Cayenne persistence features.
- * 
+ *
  * @since 1.2
  */
-public interface ObjectContext extends DataChannel, Serializable {
+public interface ObjectContext extends Serializable {
+
+    /**
+     * Returns a DataChannel that this context uses to access the persistent store.
+     */
+    DataChannel getChannel();
 
     /**
      * Returns EntityResolver that stores all mapping information accessible by
@@ -258,16 +263,6 @@ public interface ObjectContext extends DataChannel, Serializable {
      */
     GraphManager getGraphManager();
 
-    /**
-     * Returns a DataChannel used by this context.
-     *
-     * @deprecated in favor of {@link DataChannel#getParent()}
-     */
-    @Deprecated(since = "5.0", forRemoval = true)
-    default DataChannel getChannel() {
-        return getParent();
-    }
-
     /**
      * Returns <code>true</code> if there are any modified, deleted or new
      * objects registered with this ObjectContext, <code>false</code> otherwise.
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
index 52ad3f46b..77061f4dd 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
@@ -249,13 +249,17 @@ public class DataContext implements ObjectContext {
         EventManager eventManager = channel.getEventManager();
 
         if (eventManager != null) {
-            mergeHandler = new DataContextMergeHandler(this);
 
-            // listen to our channel events...
-            // note that we must reset listener on channel switch, as there is
-            // no
-            // guarantee that a new channel uses the same EventManager.
-            EventUtil.listenForChannelEvents(channel, mergeHandler);
+            // listen to our channel events... note that we must reset listener on channel switch, as there is no
+            // guarantee that a new channel uses the same EventManager. A parent context posts its events on its own
+            // behalf, not on behalf of the channel adapter wrapping it, so listen to the context in that case
+            if (channel instanceof DataContextChannel channelAdapter) {
+                mergeHandler = new DataContextMergeHandler(this, channelAdapter.context());
+                EventUtil.listenForChannelEvents(channelAdapter.context(), mergeHandler);
+            } else {
+                mergeHandler = new DataContextMergeHandler(this, channel);
+                EventUtil.listenForChannelEvents(channel, mergeHandler);
+            }
         }
 
         if (!usingSharedSnapshotCache && getObjectStore() != null) {
@@ -268,7 +272,7 @@ public class DataContext implements ObjectContext {
     }
 
     @Override
-    public DataChannel getParent() {
+    public DataChannel getChannel() {
         attachToRuntimeIfNeeded();
         return channel;
     }
@@ -465,13 +469,7 @@ public class DataContext implements ObjectContext {
         this.queryCache = queryCache;
     }
 
-    /**
-     * Returns EventManager associated with the ObjectStore.
-     *
-     * @since 1.2
-     */
-    @Override
-    public EventManager getEventManager() {
+    private EventManager eventManager() {
         return channel != null ? channel.getEventManager() : null;
     }
 
@@ -479,7 +477,7 @@ public class DataContext implements ObjectContext {
      * @since 1.2
      */
     protected void fireDataChannelCommitted(Object postedBy, GraphDiff changes) {
-        EventManager manager = getEventManager();
+        EventManager manager = eventManager();
 
         if (manager != null) {
             GraphEvent e = new GraphEvent(this, postedBy, changes);
@@ -491,7 +489,7 @@ public class DataContext implements ObjectContext {
      * @since 1.2
      */
     protected void fireDataChannelRolledback(Object postedBy, GraphDiff changes) {
-        EventManager manager = getEventManager();
+        EventManager manager = eventManager();
 
         if (manager != null) {
             GraphEvent e = new GraphEvent(this, postedBy, changes);
@@ -603,12 +601,12 @@ public class DataContext implements ObjectContext {
         // a child context has no snapshot cache, so it can't resolve rows on its own. Resolve them in the parent
         // context and transfer the resulting objects here
         if (getObjectStore().getDataRowCache() == null) {
-            if (!(getParent() instanceof DataContext parentContext)) {
+            if (!(getChannel() instanceof DataContextChannel channelAdapter)) {
                 throw new CayenneRuntimeException(
                         "DataContext has no snapshot cache and no parent DataContext to resolve DataRows");
             }
 
-            List<?> parentObjects = parentContext.objectsFromDataRows(descriptor, dataRows);
+            List<?> parentObjects = channelAdapter.context().objectsFromDataRows(descriptor, dataRows);
             ShallowMergeOperation merger = new ShallowMergeOperation(this);
             List<Persistent> objects = new ArrayList<>(parentObjects.size());
             for (Object parentObject : parentObjects) {
@@ -862,34 +860,22 @@ public class DataContext implements ObjectContext {
 
         if (!ids.isEmpty()) {
             getObjectStore().objectsInvalidated(ids);
-            getParent().onInvalidate(this, ids);
+            getChannel().onInvalidate(this, ids);
         }
     }
 
-    /**
-     * @since 5.0
-     */
-    @Override
-    public void onInvalidate(ObjectContext originatingContext, Collection<ObjectId> ids) {
+    void onInvalidate(Collection<ObjectId> ids) {
         if (!ids.isEmpty()) {
             getObjectStore().objectsInvalidated(ids);
-            getParent().onInvalidate(this, ids);
+            getChannel().onInvalidate(this, ids);
         }
     }
 
-    /**
-     * An implementation of a {@link DataChannel} method that resolves a relationship from the objects already
-     * registered in this context, delegating to the parent channel on a miss. Not intended for direct use, read
-     * the relationship property of an object instead.
-     *
-     * @since 5.0
-     */
-    @Override
-    public List<Persistent> onResolveRelationship(ObjectContext originatingContext, ObjectId sourceId, String relationshipName) {
+    List<Persistent> onResolveRelationship(ObjectContext originatingContext, ObjectId sourceId, String relationshipName) {
 
         List<Persistent> related = resolvedRelationship(sourceId, relationshipName, originatingContext != this);
         if (related == null) {
-            related = getParent().onResolveRelationship(this, sourceId, relationshipName);
+            related = getChannel().onResolveRelationship(this, sourceId, relationshipName);
         }
 
         if (originatingContext == this || related.isEmpty()) {
@@ -1046,7 +1032,7 @@ public class DataContext implements ObjectContext {
      */
     GraphDiff flushToParent(boolean cascade) {
 
-        if (this.getParent() == null) {
+        if (this.getChannel() == null) {
             throw new CayenneRuntimeException("Cannot commit changes - channel is not set.");
         }
 
@@ -1067,7 +1053,7 @@ public class DataContext implements ObjectContext {
             } else {
 
                 try {
-                    parentChanges = getParent().onSync(this, changes, syncType);
+                    parentChanges = getChannel().onSync(this, changes, syncType);
 
                     // note that this is a hack resulting from a fix to CAY-766...
                     // To support valid object state in PostPersist callback,
@@ -1105,7 +1091,7 @@ public class DataContext implements ObjectContext {
 
             // this event is caught by child DataContexts to update temporary ObjectIds with permanent
             if (!diff.isNoop()) {
-                fireDataChannelCommitted(getParent(), diff);
+                fireDataChannelCommitted(getChannel(), diff);
             }
 
             return diff;
@@ -1192,7 +1178,7 @@ public class DataContext implements ObjectContext {
             return new GenericResponse();
         }
 
-        if (this.getParent() == null) {
+        if (this.getChannel() == null) {
             throw new CayenneRuntimeException("Can't run query - parent DataChannel is not set.");
         }
 
@@ -1230,28 +1216,17 @@ public class DataContext implements ObjectContext {
         return result != null ? result : new ArrayList<>(1);
     }
 
-    /**
-     * An implementation of a {@link DataChannel} method that is used by child
-     * contexts to execute queries. Not intended for direct use.
-     *
-     * @since 1.2
-     */
-    public QueryResponse onQuery(ObjectContext context, Query query, boolean iteratedResult) {
+    QueryResponse onQuery(ObjectContext context, Query query, boolean iteratedResult) {
         return new DataContextQueryAction(this, context, query, iteratedResult).execute();
     }
 
-    @Override
-    public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType) {
-        switch (syncType) {
-            case DataChannel.ROLLBACK_CASCADE_SYNC:
-                return onContextRollback();
-            case DataChannel.FLUSH_NOCASCADE_SYNC:
-                return onContextFlush(originatingContext, changes, false);
-            case DataChannel.FLUSH_CASCADE_SYNC:
-                return onContextFlush(originatingContext, changes, true);
-            default:
-                throw new CayenneRuntimeException("Unrecognized SyncMessage type: %d", syncType);
-        }
+    GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType) {
+        return switch (syncType) {
+            case DataChannel.ROLLBACK_CASCADE_SYNC -> onContextRollback();
+            case DataChannel.FLUSH_NOCASCADE_SYNC -> onContextFlush(originatingContext, changes, false);
+            case DataChannel.FLUSH_CASCADE_SYNC -> onContextFlush(originatingContext, changes, true);
+            default -> throw new CayenneRuntimeException("Unrecognized SyncMessage type: %d", syncType);
+        };
     }
 
     GraphDiff onContextRollback() {
@@ -1450,7 +1425,7 @@ public class DataContext implements ObjectContext {
      * @since 1.2
      */
     protected void fireDataChannelChanged(Object postedBy, GraphDiff changes) {
-        EventManager manager = getEventManager();
+        EventManager manager = eventManager();
 
         if (manager != null) {
             GraphEvent e = new GraphEvent(this, postedBy, changes);
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContextChannel.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContextChannel.java
new file mode 100644
index 000000000..86fa9bf56
--- /dev/null
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContextChannel.java
@@ -0,0 +1,83 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access;
+
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.event.EventManager;
+import org.apache.cayenne.graph.GraphDiff;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.Query;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A {@link DataChannel} that connects a nested ObjectContext to its parent {@link DataContext}. All channel
+ * operations are delegated to the parent context. Note that the parent context posts channel events on its own behalf,
+ * so listeners interested in those events should listen to the context, not to this channel.
+ *
+ * @since 5.0
+ */
+public record DataContextChannel(DataContext context) implements DataChannel {
+
+    public DataContextChannel {
+        Objects.requireNonNull(context, "Null parent context");
+    }
+
+    @Override
+    public EventManager getEventManager() {
+        return context.getChannel().getEventManager();
+    }
+
+    @Override
+    public EntityResolver getEntityResolver() {
+        return context.getEntityResolver();
+    }
+
+    @Override
+    public DataChannel getParent() {
+        return context.getChannel();
+    }
+
+    @Override
+    public QueryResponse onQuery(ObjectContext originatingContext, Query query, boolean iteratedResult) {
+        return context.onQuery(originatingContext, query, iteratedResult);
+    }
+
+    @Override
+    public void onInvalidate(ObjectContext originatingContext, Collection<ObjectId> objectIds) {
+        context.onInvalidate(objectIds);
+    }
+
+    @Override
+    public List<Persistent> onResolveRelationship(ObjectContext originatingContext, ObjectId sourceId, String relationshipName) {
+        return context.onResolveRelationship(originatingContext, sourceId, relationshipName);
+    }
+
+    @Override
+    public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType) {
+        return context.onSync(originatingContext, changes, syncType);
+    }
+}
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContextMergeHandler.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContextMergeHandler.java
index 5c37d3527..5f48c0a72 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContextMergeHandler.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContextMergeHandler.java
@@ -44,10 +44,12 @@ import org.apache.cayenne.reflect.ToOneProperty;
 class DataContextMergeHandler implements GraphChangeHandler, DataChannelListener {
 
     private final DataContext context;
+    private final Object eventSource;
     private volatile boolean stopped;
 
-    DataContextMergeHandler(DataContext context) {
+    DataContextMergeHandler(DataContext context, Object eventSource) {
         this.context = context;
+        this.eventSource = eventSource;
     }
 
     void stop() {
@@ -65,11 +67,11 @@ class DataContextMergeHandler implements GraphChangeHandler, DataChannelListener
 
         // this effectively filters out all events that are not coming from peers or
         // grandparents...
-        return e.getSource() == context.getParent()
+        return e.getSource() == eventSource
                 && e.getPostedBy() != context
-                && e.getPostedBy() != context.getParent();
+                && e.getPostedBy() != eventSource;
 
-        // the first condition (e.getSource() == context.getParent()) is actually always
+        // the first condition (e.getSource() == eventSource) is actually always
         // 'true' because of how the listener is registered. Still keep it here as an
         // extra safegurad
     }
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java b/cayenne/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
index 2467492e6..2800469d1 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java
@@ -343,6 +343,6 @@ class DataContextQueryAction {
      * Fetches data from the channel.
      */
     protected void runQuery() {
-        this.response = actingContext.getParent().onQuery(actingContext, query, iteratedResult);
+        this.response = actingContext.getChannel().onQuery(actingContext, query, iteratedResult);
     }
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
index 67fec89d5..cfad0c8fa 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectStore.java
@@ -468,9 +468,9 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa
      */
     public DataRow getCachedSnapshot(ObjectId oid) {
 
-        if (context != null && context.getParent() != null) {
+        if (context != null && context.getChannel() != null) {
             ObjectIdQuery query = new ObjectIdQuery(oid, true, ObjectIdQuery.CACHE_NOREFRESH);
-            List<?> results = context.getParent().onQuery(context, query, false).firstList();
+            List<?> results = context.getChannel().onQuery(context, query, false).firstList();
             return results.isEmpty() ? null : (DataRow) results.get(0);
         }
         else {
@@ -488,9 +488,9 @@ public class ObjectStore implements Serializable, SnapshotEventListener, GraphMa
      */
     public synchronized DataRow getSnapshot(ObjectId oid) {
 
-        if (context != null && context.getParent() != null) {
+        if (context != null && context.getChannel() != null) {
             ObjectIdQuery query = new ObjectIdQuery(oid, true, ObjectIdQuery.CACHE);
-            List<?> results = context.getParent().onQuery(context, query, false).firstList();
+            List<?> results = context.getChannel().onQuery(context, query, false).firstList();
             return results.isEmpty() ? null : (DataRow) results.get(0);
         }
         else {
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ToManyHolder.java b/cayenne/src/main/java/org/apache/cayenne/access/ToManyHolder.java
index 8ebc41610..29413703c 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ToManyHolder.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ToManyHolder.java
@@ -19,7 +19,6 @@
 
 package org.apache.cayenne.access;
 
-import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.PersistenceState;
 import org.apache.cayenne.Persistent;
 
@@ -98,7 +97,7 @@ public abstract class ToManyHolder<E> implements Serializable {
             return new ArrayList<>();
         }
 
-        ObjectContext context = relationshipOwner.getObjectContext();
+        DataContext context = (DataContext) relationshipOwner.getObjectContext();
         List<E> resolved = (List<E>) context.onResolveRelationship(
                 context,
                 relationshipOwner.getObjectId(),
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/ToOneFault.java b/cayenne/src/main/java/org/apache/cayenne/access/ToOneFault.java
index 486aa8b6a..4271264b0 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/ToOneFault.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ToOneFault.java
@@ -72,7 +72,7 @@ public class ToOneFault extends Fault {
     }
 
     Object doResolveFault(Persistent sourceObject, String relationshipName) {
-        ObjectContext context = sourceObject.getObjectContext();
+        DataContext context = (DataContext) sourceObject.getObjectContext();
         List<Persistent> objects = context.onResolveRelationship(
                 context,
                 sourceObject.getObjectId(),
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java
index 54ef05ca7..0a7fc0d8b 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java
@@ -272,7 +272,7 @@ public class GraphBasedDbRowOpSorter implements DbRowOpSorter {
 
         private Map<String, Object> getCachedSnapshot(Persistent object) {
             ObjectIdQuery query = new ObjectIdQuery(object.getObjectId(), true, ObjectIdQuery.CACHE);
-            QueryResponse response = object.getObjectContext().getParent().onQuery(null, query, false);
+            QueryResponse response = object.getObjectContext().getChannel().onQuery(null, query, false);
             @SuppressWarnings("unchecked")
             List<DataRow> result = (List<DataRow>) response.firstList();
             if (result == null || result.isEmpty()) {
diff --git a/cayenne/src/main/java/org/apache/cayenne/ashwood/AshwoodEntitySorter.java b/cayenne/src/main/java/org/apache/cayenne/ashwood/AshwoodEntitySorter.java
index 6e8b3d59b..a2549e9f5 100644
--- a/cayenne/src/main/java/org/apache/cayenne/ashwood/AshwoodEntitySorter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/ashwood/AshwoodEntitySorter.java
@@ -283,7 +283,7 @@ public class AshwoodEntitySorter implements EntitySorter {
 		}
 
 		ObjectIdQuery query = new ObjectIdQuery(object.getObjectId(), true, ObjectIdQuery.CACHE);
-		QueryResponse response = context.getParent().onQuery(null, query, false);
+		QueryResponse response = context.getChannel().onQuery(null, query, false);
 		List<?> result = response.firstList();
 		if (result == null || result.isEmpty()) {
 			return null;
diff --git a/cayenne/src/main/java/org/apache/cayenne/commitlog/CommitLogFilter.java b/cayenne/src/main/java/org/apache/cayenne/commitlog/CommitLogFilter.java
index 758cb4495..706ea2fe1 100644
--- a/cayenne/src/main/java/org/apache/cayenne/commitlog/CommitLogFilter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/commitlog/CommitLogFilter.java
@@ -67,7 +67,7 @@ public class CommitLogFilter implements DataChannelSyncFilter {
 
 		// passing DataDomain, not ObjectContext to speed things up
 		// and avoid capturing changed state when fetching snapshots
-		DataChannel channel = originatingContext.getParent();
+		DataChannel channel = originatingContext.getChannel();
 
 		beforeCommit(changes, channel, beforeDiff);
 		GraphDiff afterDiff = filterChain.onSync(originatingContext, beforeDiff, syncType);
diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/ObjectContextFactory.java b/cayenne/src/main/java/org/apache/cayenne/configuration/ObjectContextFactory.java
index 785099e0b..edb00fbc8 100644
--- a/cayenne/src/main/java/org/apache/cayenne/configuration/ObjectContextFactory.java
+++ b/cayenne/src/main/java/org/apache/cayenne/configuration/ObjectContextFactory.java
@@ -23,19 +23,21 @@ import org.apache.cayenne.ObjectContext;
 
 /**
  * A factory for regular and nested contexts.
- * 
+ *
  * @since 3.1
  */
 public interface ObjectContextFactory {
 
     /**
-     * Creates an ObjectContext attached to a default DataChannel.
+     * Creates an ObjectContext attached to a provided channel
      */
-    ObjectContext createContext();
+    ObjectContext createContext(DataChannel parent);
 
     /**
-     * Creates an ObjectContext attached to a provided channel. This is often used for
-     * nested context creation.
+     * Creates an ObjectContext nested in the provided parent context. The nested context selects and commits its
+     * objects via the parent.
+     *
+     * @since 5.0
      */
-    ObjectContext createContext(DataChannel parent);
+    ObjectContext createContext(ObjectContext parent);
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/DataContextFactory.java b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/DataContextFactory.java
index 4416f5074..97f5aef70 100644
--- a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/DataContextFactory.java
+++ b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/DataContextFactory.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.configuration.runtime;
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.access.DataContextChannel;
 import org.apache.cayenne.access.DataDomain;
 import org.apache.cayenne.access.DataRowStore;
 import org.apache.cayenne.access.DataRowStoreFactory;
@@ -30,92 +31,66 @@ import org.apache.cayenne.cache.QueryCache;
 import org.apache.cayenne.configuration.ObjectContextFactory;
 import org.apache.cayenne.configuration.ObjectStoreFactory;
 import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.event.EventManager;
 
 /**
  * @since 3.1
  */
 public class DataContextFactory implements ObjectContextFactory {
 
-    @Inject
-    protected DataDomain dataDomain;
-
-    @Inject
-    protected EventManager eventManager;
-
     @Inject
     protected DataRowStoreFactory dataRowStoreFactory;
-    
+
     @Inject
     protected ObjectStoreFactory objectStoreFactory;
-    
+
     @Inject
     protected QueryCache queryCache;
 
-    @Override
-    public ObjectContext createContext() {
-        return createdFromDataDomain(dataDomain);
-    }
-
     @Override
     public ObjectContext createContext(DataChannel parent) {
 
-        // this switch may go away once we figure out clean property configuration...
-        if (parent instanceof DataDomain dataDomain) {
-            return createdFromDataDomain(dataDomain);
+        DataChannel c = parent;
+        while (c != null && !(c instanceof DataDomain)) {
+            c = c.getParent();
         }
-        else if (parent instanceof DataContext dataContext) {
-            return createFromDataContext(dataContext);
-        }
-        else {
-            return createFromGenericChannel(parent);
+
+        if (c == null) {
+            throw new IllegalArgumentException("Parent DataChannel is not attached to a DataDomain: " + parent);
         }
-    }
 
-    protected ObjectContext createFromGenericChannel(DataChannel parent) {
+        DataDomain domain = (DataDomain) c;
 
-        // for new dataRowStores use the same name for all stores
-        // it makes it easier to track the event subject
-        DataRowStore snapshotCache = (dataDomain.isSharedCacheEnabled())
-                ? dataDomain.getSharedSnapshotCache()
-                : dataRowStoreFactory.createDataRowStore(dataDomain.getName());
+        // for new dataRowStores use the same name for all stores it makes it easier to track the event subject
+        DataRowStore snapshotCache = domain.isSharedCacheEnabled()
+                ? domain.getSharedSnapshotCache()
+                : dataRowStoreFactory.createDataRowStore(domain.getName());
 
-        DataContext context = newInstance(
-                parent, objectStoreFactory.createObjectStore(snapshotCache));
-        context.setValidatingObjectsOnCommit(dataDomain.isValidatingObjectsOnCommit());
+        DataContext context = newInstance(parent, objectStoreFactory.createObjectStore(snapshotCache));
+        context.setValidatingObjectsOnCommit(domain.isValidatingObjectsOnCommit());
         context.setQueryCache(new NestedQueryCache(queryCache));
         return context;
     }
 
-    protected ObjectContext createFromDataContext(DataContext parent) {
+    @Override
+    public ObjectContext createContext(ObjectContext parent) {
+        if (!(parent instanceof DataContext dataContext)) {
+            throw new IllegalArgumentException("Only a DataContext can be a parent of a nested context. "
+                    + "Unsupported context type: " + parent);
+        }
+
         // child ObjectStore should not have direct access to snapshot cache, so do not
         // pass it in constructor.
         ObjectStore objectStore = objectStoreFactory.createObjectStore(null);
 
-        DataContext context = newInstance(parent, objectStore);
+        DataContext context = newInstance(new DataContextChannel(dataContext), objectStore);
 
-        context.setValidatingObjectsOnCommit(parent.isValidatingObjectsOnCommit());
-        context.setUsingSharedSnapshotCache(parent.isUsingSharedSnapshotCache());
+        context.setValidatingObjectsOnCommit(dataContext.isValidatingObjectsOnCommit());
+        context.setUsingSharedSnapshotCache(dataContext.isUsingSharedSnapshotCache());
         context.setQueryCache(new NestedQueryCache(queryCache));
 
         return context;
     }
 
-    protected ObjectContext createdFromDataDomain(DataDomain parent) {
-
-        // for new dataRowStores use the same name for all stores
-        // it makes it easier to track the event subject
-        DataRowStore snapshotCache = (parent.isSharedCacheEnabled())
-                ? parent.getSharedSnapshotCache()
-                : dataRowStoreFactory.createDataRowStore(parent.getName());
-
-        DataContext context = newInstance(
-                parent, objectStoreFactory.createObjectStore(snapshotCache));
-        context.setValidatingObjectsOnCommit(parent.isValidatingObjectsOnCommit());
-        context.setQueryCache(new NestedQueryCache(queryCache));
-        return context;
-    }
-
     protected DataContext newInstance(DataChannel parent, ObjectStore objectStore) {
         return new DataContext(parent, objectStore);
     }
diff --git a/cayenne/src/main/java/org/apache/cayenne/graph/ChildDiffLoader.java b/cayenne/src/main/java/org/apache/cayenne/graph/ChildDiffLoader.java
index bac1687bb..bcc60a69f 100644
--- a/cayenne/src/main/java/org/apache/cayenne/graph/ChildDiffLoader.java
+++ b/cayenne/src/main/java/org/apache/cayenne/graph/ChildDiffLoader.java
@@ -263,7 +263,7 @@ public class ChildDiffLoader implements GraphChangeHandler {
 
 		// skip context cache lookup, go directly to its channel
 		Query query = new ObjectIdQuery((ObjectId) nodeId);
-		QueryResponse response = context.getParent().onQuery(context, query, false);
+		QueryResponse response = context.getChannel().onQuery(context, query, false);
 		List<?> objects = response.firstList();
 
 		if (objects.size() == 0) {
diff --git a/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntime.java b/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntime.java
index 97fd3b82f..13836c49c 100644
--- a/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntime.java
+++ b/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntime.java
@@ -310,18 +310,29 @@ public class CayenneRuntime {
      * @since 4.0
      */
     public ObjectContext newContext() {
-        return injector.getInstance(ObjectContextFactory.class).createContext();
+        DataDomain domain = getDataDomain();
+        return injector.getInstance(ObjectContextFactory.class).createContext(domain);
     }
 
     /**
-     * Returns a new ObjectContext which is a child of the specified
-     * DataChannel. This method is used for creation of nested ObjectContexts,
-     * with parent ObjectContext passed as an argument.
+     * Returns a new ObjectContext attached to the specified DataChannel.
      *
      * @since 4.0
+     * @deprecated use either {@link #newContext()} or {@link #newContext(ObjectContext)}
      */
+    @Deprecated(since = "5.0", forRemoval = true)
     public ObjectContext newContext(DataChannel parentChannel) {
         return injector.getInstance(ObjectContextFactory.class).createContext(parentChannel);
     }
 
+    /**
+     * Returns a new ObjectContext nested in the specified parent context. The nested context selects and commits its
+     * objects via the parent.
+     *
+     * @since 5.0
+     */
+    public ObjectContext newContext(ObjectContext parentContext) {
+        return injector.getInstance(ObjectContextFactory.class).createContext(parentContext);
+    }
+
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/tx/TransactionFilter.java b/cayenne/src/main/java/org/apache/cayenne/tx/TransactionFilter.java
index 8e28ca212..ea20e11c3 100644
--- a/cayenne/src/main/java/org/apache/cayenne/tx/TransactionFilter.java
+++ b/cayenne/src/main/java/org/apache/cayenne/tx/TransactionFilter.java
@@ -41,7 +41,7 @@ public class TransactionFilter implements DataChannelSyncFilter {
     @Override
     public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType, DataChannelSyncFilterChain filterChain) {
         DataChannelSyncCallbackAction callbackAction = DataChannelSyncCallbackAction.getCallbackAction(
-                originatingContext.getParent().getEntityResolver().getCallbackRegistry(),
+                originatingContext.getEntityResolver().getCallbackRegistry(),
                 originatingContext.getGraphManager(),
                 changes,
                 syncType
diff --git a/cayenne/src/main/java/org/apache/cayenne/util/EventUtil.java b/cayenne/src/main/java/org/apache/cayenne/util/EventUtil.java
index 1c10d7d01..0663d9ac1 100644
--- a/cayenne/src/main/java/org/apache/cayenne/util/EventUtil.java
+++ b/cayenne/src/main/java/org/apache/cayenne/util/EventUtil.java
@@ -21,6 +21,7 @@ package org.apache.cayenne.util;
 
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.DataChannelListener;
+import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.event.EventManager;
 import org.apache.cayenne.event.EventSubject;
 import org.apache.cayenne.graph.GraphEvent;
@@ -58,6 +59,27 @@ public class EventUtil {
         return true;
     }
 
+    /**
+     * Utility method that sets up a GraphChangeListener to be notified when an ObjectContext posts an event, such as
+     * the events a parent context posts on flush, rollback, or a change merged from one of its nested contexts.
+     *
+     * @return false if the context's channel doesn't have an EventManager and therefore does not support events.
+     * @since 5.0
+     */
+    public static boolean listenForChannelEvents(
+            ObjectContext context,
+            DataChannelListener listener) {
+
+        EventManager manager = context.getChannel().getEventManager();
+
+        if (manager == null) {
+            return false;
+        }
+
+        listenForSubjects(manager, listener, context, CHANNEL_SUBJECTS);
+        return true;
+    }
+
     /**
      * Listen for events from all channels that use a given EventManager.
      */
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextDataChannelEventsIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextDataChannelEventsIT.java
index 5e99411f0..88754201e 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextDataChannelEventsIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextDataChannelEventsIT.java
@@ -19,7 +19,6 @@
 
 package org.apache.cayenne.access;
 
-import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.DataChannelListener;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.configuration.runtime.CoreModule;
@@ -162,7 +161,7 @@ public class DataContextDataChannelEventsIT {
         childPeer1.commitChanges();
 
         final MockChannelListener listener = new MockChannelListener();
-        EventUtil.listenForChannelEvents((DataChannel) childPeer1, listener);
+        EventUtil.listenForChannelEvents(childPeer1, listener);
 
         ObjectContext childPeer2 = env.runtime().newContext(context);
 
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextResolveRelationshipIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextResolveRelationshipIT.java
index 94758ef11..507779b73 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextResolveRelationshipIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextResolveRelationshipIT.java
@@ -123,7 +123,7 @@ public class DataContextResolveRelationshipIT {
         Artist childA = child.localObject(a);
 
         env.runWithQueriesBlocked(() -> {
-            List<Persistent> related = child.onResolveRelationship(child, childA.getObjectId(),
+            List<Persistent> related = ((DataContext) child).onResolveRelationship(child, childA.getObjectId(),
                     Artist.PAINTING_ARRAY.getName());
             assertEquals(2, related.size());
             for (Persistent p : related) {
@@ -145,7 +145,7 @@ public class DataContextResolveRelationshipIT {
         Painting childP = child.localObject(p);
 
         env.runWithQueriesBlocked(() -> {
-            List<Persistent> related = child.onResolveRelationship(child, childP.getObjectId(),
+            List<Persistent> related = ((DataContext) child).onResolveRelationship(child, childP.getObjectId(),
                     Painting.TO_ARTIST.getName());
             assertEquals(1, related.size());
             assertSame(child, related.getFirst().getObjectContext());
@@ -166,7 +166,7 @@ public class DataContextResolveRelationshipIT {
 
         // a NEW object is unknown to the database, so its relationships must be resolved from the parent context
         env.runWithQueriesBlocked(() -> {
-            List<Persistent> related = child.onResolveRelationship(child, childA.getObjectId(),
+            List<Persistent> related = ((DataContext) child).onResolveRelationship(child, childA.getObjectId(),
                     Artist.PAINTING_ARRAY.getName());
             assertEquals(1, related.size());
             assertSame(child, related.getFirst().getObjectContext());
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/DataContextSerializationIT.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextSerializationIT.java
index 4ca0b9377..435537e27 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/DataContextSerializationIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextSerializationIT.java
@@ -84,8 +84,8 @@ public class DataContextSerializationIT {
 
         DataContext deserializedContext = Util.cloneViaSerialization(context);
 
-        assertNotNull(deserializedContext.getParent());
-        assertSame(context.getParent(), deserializedContext.getParent());
+        assertNotNull(deserializedContext.getChannel());
+        assertSame(context.getChannel(), deserializedContext.getChannel());
     }
 
     @Test
@@ -95,7 +95,7 @@ public class DataContextSerializationIT {
 
         ObjectContext deserializedContext = Util.cloneViaSerialization(child);
 
-        assertNotNull(deserializedContext.getParent());
+        assertNotNull(deserializedContext.getChannel());
         assertNotNull(deserializedContext.getEntityResolver());
     }
 
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java b/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
index a39de2406..1bc912413 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextReadIT.java
@@ -103,7 +103,7 @@ public class NestedDataContextReadIT {
         ObjectContext child1 = runtime.newContext(context);
 
         assertNotNull(child1);
-        assertSame(context, child1.getChannel());
+        assertSame(context, ((DataContextChannel) child1.getChannel()).context());
         assertTrue(((DataContext) child1).isValidatingObjectsOnCommit());
 
         context.setValidatingObjectsOnCommit(false);
@@ -111,14 +111,14 @@ public class NestedDataContextReadIT {
         ObjectContext child2 = runtime.newContext(context);
 
         assertNotNull(child2);
-        assertSame(context, child2.getChannel());
+        assertSame(context, ((DataContextChannel) child2.getChannel()).context());
         assertFalse(((DataContext) child2).isValidatingObjectsOnCommit());
 
         // second level of nesting
         ObjectContext child21 = runtime.newContext(child2);
 
         assertNotNull(child21);
-        assertSame(child2, child21.getChannel());
+        assertSame(child2, ((DataContextChannel) child21.getChannel()).context());
         assertFalse(((DataContext) child2).isValidatingObjectsOnCommit());
     }
 
diff --git a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactory_DedicatedCacheIT.java b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactory_DedicatedCacheIT.java
index 2a254f55f..0c68ef099 100644
--- a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactory_DedicatedCacheIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactory_DedicatedCacheIT.java
@@ -41,7 +41,7 @@ public class DataContextFactory_DedicatedCacheIT {
 
         DataContext c3 = (DataContext) env.runtime().getInjector()
                 .getInstance(ObjectContextFactory.class)
-                .createContext();
+                .createContext(domain);
 
         assertNotNull(c3.getObjectStore().getDataRowCache());
         assertNull(domain.getSharedSnapshotCache());
diff --git a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactory_ValidationIT.java b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactory_ValidationIT.java
index 44d4036b3..193711af4 100644
--- a/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactory_ValidationIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/configuration/runtime/DataContextFactory_ValidationIT.java
@@ -36,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 public class DataContextFactory_ValidationIT {
 
     private static DataContext createContext(CayenneTestsEnv env) {
-        return (DataContext) env.runtime().getInjector().getInstance(ObjectContextFactory.class).createContext();
+        return (DataContext) env.runtime().getInjector().getInstance(ObjectContextFactory.class).createContext(env.runtime().getDataDomain());
     }
 
     @Nested
diff --git a/cayenne/src/test/java/org/apache/cayenne/runtime/CayenneRuntimeTest.java b/cayenne/src/test/java/org/apache/cayenne/runtime/CayenneRuntimeTest.java
index ee3b9aa57..0d322aff4 100644
--- a/cayenne/src/test/java/org/apache/cayenne/runtime/CayenneRuntimeTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/runtime/CayenneRuntimeTest.java
@@ -21,13 +21,13 @@ package org.apache.cayenne.runtime;
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.access.DataDomain;
 import org.apache.cayenne.configuration.ObjectContextFactory;
 import org.apache.cayenne.di.Injector;
 import org.apache.cayenne.di.Module;
 import org.apache.cayenne.tx.BaseTransaction;
 import org.apache.cayenne.tx.TransactionDescriptor;
 import org.apache.cayenne.tx.TransactionFactory;
-import org.apache.cayenne.tx.TransactionalOperation;
 import org.junit.jupiter.api.Test;
 
 import java.util.Collections;
@@ -54,11 +54,9 @@ public class CayenneRuntimeTest {
         try {
 
             final Object expectedResult = new Object();
-            Object result = runtime.performInTransaction(new TransactionalOperation<Object>() {
-                public Object perform() {
-                    assertSame(tx, BaseTransaction.getThreadTransaction());
-                    return expectedResult;
-                }
+            Object result = runtime.performInTransaction(() -> {
+                assertSame(tx, BaseTransaction.getThreadTransaction());
+                return expectedResult;
             });
 
             assertSame(expectedResult, result);
@@ -87,19 +85,25 @@ public class CayenneRuntimeTest {
 
     @Test
     public void getObjectContext_CustomModule() {
-        final ObjectContext context = new DataContext();
-        final ObjectContextFactory factory = new ObjectContextFactory() {
+        ObjectContext context = new DataContext();
+        ObjectContextFactory factory = new ObjectContextFactory() {
 
+            @Override
             public ObjectContext createContext(DataChannel parent) {
                 return context;
             }
 
-            public ObjectContext createContext() {
+            @Override
+            public ObjectContext createContext(ObjectContext parent) {
                 return context;
             }
         };
 
-        Module module = binder -> binder.bind(ObjectContextFactory.class).toInstance(factory);
+        // 'newContext()' resolves the DataDomain to pass to the factory, so a binding must exist
+        Module module = binder -> {
+            binder.bind(DataDomain.class).toInstance(mock(DataDomain.class));
+            binder.bind(ObjectContextFactory.class).toInstance(factory);
+        };
 
         CayenneRuntime runtime = new CayenneRuntime(Collections.singleton(module));
         assertSame(context, runtime.newContext());
@@ -116,8 +120,7 @@ public class CayenneRuntimeTest {
         try {
             CayenneRuntime.bindThreadInjector(injector);
             assertSame(injector, CayenneRuntime.getThreadInjector());
-        }
-        finally {
+        } finally {
             CayenneRuntime.bindThreadInjector(null);
         }