You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by nt...@apache.org on 2023/12/05 10:57:44 UTC

(cayenne) 02/02: CAY-2829 Merge `BaseContext` with `DataContext` and deprecate it

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

commit d8941d1a89a87ad8417831be2c021d3da758ad8f
Author: stariy95 <st...@gmail.com>
AuthorDate: Tue Dec 5 14:58:54 2023 +0400

    CAY-2829 Merge `BaseContext` with `DataContext` and deprecate it
---
 RELEASE-NOTES.txt                                  |   1 +
 .../main/java/org/apache/cayenne/BaseContext.java  | 681 +--------------------
 .../org/apache/cayenne/access/DataContext.java     | 629 ++++++++++++++++++-
 .../{ => access}/ObjectContextDeleteAction.java    |   7 +-
 .../java/org/apache/cayenne/MockBaseContext.java   | 124 ----
 .../DataContextTest.java}                          | 191 +++---
 .../access/NestedDataContextLocalCacheIT.java      |   5 +-
 7 files changed, 720 insertions(+), 918 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index c4c13a70c..ce84893f8 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -50,6 +50,7 @@ CAY-2824 Rename CayenneServerModuleProvider to CayenneRuntimeModuleProvider
 CAY-2825 Rename package `o.a.c.configuration.server` to `o.a.c.configuration.runtime`
 CAY-2826 Rename `ServerModule` to `CoreModule`
 CAY-2828 Rename values in `org.apache.cayenne.configuration.Constants`
+CAY-2829 Merge `BaseContext` with `DataContext` and deprecate it
 
 Bug Fixes:
 
diff --git a/cayenne/src/main/java/org/apache/cayenne/BaseContext.java b/cayenne/src/main/java/org/apache/cayenne/BaseContext.java
index fd8a5ea4f..d308b4fef 100644
--- a/cayenne/src/main/java/org/apache/cayenne/BaseContext.java
+++ b/cayenne/src/main/java/org/apache/cayenne/BaseContext.java
@@ -18,689 +18,16 @@
  ****************************************************************/
 package org.apache.cayenne;
 
-import org.apache.cayenne.cache.NestedQueryCache;
-import org.apache.cayenne.cache.QueryCache;
-import org.apache.cayenne.runtime.CayenneRuntime;
-import org.apache.cayenne.di.Injector;
-import org.apache.cayenne.event.EventManager;
-import org.apache.cayenne.exp.ValueInjector;
-import org.apache.cayenne.graph.CompoundDiff;
-import org.apache.cayenne.graph.GraphDiff;
-import org.apache.cayenne.graph.GraphEvent;
-import org.apache.cayenne.graph.GraphManager;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.LifecycleEvent;
-import org.apache.cayenne.map.ObjEntity;
-import org.apache.cayenne.query.ObjectIdQuery;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.RefreshQuery;
-import org.apache.cayenne.query.Select;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.util.ObjectContextGraphAction;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import org.apache.cayenne.access.DataContext;
 
 /**
  * A common base superclass for Cayenne ObjectContext implementors.
  * 
  * @since 3.0
+ * @deprecated since 5.0, use {@link DataContext} directly
  */
-public abstract class BaseContext implements ObjectContext {
-
-	/**
-	 * A holder of a ObjectContext bound to the current thread.
-	 * 
-	 * @since 3.0
-	 */
-	protected static final ThreadLocal<ObjectContext> threadObjectContext = new ThreadLocal<ObjectContext>();
-
-	/**
-	 * Returns the ObjectContext bound to the current thread.
-	 * 
-	 * @since 3.0
-	 * @return the ObjectContext associated with caller thread.
-	 * @throws IllegalStateException
-	 *             if there is no ObjectContext bound to the current thread.
-	 */
-	public static ObjectContext getThreadObjectContext() throws IllegalStateException {
-		ObjectContext context = threadObjectContext.get();
-		if (context == null) {
-			throw new IllegalStateException("Current thread has no bound ObjectContext.");
-		}
-
-		return context;
-	}
-
-	/**
-	 * Binds a ObjectContext to the current thread. ObjectContext can later be
-	 * retrieved by users in the same thread by calling
-	 * {@link BaseContext#getThreadObjectContext}. Using null parameter will
-	 * unbind currently bound ObjectContext.
-	 * 
-	 * @since 3.0
-	 */
-	public static void bindThreadObjectContext(ObjectContext context) {
-		threadObjectContext.set(context);
-	}
-
-	// transient variables that should be reinitialized on deserialization from
-	// the
-	// registry
-	protected transient DataChannel channel;
-	protected transient QueryCache queryCache;
-	protected transient EntityResolver entityResolver;
-
-	protected boolean validatingObjectsOnCommit = true;
-
-	/**
-	 * Graph action that handles property changes
-	 * 
-	 * @since 3.1
-	 */
-	protected ObjectContextGraphAction graphAction;
-
-	/**
-	 * Stores user defined properties associated with this DataContext.
-	 * 
-	 * @since 3.0
-	 */
-	protected volatile Map<String, Object> userProperties;
-
+@Deprecated(since = "5.0", forRemoval = true)
+public abstract class BaseContext extends DataContext {
 	protected BaseContext() {
-		graphAction = new ObjectContextGraphAction(this);
-	}
-
-	/**
-	 * Checks whether this context is attached to Cayenne runtime stack and if
-	 * not, attempts to attach itself to the runtime using Injector returned
-	 * from the call to {@link CayenneRuntime#getThreadInjector()}. If thread
-	 * Injector is not available and the context is not attached, throws
-	 * CayenneRuntimeException.
-	 * <p>
-	 * This method is called internally by the context before access to
-	 * transient variables to allow the context to attach to the stack lazily
-	 * following deserialization.
-	 * 
-	 * @return true if the context successfully attached to the thread runtime,
-	 *         false - if it was already attached.
-	 * @since 3.1
-	 */
-	protected boolean attachToRuntimeIfNeeded() {
-		if (channel != null) {
-			return false;
-		}
-
-		Injector injector = CayenneRuntime.getThreadInjector();
-		if (injector == null) {
-			throw new CayenneRuntimeException("Can't attach to Cayenne runtime. "
-					+ "Null injector returned from CayenneRuntime.getThreadInjector()");
-		}
-
-		attachToRuntime(injector);
-		return true;
-	}
-
-	/**
-	 * Attaches this context to the CayenneRuntime whose Injector is passed as
-	 * an argument to this method.
-	 * 
-	 * @since 3.1
-	 */
-	protected void attachToRuntime(Injector injector) {
-
-		// TODO: nested contexts handling??
-		attachToChannel(injector.getInstance(DataChannel.class));
-		setQueryCache(new NestedQueryCache(injector.getInstance(QueryCache.class)));
-	}
-
-	/**
-	 * Attaches to a provided DataChannel.
-	 * 
-	 * @since 3.1
-	 */
-	protected void attachToChannel(DataChannel channel) {
-		if (channel == null) {
-			throw new NullPointerException("Null channel");
-		}
-
-		setChannel(channel);
-		setEntityResolver(channel.getEntityResolver());
-	}
-
-	@Override
-	public abstract void commitChanges();
-
-	@Override
-	public abstract void commitChangesToParent();
-
-	@Override
-	public void deleteObject(Object object) throws DeleteDenyException {
-		deleteObjects(object);
-	}
-
-	@Override
-	public abstract Collection<?> deletedObjects();
-
-	@Override
-	public DataChannel getChannel() {
-		attachToRuntimeIfNeeded();
-		return channel;
-	}
-
-	/**
-	 * Sets a new DataChannel for this context.
-	 * 
-	 * @since 3.1
-	 */
-	public void setChannel(DataChannel channel) {
-		this.channel = channel;
-	}
-
-	@Override
-	public EntityResolver getEntityResolver() {
-		attachToRuntimeIfNeeded();
-		return entityResolver;
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	public void setEntityResolver(EntityResolver entityResolver) {
-		this.entityResolver = entityResolver;
-	}
-
-	/**
-	 * Returns whether this ObjectContext performs object validation before
-	 * commit is executed.
-	 * 
-	 * @since 1.1
-	 */
-	public boolean isValidatingObjectsOnCommit() {
-		return validatingObjectsOnCommit;
-	}
-
-	/**
-	 * Sets the property defining whether this ObjectContext should perform
-	 * object validation before commit is executed.
-	 * 
-	 * @since 1.1
-	 */
-	public void setValidatingObjectsOnCommit(boolean flag) {
-		this.validatingObjectsOnCommit = flag;
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	@Override
-	public <T extends Persistent> T localObject(T objectFromAnotherContext) {
-
-		if (objectFromAnotherContext == null) {
-			throw new NullPointerException("Null object argument");
-		}
-
-		ObjectId id = objectFromAnotherContext.getObjectId();
-
-		// first look for the ID in the local GraphManager
-		synchronized (getGraphManager()) {
-			@SuppressWarnings("unchecked")
-			T localObject = (T) getGraphManager().getNode(id);
-			if (localObject != null) {
-				return localObject;
-			}
-
-			// create a hollow object, optimistically assuming that the ID we got from
-			// 'objectFromAnotherContext' is a valid ID either in the parent context or in the DB.
-			// This essentially defers possible FaultFailureExceptions.
-
-			ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(id.getEntityName());
-			@SuppressWarnings("unchecked")
-			T persistent = (T) descriptor.createObject();
-
-			persistent.setObjectContext(this);
-			persistent.setObjectId(id);
-			persistent.setPersistenceState(PersistenceState.HOLLOW);
-
-			getGraphManager().registerNode(id, persistent);
-
-			return persistent;
-		}
-	}
-
-	@Override
-	public abstract GraphManager getGraphManager();
-
-	@Override
-	public abstract Collection<?> modifiedObjects();
-
-	@Override
-	public abstract <T> T newObject(Class<T> persistentClass);
-
-	@Override
-	public abstract void registerNewObject(Object object);
-
-	@Override
-	public abstract Collection<?> newObjects();
-
-	@Override
-	public abstract QueryResponse performGenericQuery(Query query);
-
-	@Override
-	public abstract List performQuery(Query query);
-
-	/**
-	 * @since 4.0
-	 */
-	@SuppressWarnings("unchecked")
-	@Override
-	public <T> List<T> select(Select<T> query) {
-		return performQuery(query);
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	public <T> T selectOne(Select<T> query) {
-		List<T> objects = select(query);
-
-		if (objects.size() == 0) {
-			return null;
-		} else if (objects.size() > 1) {
-			throw new CayenneRuntimeException("Expected zero or one object, instead query matched: %d", objects.size());
-		}
-
-		return objects.get(0);
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	public <T> T selectFirst(Select<T> query) {
-		List<T> objects = select(query);
-
-		return (objects == null || objects.isEmpty()) ? null : objects.get(0);
-	}
-
-	/**
-	 * @since 4.0
-	 */
-	@Override
-	public <T> void iterate(Select<T> query, ResultIteratorCallback<T> callback) {
-
-		try (ResultIterator<T> it = iterator(query);) {
-			for (T t : it) {
-				callback.next(t);
-			}
-		}
-	}
-
-	@Override
-	public abstract <T> ResultIterator<T> iterator(Select<T> query);
-
-	@Override
-	public <T> ResultBatchIterator<T> batchIterator(Select<T> query, int size) {
-		return new ResultBatchIterator<T>(iterator(query), size);
-	}
-
-	@Override
-	public void prepareForAccess(Persistent object, String property, boolean lazyFaulting) {
-		if (object.getPersistenceState() == PersistenceState.HOLLOW) {
-
-			ObjectId oid = object.getObjectId();
-			List<?> objects = performQuery(new ObjectIdQuery(oid, false, ObjectIdQuery.CACHE));
-
-			if (objects.size() == 0) {
-				throw new FaultFailureException(
-						"Error resolving fault, no matching row exists in the database for ObjectId: " + oid);
-			} else if (objects.size() > 1) {
-				throw new FaultFailureException(
-						"Error resolving fault, more than one row exists in the database for ObjectId: " + oid);
-			}
-
-			// 5/28/2013 - Commented out this block to allow for modifying
-			// objects in the postLoad callback
-			// sanity check...
-			// if (object.getPersistenceState() != PersistenceState.COMMITTED) {
-			//
-			// String state =
-			// PersistenceState.persistenceStateName(object.getPersistenceState());
-			//
-			// // TODO: andrus 4/13/2006, modified and deleted states are
-			// // possible due to
-			// // a race condition, should we handle them here?
-			// throw new
-			// FaultFailureException("Error resolving fault for ObjectId: " +
-			// oid + " and state (" + state
-			// +
-			// "). Possible cause - matching row is missing from the database.");
-			// }
-		}
-
-		// resolve relationship fault
-		if (lazyFaulting && property != null) {
-			ClassDescriptor classDescriptor = getEntityResolver().getClassDescriptor(
-					object.getObjectId().getEntityName());
-			PropertyDescriptor propertyDescriptor = classDescriptor.getProperty(property);
-
-			// If we don't have a property descriptor, there's not much we can
-			// do.
-			// Let the caller know that the specified property could not be
-			// found and list
-			// all of the properties that could be so the caller knows what can
-			// be used.
-			if (propertyDescriptor == null) {
-				final StringBuilder errorMessage = new StringBuilder();
-
-				errorMessage.append(String.format("Property '%s' is not declared for entity '%s'.", property, object
-						.getObjectId().getEntityName()));
-
-				errorMessage.append(" Declared properties are: ");
-
-				// Grab each of the declared properties.
-				final List<String> properties = new ArrayList<>();
-				classDescriptor.visitProperties(new PropertyVisitor() {
-					@Override
-					public boolean visitAttribute(final AttributeProperty property) {
-						properties.add(property.getName());
-
-						return true;
-					}
-
-					@Override
-					public boolean visitToOne(final ToOneProperty property) {
-						properties.add(property.getName());
-
-						return true;
-					}
-
-					@Override
-					public boolean visitToMany(final ToManyProperty property) {
-						properties.add(property.getName());
-
-						return true;
-					}
-				});
-
-				// Now add the declared property names to the error message.
-				boolean first = true;
-				for (String declaredProperty : properties) {
-					if (first) {
-						errorMessage.append(String.format("'%s'", declaredProperty));
-
-						first = false;
-					} else {
-						errorMessage.append(String.format(", '%s'", declaredProperty));
-					}
-				}
-
-				errorMessage.append(".");
-
-				throw new CayenneRuntimeException(errorMessage.toString());
-			}
-
-			// this should trigger fault resolving
-			propertyDescriptor.readProperty(object);
-		}
-	}
-
-	@Override
-	public void propertyChanged(Persistent object, String property, Object oldValue, Object newValue) {
-		graphAction.handlePropertyChange(object, property, oldValue, newValue);
-	}
-
-	@Override
-	public abstract void rollbackChanges();
-
-	@Override
-	public abstract void rollbackChangesLocally();
-
-	@Override
-	public abstract Collection<?> uncommittedObjects();
-
-	public QueryCache getQueryCache() {
-		attachToRuntimeIfNeeded();
-		return queryCache;
-	}
-
-	/**
-	 * Sets a QueryCache to be used for storing cached query results.
-	 */
-	public void setQueryCache(QueryCache queryCache) {
-		this.queryCache = queryCache;
-	}
-
-	/**
-	 * Returns EventManager associated with the ObjectStore.
-	 * 
-	 * @since 1.2
-	 */
-	@Override
-	public EventManager getEventManager() {
-		return channel != null ? channel.getEventManager() : null;
-	}
-
-	@Override
-	public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType) {
-		switch (syncType) {
-		case DataChannel.ROLLBACK_CASCADE_SYNC:
-			return onContextRollback(originatingContext);
-		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 onContextRollback(ObjectContext originatingContext) {
-		rollbackChanges();
-		return new CompoundDiff();
-	}
-
-	protected abstract GraphDiff onContextFlush(ObjectContext originatingContext, GraphDiff changes, boolean cascade);
-
-	/**
-	 * @since 1.2
-	 */
-	protected void fireDataChannelCommitted(Object postedBy, GraphDiff changes) {
-		EventManager manager = getEventManager();
-
-		if (manager != null) {
-			GraphEvent e = new GraphEvent(this, postedBy, changes);
-			manager.postEvent(e, DataChannel.GRAPH_FLUSHED_SUBJECT);
-		}
-	}
-
-	/**
-	 * @since 1.2
-	 */
-	protected void fireDataChannelRolledback(Object postedBy, GraphDiff changes) {
-		EventManager manager = getEventManager();
-
-		if (manager != null) {
-			GraphEvent e = new GraphEvent(this, postedBy, changes);
-			manager.postEvent(e, DataChannel.GRAPH_ROLLEDBACK_SUBJECT);
-		}
-	}
-
-	/**
-	 * @since 1.2
-	 */
-	protected void fireDataChannelChanged(Object postedBy, GraphDiff changes) {
-		EventManager manager = getEventManager();
-
-		if (manager != null) {
-			GraphEvent e = new GraphEvent(this, postedBy, changes);
-			manager.postEvent(e, DataChannel.GRAPH_CHANGED_SUBJECT);
-		}
-	}
-
-	@Override
-	public void invalidateObjects(Collection<?> objects) {
-
-		// don't allow null collections as a matter of coding discipline
-		if (objects == null) {
-			throw new NullPointerException("Null collection of objects to invalidate");
-		}
-
-		if (!objects.isEmpty()) {
-			performGenericQuery(new RefreshQuery(objects));
-		}
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	@Override
-	public <T> void invalidateObjects(T... objects) {
-		if (objects != null && objects.length > 0) {
-			performGenericQuery(new RefreshQuery(Arrays.asList(objects)));
-		}
-	}
-
-	/**
-	 * Returns a map of user-defined properties associated with this
-	 * DataContext.
-	 * 
-	 * @since 3.0
-	 */
-	protected Map<String, Object> getUserProperties() {
-
-		// as not all users will take advantage of properties, creating the
-		// map on demand to keep the context lean...
-		if (userProperties == null) {
-			synchronized (this) {
-				if (userProperties == null) {
-					userProperties = new ConcurrentHashMap<>();
-				}
-			}
-		}
-
-		return userProperties;
-	}
-
-	/**
-	 * Returns a user-defined property previously set via 'setUserProperty'.
-	 * Note that it is a caller responsibility to synchronize access to
-	 * properties.
-	 * 
-	 * @since 3.0
-	 */
-	@Override
-	public Object getUserProperty(String key) {
-		return getUserProperties().get(key);
-	}
-
-	/**
-	 * Sets a user-defined property. Note that it is a caller responsibility to
-	 * synchronize access to properties.
-	 * 
-	 * @since 3.0
-	 */
-	@Override
-	public void setUserProperty(String key, Object value) {
-		getUserProperties().put(key, value);
-	}
-
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @since 5.0
-	 */
-	@Override
-	public void removeUserProperty(String key) {
-		getUserProperties().remove(key);
-	}
-
-	/**
-	 * {@inheritDoc}
-	 *
-	 * @since 5.0
-	 */
-	@Override
-	public void clearUserProperties() {
-		getUserProperties().clear();
-	}
-
-	/**
-	 * If ObjEntity qualifier is set, asks it to inject initial value to an
-	 * object. Also performs all Persistent initialization operations
-	 */
-	protected void injectInitialValue(Object obj) {
-		// must follow this exact order of property initialization per CAY-653,
-		// i.e. have
-		// the id and the context in place BEFORE setPersistence is called
-
-		Persistent object = (Persistent) obj;
-
-		object.setObjectContext(this);
-		object.setPersistenceState(PersistenceState.NEW);
-
-		GraphManager graphManager = getGraphManager();
-		synchronized (graphManager) {
-			graphManager.registerNode(object.getObjectId(), object);
-			graphManager.nodeCreated(object.getObjectId());
-		}
-
-		ObjEntity entity;
-		try {
-			entity = getEntityResolver().getObjEntity(object.getClass());
-		} catch (CayenneRuntimeException ex) {
-			// ObjEntity cannot be fetched, ignored
-			entity = null;
-		}
-
-		if (entity != null) {
-			if (entity.getDeclaredQualifier() instanceof ValueInjector) {
-				((ValueInjector) entity.getDeclaredQualifier()).injectValue(object);
-			}
-		}
-
-		// invoke callbacks
-		getEntityResolver().getCallbackRegistry().performCallbacks(LifecycleEvent.POST_ADD, object);
-	}
-
-	/**
-	 * @since 3.1
-	 */
-	@Override
-	public <T> void deleteObjects(T... objects) throws DeleteDenyException {
-		if (objects == null || objects.length == 0) {
-			return;
-		}
-
-		ObjectContextDeleteAction action = new ObjectContextDeleteAction(this);
-
-		for (Object object : objects) {
-			action.performDelete((Persistent) object);
-		}
-	}
-
-	@Override
-	public void deleteObjects(Collection<?> objects) throws DeleteDenyException {
-		if (objects.isEmpty()) {
-			return;
-		}
-
-		ObjectContextDeleteAction action = new ObjectContextDeleteAction(this);
-
-		// Make a copy to iterate over to avoid ConcurrentModificationException.
-		List<Object> copy = new ArrayList<>(objects);
-		for (Object object : copy) {
-			action.performDelete((Persistent) object);
-		}
 	}
 }
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 4206edfd5..6cf895aa2 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataContext.java
@@ -23,48 +23,59 @@ import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 
-import org.apache.cayenne.BaseContext;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.DataObject;
 import org.apache.cayenne.DataRow;
+import org.apache.cayenne.DeleteDenyException;
 import org.apache.cayenne.Fault;
+import org.apache.cayenne.FaultFailureException;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.PersistenceState;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.ResultBatchIterator;
 import org.apache.cayenne.ResultIterator;
-import org.apache.cayenne.access.util.IteratedSelectObserver;
+import org.apache.cayenne.ResultIteratorCallback;
+import org.apache.cayenne.cache.NestedQueryCache;
+import org.apache.cayenne.cache.QueryCache;
 import org.apache.cayenne.di.Injector;
 import org.apache.cayenne.event.EventManager;
+import org.apache.cayenne.exp.ValueInjector;
 import org.apache.cayenne.graph.ArcId;
 import org.apache.cayenne.graph.ChildDiffLoader;
 import org.apache.cayenne.graph.CompoundDiff;
 import org.apache.cayenne.graph.GraphDiff;
+import org.apache.cayenne.graph.GraphEvent;
 import org.apache.cayenne.graph.GraphManager;
 import org.apache.cayenne.map.DbJoin;
 import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.map.LifecycleEvent;
 import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.map.ObjRelationship;
 import org.apache.cayenne.query.*;
 import org.apache.cayenne.reflect.AttributeProperty;
 import org.apache.cayenne.reflect.ClassDescriptor;
+import org.apache.cayenne.reflect.PropertyDescriptor;
 import org.apache.cayenne.reflect.PropertyVisitor;
 import org.apache.cayenne.reflect.ToManyProperty;
 import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.tx.BaseTransaction;
-import org.apache.cayenne.tx.Transaction;
+import org.apache.cayenne.runtime.CayenneRuntime;
 import org.apache.cayenne.tx.TransactionFactory;
 import org.apache.cayenne.util.EventUtil;
 import org.apache.cayenne.util.GenericResponse;
+import org.apache.cayenne.util.ObjectContextGraphAction;
 import org.apache.cayenne.util.Util;
 
 /**
@@ -73,12 +84,68 @@ import org.apache.cayenne.util.Util;
  * changes to persistent objects that are registered with the context, are not
  * visible to the users of other contexts.
  */
-public class DataContext extends BaseContext {
+public class DataContext implements ObjectContext {
+
+    /**
+     * A holder of a ObjectContext bound to the current thread.
+     *
+     * @since 3.0
+     */
+    protected static final ThreadLocal<ObjectContext> threadObjectContext = new ThreadLocal<ObjectContext>();
+
+    /**
+     * Returns the ObjectContext bound to the current thread.
+     *
+     * @since 3.0
+     * @return the ObjectContext associated with caller thread.
+     * @throws IllegalStateException
+     *             if there is no ObjectContext bound to the current thread.
+     */
+    public static ObjectContext getThreadObjectContext() throws IllegalStateException {
+        ObjectContext context = threadObjectContext.get();
+        if (context == null) {
+            throw new IllegalStateException("Current thread has no bound ObjectContext.");
+        }
+
+        return context;
+    }
+
+    /**
+     * Binds a ObjectContext to the current thread. ObjectContext can later be
+     * retrieved by users in the same thread by calling
+     * {@link #getThreadObjectContext}. Using null parameter will
+     * unbind currently bound ObjectContext.
+     *
+     * @since 3.0
+     */
+    public static void bindThreadObjectContext(ObjectContext context) {
+        threadObjectContext.set(context);
+    }
+
 
     private DataContextDelegate delegate;
-    protected boolean usingSharedSnaphsotCache;
+    protected boolean usingSharedSnapshotCache;
     protected ObjectStore objectStore;
 
+    /**
+     * Graph action that handles property changes
+     *
+     * @since 3.1
+     */
+    protected ObjectContextGraphAction graphAction;
+
+    /**
+     * Stores user defined properties associated with this DataContext.
+     *
+     * @since 3.0
+     */
+    protected volatile Map<String, Object> userProperties;
+
+    // transient variables that should be reinitialized on deserialization from the registry
+    protected transient DataChannel channel;
+    protected transient QueryCache queryCache;
+    protected transient EntityResolver entityResolver;
+
     /**
      * @deprecated since 4.0 used in a method that itself should be deprecated,
      *             so this is a temp code
@@ -88,6 +155,8 @@ public class DataContext extends BaseContext {
 
     protected transient DataContextMergeHandler mergeHandler;
 
+    protected boolean validatingObjectsOnCommit = true;
+
     /**
      * Creates a new DataContext that is not attached to the Cayenne stack.
      */
@@ -102,6 +171,8 @@ public class DataContext extends BaseContext {
      */
     public DataContext(DataChannel channel, ObjectStore objectStore) {
 
+        graphAction = new ObjectContextGraphAction(this);
+
         // inject self as parent context
         if (objectStore != null) {
             this.objectStore = objectStore;
@@ -114,24 +185,66 @@ public class DataContext extends BaseContext {
 
         if (objectStore != null) {
             DataDomain domain = getParentDataDomain();
-            this.usingSharedSnaphsotCache = domain != null
+            this.usingSharedSnapshotCache = domain != null
                     && objectStore.getDataRowCache() == domain.getSharedSnapshotCache();
         }
     }
 
-    @Override
+    /**
+     * Checks whether this context is attached to Cayenne runtime stack and if
+     * not, attempts to attach itself to the runtime using Injector returned
+     * from the call to {@link CayenneRuntime#getThreadInjector()}. If thread
+     * Injector is not available and the context is not attached, throws
+     * CayenneRuntimeException.
+     * <p>
+     * This method is called internally by the context before access to
+     * transient variables to allow the context to attach to the stack lazily
+     * following deserialization.
+     *
+     * @return true if the context successfully attached to the thread runtime,
+     *         false - if it was already attached.
+     * @since 3.1
+     */
+    protected boolean attachToRuntimeIfNeeded() {
+        if (channel != null) {
+            return false;
+        }
+
+        Injector injector = CayenneRuntime.getThreadInjector();
+        if (injector == null) {
+            throw new CayenneRuntimeException("Can't attach to Cayenne runtime. "
+                    + "Null injector returned from CayenneRuntime.getThreadInjector()");
+        }
+
+        attachToRuntime(injector);
+        return true;
+    }
+
+    /**
+     * Attaches this context to the CayenneRuntime whose Injector is passed as
+     * an argument to this method.
+     *
+     * @since 3.1
+     */
     protected void attachToRuntime(Injector injector) {
-        super.attachToRuntime(injector);
+        attachToChannel(injector.getInstance(DataChannel.class));
+        setQueryCache(new NestedQueryCache(injector.getInstance(QueryCache.class)));
         this.transactionFactory = injector.getInstance(TransactionFactory.class);
     }
 
     /**
+     * Attaches to a provided DataChannel.
+     *
      * @since 3.1
      */
-    @Override
     protected void attachToChannel(DataChannel channel) {
 
-        super.attachToChannel(channel);
+        if (channel == null) {
+            throw new NullPointerException("Null channel");
+        }
+
+        setChannel(channel);
+        setEntityResolver(channel.getEntityResolver());
 
         if (mergeHandler != null) {
             mergeHandler.setActive(false);
@@ -150,7 +263,7 @@ public class DataContext extends BaseContext {
             EventUtil.listenForChannelEvents(channel, mergeHandler);
         }
 
-        if (!usingSharedSnaphsotCache && getObjectStore() != null) {
+        if (!usingSharedSnapshotCache && getObjectStore() != null) {
             DataRowStore cache = getObjectStore().getDataRowCache();
 
             if (cache != null) {
@@ -159,6 +272,35 @@ public class DataContext extends BaseContext {
         }
     }
 
+    @Override
+    public DataChannel getChannel() {
+        attachToRuntimeIfNeeded();
+        return channel;
+    }
+
+    /**
+     * Sets a new DataChannel for this context.
+     *
+     * @since 3.1
+     */
+    public void setChannel(DataChannel channel) {
+        this.channel = channel;
+    }
+
+    @Override
+    public EntityResolver getEntityResolver() {
+        attachToRuntimeIfNeeded();
+        return entityResolver;
+    }
+
+    /**
+     * @since 3.1
+     */
+    public void setEntityResolver(EntityResolver entityResolver) {
+        this.entityResolver = entityResolver;
+    }
+
+
     /**
      * Returns a DataDomain used by this DataContext. DataDomain is looked up in
      * the DataChannel hierarchy. If a channel is not a DataDomain or a
@@ -251,6 +393,42 @@ public class DataContext extends BaseContext {
         return getObjectStore().objectsInState(PersistenceState.DELETED);
     }
 
+    @Override
+    public void deleteObject(Object object) throws DeleteDenyException {
+        deleteObjects(object);
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public <T> void deleteObjects(T... objects) throws DeleteDenyException {
+        if (objects == null || objects.length == 0) {
+            return;
+        }
+
+        ObjectContextDeleteAction action = new ObjectContextDeleteAction(this);
+
+        for (Object object : objects) {
+            action.performDelete((Persistent) object);
+        }
+    }
+
+    @Override
+    public void deleteObjects(Collection<?> objects) throws DeleteDenyException {
+        if (objects.isEmpty()) {
+            return;
+        }
+
+        ObjectContextDeleteAction action = new ObjectContextDeleteAction(this);
+
+        // Make a copy to iterate over to avoid ConcurrentModificationException.
+        List<Object> copy = new ArrayList<>(objects);
+        for (Object object : copy) {
+            action.performDelete((Persistent) object);
+        }
+    }
+
     /**
      * Returns a list of objects that are registered with this DataContext and
      * have a state PersistenceState.MODIFIED
@@ -290,6 +468,90 @@ public class DataContext extends BaseContext {
         return objects;
     }
 
+    public QueryCache getQueryCache() {
+        attachToRuntimeIfNeeded();
+        return queryCache;
+    }
+
+    /**
+     * Sets a QueryCache to be used for storing cached query results.
+     */
+    public void setQueryCache(QueryCache queryCache) {
+        this.queryCache = queryCache;
+    }
+
+    /**
+     * Returns EventManager associated with the ObjectStore.
+     *
+     * @since 1.2
+     */
+    @Override
+    public EventManager getEventManager() {
+        return channel != null ? channel.getEventManager() : null;
+    }
+
+    /**
+     * @since 1.2
+     */
+    protected void fireDataChannelCommitted(Object postedBy, GraphDiff changes) {
+        EventManager manager = getEventManager();
+
+        if (manager != null) {
+            GraphEvent e = new GraphEvent(this, postedBy, changes);
+            manager.postEvent(e, DataChannel.GRAPH_FLUSHED_SUBJECT);
+        }
+    }
+
+    /**
+     * @since 1.2
+     */
+    protected void fireDataChannelRolledback(Object postedBy, GraphDiff changes) {
+        EventManager manager = getEventManager();
+
+        if (manager != null) {
+            GraphEvent e = new GraphEvent(this, postedBy, changes);
+            manager.postEvent(e, DataChannel.GRAPH_ROLLEDBACK_SUBJECT);
+        }
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public <T extends Persistent> T localObject(T objectFromAnotherContext) {
+
+        if (objectFromAnotherContext == null) {
+            throw new NullPointerException("Null object argument");
+        }
+
+        ObjectId id = objectFromAnotherContext.getObjectId();
+
+        // first look for the ID in the local GraphManager
+        synchronized (getGraphManager()) {
+            @SuppressWarnings("unchecked")
+            T localObject = (T) getGraphManager().getNode(id);
+            if (localObject != null) {
+                return localObject;
+            }
+
+            // create a hollow object, optimistically assuming that the ID we got from
+            // 'objectFromAnotherContext' is a valid ID either in the parent context or in the DB.
+            // This essentially defers possible FaultFailureExceptions.
+
+            ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(id.getEntityName());
+            @SuppressWarnings("unchecked")
+            T persistent = (T) descriptor.createObject();
+
+            persistent.setObjectContext(this);
+            persistent.setObjectId(id);
+            persistent.setPersistenceState(PersistenceState.HOLLOW);
+
+            getGraphManager().registerNode(id, persistent);
+
+            return persistent;
+        }
+    }
+
     /**
      * Returns a DataRow reflecting current, possibly uncommitted, object state.
      * <p>
@@ -399,6 +661,107 @@ public class DataContext extends BaseContext {
         return snapshot;
     }
 
+    @Override
+    public void prepareForAccess(Persistent object, String property, boolean lazyFaulting) {
+        if (object.getPersistenceState() == PersistenceState.HOLLOW) {
+
+            ObjectId oid = object.getObjectId();
+            List<?> objects = performQuery(new ObjectIdQuery(oid, false, ObjectIdQuery.CACHE));
+
+            if (objects.size() == 0) {
+                throw new FaultFailureException(
+                        "Error resolving fault, no matching row exists in the database for ObjectId: " + oid);
+            } else if (objects.size() > 1) {
+                throw new FaultFailureException(
+                        "Error resolving fault, more than one row exists in the database for ObjectId: " + oid);
+            }
+
+            // 5/28/2013 - Commented out this block to allow for modifying
+            // objects in the postLoad callback
+            // sanity check...
+            // if (object.getPersistenceState() != PersistenceState.COMMITTED) {
+            //
+            // String state =
+            // PersistenceState.persistenceStateName(object.getPersistenceState());
+            //
+            // // TODO: andrus 4/13/2006, modified and deleted states are
+            // // possible due to
+            // // a race condition, should we handle them here?
+            // throw new
+            // FaultFailureException("Error resolving fault for ObjectId: " +
+            // oid + " and state (" + state
+            // +
+            // "). Possible cause - matching row is missing from the database.");
+            // }
+        }
+
+        // resolve relationship fault
+        if (lazyFaulting && property != null) {
+            ClassDescriptor classDescriptor = getEntityResolver().getClassDescriptor(
+                    object.getObjectId().getEntityName());
+            PropertyDescriptor propertyDescriptor = classDescriptor.getProperty(property);
+
+            // If we don't have a property descriptor, there's not much we can
+            // do.
+            // Let the caller know that the specified property could not be
+            // found and list
+            // all of the properties that could be so the caller knows what can
+            // be used.
+            if (propertyDescriptor == null) {
+                final StringBuilder errorMessage = new StringBuilder();
+
+                errorMessage.append(String.format("Property '%s' is not declared for entity '%s'.", property, object
+                        .getObjectId().getEntityName()));
+
+                errorMessage.append(" Declared properties are: ");
+
+                // Grab each of the declared properties.
+                final List<String> properties = new ArrayList<>();
+                classDescriptor.visitProperties(new PropertyVisitor() {
+                    @Override
+                    public boolean visitAttribute(final AttributeProperty property) {
+                        properties.add(property.getName());
+
+                        return true;
+                    }
+
+                    @Override
+                    public boolean visitToOne(final ToOneProperty property) {
+                        properties.add(property.getName());
+
+                        return true;
+                    }
+
+                    @Override
+                    public boolean visitToMany(final ToManyProperty property) {
+                        properties.add(property.getName());
+
+                        return true;
+                    }
+                });
+
+                // Now add the declared property names to the error message.
+                boolean first = true;
+                for (String declaredProperty : properties) {
+                    if (first) {
+                        errorMessage.append(String.format("'%s'", declaredProperty));
+
+                        first = false;
+                    } else {
+                        errorMessage.append(String.format(", '%s'", declaredProperty));
+                    }
+                }
+
+                errorMessage.append(".");
+
+                throw new CayenneRuntimeException(errorMessage.toString());
+            }
+
+            // this should trigger fault resolving
+            propertyDescriptor.readProperty(object);
+        }
+    }
+
     /**
      * Converts a list of DataRows to a List of DataObject registered with this
      * DataContext.
@@ -604,6 +967,44 @@ public class DataContext extends BaseContext {
         });
     }
 
+    /**
+     * If ObjEntity qualifier is set, asks it to inject initial value to an
+     * object. Also performs all Persistent initialization operations
+     */
+    protected void injectInitialValue(Object obj) {
+        // must follow this exact order of property initialization per CAY-653,
+        // i.e. have
+        // the id and the context in place BEFORE setPersistence is called
+
+        Persistent object = (Persistent) obj;
+
+        object.setObjectContext(this);
+        object.setPersistenceState(PersistenceState.NEW);
+
+        GraphManager graphManager = getGraphManager();
+        synchronized (graphManager) {
+            graphManager.registerNode(object.getObjectId(), object);
+            graphManager.nodeCreated(object.getObjectId());
+        }
+
+        ObjEntity entity;
+        try {
+            entity = getEntityResolver().getObjEntity(object.getClass());
+        } catch (CayenneRuntimeException ex) {
+            // ObjEntity cannot be fetched, ignored
+            entity = null;
+        }
+
+        if (entity != null) {
+            if (entity.getDeclaredQualifier() instanceof ValueInjector) {
+                ((ValueInjector) entity.getDeclaredQualifier()).injectValue(object);
+            }
+        }
+
+        // invoke callbacks
+        getEntityResolver().getCallbackRegistry().performCallbacks(LifecycleEvent.POST_ADD, object);
+    }
+
     /**
      * Unregisters a Collection of DataObjects from the DataContext and the
      * underlying ObjectStore. This operation also unsets DataContext for
@@ -615,6 +1016,34 @@ public class DataContext extends BaseContext {
         getObjectStore().objectsUnregistered(dataObjects);
     }
 
+    @Override
+    public void invalidateObjects(Collection<?> objects) {
+
+        // don't allow null collections as a matter of coding discipline
+        if (objects == null) {
+            throw new NullPointerException("Null collection of objects to invalidate");
+        }
+
+        if (!objects.isEmpty()) {
+            performGenericQuery(new RefreshQuery(objects));
+        }
+    }
+
+    /**
+     * @since 3.1
+     */
+    @Override
+    public <T> void invalidateObjects(T... objects) {
+        if (objects != null && objects.length > 0) {
+            performGenericQuery(new RefreshQuery(Arrays.asList(objects)));
+        }
+    }
+
+    @Override
+    public void propertyChanged(Persistent object, String property, Object oldValue, Object newValue) {
+        graphAction.handlePropertyChange(object, property, oldValue, newValue);
+    }
+
     /**
      * If the parent channel is a DataContext, reverts local changes to make
      * this context look like the parent, if the parent channel is a DataDomain,
@@ -685,7 +1114,6 @@ public class DataContext extends BaseContext {
         flushToParent(true);
     }
 
-    @Override
     protected GraphDiff onContextFlush(ObjectContext originatingContext, GraphDiff changes, boolean cascade) {
 
         boolean childContext = this != originatingContext && changes != null;
@@ -792,6 +1220,54 @@ public class DataContext extends BaseContext {
 
     }
 
+    /**
+     * @since 4.0
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> List<T> select(Select<T> query) {
+        return performQuery(query);
+    }
+
+    /**
+     * @since 4.0
+     */
+    @Override
+    public <T> T selectOne(Select<T> query) {
+        List<T> objects = select(query);
+
+        if (objects.size() == 0) {
+            return null;
+        } else if (objects.size() > 1) {
+            throw new CayenneRuntimeException("Expected zero or one object, instead query matched: %d", objects.size());
+        }
+
+        return objects.get(0);
+    }
+
+    /**
+     * @since 4.0
+     */
+    @Override
+    public <T> T selectFirst(Select<T> query) {
+        List<T> objects = select(query);
+
+        return (objects == null || objects.isEmpty()) ? null : objects.get(0);
+    }
+
+    /**
+     * @since 4.0
+     */
+    @Override
+    public <T> void iterate(Select<T> query, ResultIteratorCallback<T> callback) {
+
+        try (ResultIterator<T> it = iterator(query);) {
+            for (T t : it) {
+                callback.next(t);
+            }
+        }
+    }
+
     /**
      * Performs a single database select query returning result as a {@link ResultIterator}.
      * <p>
@@ -894,6 +1370,25 @@ public class DataContext extends BaseContext {
         return new DataContextQueryAction(this, context, query).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 onContextRollback() {
+        rollbackChanges();
+        return new CompoundDiff();
+    }
+
     /**
      * Performs a single database query that does not select rows. Returns an
      * array of update counts.
@@ -941,7 +1436,7 @@ public class DataContext extends BaseContext {
      * @since 1.1
      */
     public List<?> performQuery(String queryName, boolean expireCachedLists) {
-        return performQuery(queryName, Collections.EMPTY_MAP, expireCachedLists);
+        return performQuery(queryName, Collections.emptyMap(), expireCachedLists);
     }
 
     /**
@@ -974,14 +1469,14 @@ public class DataContext extends BaseContext {
      * @since 1.1
      */
     public boolean isUsingSharedSnapshotCache() {
-        return usingSharedSnaphsotCache;
+        return usingSharedSnapshotCache;
     }
 
     /**
      * @since 3.1
      */
     public void setUsingSharedSnapshotCache(boolean flag) {
-        this.usingSharedSnaphsotCache = flag;
+        this.usingSharedSnapshotCache = flag;
     }
 
     // ---------------------------------------------
@@ -1103,11 +1598,16 @@ public class DataContext extends BaseContext {
 
     }
 
-    // this completely meaningless override is needed to expose the method as
-    // package-private ... is there a better way?
-    @Override
+    /**
+     * @since 1.2
+     */
     protected void fireDataChannelChanged(Object postedBy, GraphDiff changes) {
-        super.fireDataChannelChanged(postedBy, changes);
+        EventManager manager = getEventManager();
+
+        if (manager != null) {
+            GraphEvent e = new GraphEvent(this, postedBy, changes);
+            manager.postEvent(e, DataChannel.GRAPH_CHANGED_SUBJECT);
+        }
     }
 
     TransactionFactory getTransactionFactory() {
@@ -1125,4 +1625,93 @@ public class DataContext extends BaseContext {
         this.transactionFactory = transactionFactory;
     }
 
+    @Override
+    public <T> ResultBatchIterator<T> batchIterator(Select<T> query, int size) {
+        return new ResultBatchIterator<T>(iterator(query), size);
+    }
+
+    /**
+     * Returns whether this ObjectContext performs object validation before
+     * commit is executed.
+     *
+     * @since 1.1
+     */
+    public boolean isValidatingObjectsOnCommit() {
+        return validatingObjectsOnCommit;
+    }
+
+    /**
+     * Sets the property defining whether this ObjectContext should perform
+     * object validation before commit is executed.
+     *
+     * @since 1.1
+     */
+    public void setValidatingObjectsOnCommit(boolean flag) {
+        this.validatingObjectsOnCommit = flag;
+    }
+
+    /**
+     * Returns a map of user-defined properties associated with this
+     * DataContext.
+     *
+     * @since 3.0
+     */
+    protected Map<String, Object> getUserProperties() {
+
+        // as not all users will take advantage of properties, creating the
+        // map on demand to keep the context lean...
+        if (userProperties == null) {
+            synchronized (this) {
+                if (userProperties == null) {
+                    userProperties = new ConcurrentHashMap<>();
+                }
+            }
+        }
+
+        return userProperties;
+    }
+
+    /**
+     * Returns a user-defined property previously set via 'setUserProperty'.
+     * Note that it is a caller responsibility to synchronize access to
+     * properties.
+     *
+     * @since 3.0
+     */
+    @Override
+    public Object getUserProperty(String key) {
+        return getUserProperties().get(key);
+    }
+
+    /**
+     * Sets a user-defined property. Note that it is a caller responsibility to
+     * synchronize access to properties.
+     *
+     * @since 3.0
+     */
+    @Override
+    public void setUserProperty(String key, Object value) {
+        getUserProperties().put(key, value);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @since 5.0
+     */
+    @Override
+    public void removeUserProperty(String key) {
+        getUserProperties().remove(key);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @since 5.0
+     */
+    @Override
+    public void clearUserProperties() {
+        getUserProperties().clear();
+    }
+
 }
diff --git a/cayenne/src/main/java/org/apache/cayenne/ObjectContextDeleteAction.java b/cayenne/src/main/java/org/apache/cayenne/access/ObjectContextDeleteAction.java
similarity index 97%
rename from cayenne/src/main/java/org/apache/cayenne/ObjectContextDeleteAction.java
rename to cayenne/src/main/java/org/apache/cayenne/access/ObjectContextDeleteAction.java
index e017c9660..430586c58 100644
--- a/cayenne/src/main/java/org/apache/cayenne/ObjectContextDeleteAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/ObjectContextDeleteAction.java
@@ -17,13 +17,18 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne;
+package org.apache.cayenne.access;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.DeleteDenyException;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
 import org.apache.cayenne.graph.ArcId;
 import org.apache.cayenne.map.DeleteRule;
 import org.apache.cayenne.map.LifecycleEvent;
diff --git a/cayenne/src/test/java/org/apache/cayenne/MockBaseContext.java b/cayenne/src/test/java/org/apache/cayenne/MockBaseContext.java
deleted file mode 100644
index 30ef33769..000000000
--- a/cayenne/src/test/java/org/apache/cayenne/MockBaseContext.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*****************************************************************
- *   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;
-
-import org.apache.cayenne.graph.GraphDiff;
-import org.apache.cayenne.graph.GraphManager;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.Select;
-
-import java.util.Collection;
-import java.util.List;
-
-public class MockBaseContext extends BaseContext {
-
-    @Override
-    public void commitChanges() {
-    }
-
-    @Override
-    public void commitChangesToParent() {
-    }
-
-    @Override
-    public Collection<?> deletedObjects() {
-        return null;
-    }
-
-    @Override
-    public EntityResolver getEntityResolver() {
-        return null;
-    }
-
-    @Override
-    public GraphManager getGraphManager() {
-        return null;
-    }
-
-    @Override
-    public Collection<?> modifiedObjects() {
-        return null;
-    }
-
-    @Override
-    public <T> T newObject(Class<T> persistentClass) {
-        return null;
-    }
-
-    @Override
-    public Collection<?> newObjects() {
-        return null;
-    }
-
-    @Override
-    protected GraphDiff onContextFlush(ObjectContext originatingContext, GraphDiff changes, boolean cascade) {
-        return null;
-    }
-
-    @Override
-    public QueryResponse performGenericQuery(Query query) {
-        return null;
-    }
-
-    @Override
-    public List performQuery(Query query) {
-        return null;
-    }
-
-    @Override
-    public void registerNewObject(Object object) {
-    }
-
-    @Override
-    public void rollbackChanges() {
-    }
-
-    @Override
-    public void rollbackChangesLocally() {
-    }
-
-    @Override
-    public Collection<?> uncommittedObjects() {
-        return null;
-    }
-
-    public ObjectContext createChildContext() {
-        return null;
-    }
-
-    public boolean hasChanges() {
-        return false;
-    }
-
-    public QueryResponse onQuery(ObjectContext originatingContext, Query query) {
-        return null;
-    }
-
-    @Override
-    public <T> ResultIterator<T> iterator(Select<T> query) {
-        return null;
-    }
-
-    @Override
-    public <T> ResultBatchIterator<T> batchIterator(Select<T> query, int size) {
-        return null;
-    }
-
-}
diff --git a/cayenne/src/test/java/org/apache/cayenne/BaseContextTest.java b/cayenne/src/test/java/org/apache/cayenne/access/DataContextTest.java
similarity index 83%
rename from cayenne/src/test/java/org/apache/cayenne/BaseContextTest.java
rename to cayenne/src/test/java/org/apache/cayenne/access/DataContextTest.java
index d53e356e7..1df21d649 100644
--- a/cayenne/src/test/java/org/apache/cayenne/BaseContextTest.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/DataContextTest.java
@@ -1,93 +1,98 @@
-/*****************************************************************
- *   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;
-
-import org.apache.cayenne.cache.QueryCache;
-import org.apache.cayenne.runtime.CayenneRuntime;
-import org.apache.cayenne.di.DIBootstrap;
-import org.apache.cayenne.di.Injector;
-import org.apache.cayenne.di.Module;
-import org.junit.Test;
-
-import java.util.Map;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.mock;
-
-public class BaseContextTest {
-
-    @Test
-    public void testUserPropertiesLazyInit() {
-        BaseContext context = new MockBaseContext();
-        assertNull(context.userProperties);
-
-        Map<String, Object> properties = context.getUserProperties();
-        assertNotNull(properties);
-        assertSame(properties, context.getUserProperties());
-    }
-
-    @Test
-    public void testAttachToRuntimeIfNeeded() {
-
-        final DataChannel channel = mock(DataChannel.class);
-        final QueryCache cache = mock(QueryCache.class);
-
-        Module testModule = binder -> {
-            binder.bind(DataChannel.class).toInstance(channel);
-            binder.bind(QueryCache.class).toInstance(cache);
-        };
-
-        Injector injector = DIBootstrap.createInjector(testModule);
-
-        BaseContext context = new MockBaseContext();
-        assertNull(context.channel);
-        assertNull(context.queryCache);
-
-        Injector oldInjector = CayenneRuntime.getThreadInjector();
-        try {
-
-            CayenneRuntime.bindThreadInjector(injector);
-
-            assertTrue(context.attachToRuntimeIfNeeded());
-            assertSame(channel, context.channel);
-
-            assertFalse(context.attachToRuntimeIfNeeded());
-            assertFalse(context.attachToRuntimeIfNeeded());
-        }
-        finally {
-            CayenneRuntime.bindThreadInjector(oldInjector);
-        }
-    }
-
-    @Test
-    public void testAttachToRuntimeIfNeeded_NoStack() {
-
-        BaseContext context = new MockBaseContext();
-        assertNull(context.channel);
-        assertNull(context.queryCache);
-
-        try {
-            context.attachToRuntimeIfNeeded();
-            fail("No thread stack, must have thrown");
-        }
-        catch (CayenneRuntimeException e) {
-            // expected
-        }
-    }
-}
+/*****************************************************************
+ *   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.CayenneRuntimeException;
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.cache.QueryCache;
+import org.apache.cayenne.di.DIBootstrap;
+import org.apache.cayenne.di.Injector;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.apache.cayenne.tx.TransactionFactory;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+
+public class DataContextTest {
+    @Test
+    public void testUserPropertiesLazyInit() {
+        DataContext context = new DataContext();
+        assertNull(context.userProperties);
+
+        Map<String, Object> properties = context.getUserProperties();
+        assertNotNull(properties);
+        assertSame(properties, context.getUserProperties());
+    }
+
+    @Test
+    public void testAttachToRuntimeIfNeeded() {
+
+        final DataChannel channel = mock(DataChannel.class);
+        final QueryCache cache = mock(QueryCache.class);
+        final TransactionFactory factory = mock(TransactionFactory.class);
+
+        Module testModule = binder -> {
+            binder.bind(DataChannel.class).toInstance(channel);
+            binder.bind(QueryCache.class).toInstance(cache);
+            binder.bind(TransactionFactory.class).toInstance(factory);
+        };
+
+        Injector injector = DIBootstrap.createInjector(testModule);
+
+        DataContext context = new DataContext();
+        assertNull(context.channel);
+        assertNull(context.queryCache);
+
+        Injector oldInjector = CayenneRuntime.getThreadInjector();
+        try {
+
+            CayenneRuntime.bindThreadInjector(injector);
+
+            assertTrue(context.attachToRuntimeIfNeeded());
+            assertSame(channel, context.getChannel());
+
+            assertFalse(context.attachToRuntimeIfNeeded());
+            assertFalse(context.attachToRuntimeIfNeeded());
+        }
+        finally {
+            CayenneRuntime.bindThreadInjector(oldInjector);
+        }
+    }
+
+    @Test
+    public void testAttachToRuntimeIfNeeded_NoStack() {
+
+        DataContext context = new DataContext();
+        assertNull(context.channel);
+        assertNull(context.queryCache);
+
+        try {
+            context.attachToRuntimeIfNeeded();
+            fail("No thread stack, must have thrown");
+        }
+        catch (CayenneRuntimeException e) {
+            // expected
+        }
+    }
+}
\ No newline at end of file
diff --git a/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextLocalCacheIT.java b/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextLocalCacheIT.java
index 67801b692..c15d8e82d 100644
--- a/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextLocalCacheIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/access/NestedDataContextLocalCacheIT.java
@@ -20,7 +20,6 @@ package org.apache.cayenne.access;
 
 import java.util.List;
 
-import org.apache.cayenne.BaseContext;
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.query.ObjectSelect;
@@ -52,14 +51,14 @@ public class NestedDataContextLocalCacheIT extends RuntimeCase {
 
         ObjectContext child1 = runtime.newContext(context);
 
-        assertNull(((BaseContext) child1).getQueryCache().get(
+        assertNull(((DataContext) child1).getQueryCache().get(
                 query.getMetaData(child1.getEntityResolver())));
 
         assertNull(context.getQueryCache().get(
                 query.getMetaData(context.getEntityResolver())));
 
         List<?> results = child1.performQuery(query);
-        assertSame(results, ((BaseContext) child1).getQueryCache().get(
+        assertSame(results, ((DataContext) child1).getQueryCache().get(
                 query.getMetaData(child1.getEntityResolver())));
 
         assertNull(context.getQueryCache().get(