/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
****************************************************************/
package org.apache.cayenne.access;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.Fault;
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.ResultIterator;
import org.apache.cayenne.access.util.IteratedSelectObserver;
import org.apache.cayenne.di.Injector;
import org.apache.cayenne.event.EventManager;
import org.apache.cayenne.graph.ChildDiffLoader;
import org.apache.cayenne.graph.CompoundDiff;
import org.apache.cayenne.graph.GraphDiff;
import org.apache.cayenne.graph.GraphManager;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
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.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.tx.TransactionFactory;
import org.apache.cayenne.util.EventUtil;
import org.apache.cayenne.util.GenericResponse;
import org.apache.cayenne.util.ResultIteratorIterator;
import org.apache.cayenne.util.Util;
/**
* The most common implementation of {@link ObjectContext}. DataContext is an
* isolated container of an object graph, in a sense that any uncommitted
* changes to persistent objects that are registered with the context, are not
* visible to the users of other contexts.
*/
public class DataContext extends BaseContext {
private DataContextDelegate delegate;
protected boolean usingSharedSnaphsotCache;
protected ObjectStore objectStore;
/**
* @deprecated since 4.0 used in a method that itself should be deprecated,
* so this is a temp code
*/
@Deprecated
protected transient TransactionFactory transactionFactory;
protected transient DataContextMergeHandler mergeHandler;
/**
* Creates a new DataContext that is not attached to the Cayenne stack.
*/
public DataContext() {
this(null, null);
}
/**
* Creates a new DataContext with parent DataChannel and ObjectStore.
*
* @since 1.2
*/
public DataContext(DataChannel channel, ObjectStore objectStore) {
// inject self as parent context
if (objectStore != null) {
this.objectStore = objectStore;
objectStore.setContext(this);
}
if (channel != null) {
attachToChannel(channel);
}
if (objectStore != null) {
DataDomain domain = getParentDataDomain();
this.usingSharedSnaphsotCache = domain != null
&& objectStore.getDataRowCache() == domain.getSharedSnapshotCache();
}
}
@Override
protected void attachToRuntime(Injector injector) {
super.attachToRuntime(injector);
this.transactionFactory = injector.getInstance(TransactionFactory.class);
}
/**
* @since 3.1
*/
@Override
protected void attachToChannel(DataChannel channel) {
super.attachToChannel(channel);
if (mergeHandler != null) {
mergeHandler.setActive(false);
mergeHandler = null;
}
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);
}
if (!usingSharedSnaphsotCache && getObjectStore() != null) {
DataRowStore cache = getObjectStore().getDataRowCache();
if (cache != null) {
cache.setEventManager(eventManager);
}
}
}
/**
* Returns a DataDomain used by this DataContext. DataDomain is looked up in
* the DataChannel hierarchy. If a channel is not a DataDomain or a
* DataContext, null is returned.
*
* @return DataDomain that is a direct or indirect parent of this
* DataContext in the DataChannel hierarchy.
* @since 1.1
*/
public DataDomain getParentDataDomain() {
attachToRuntimeIfNeeded();
if (channel == null) {
return null;
}
if (channel instanceof DataDomain) {
return (DataDomain) channel;
}
List response = channel.onQuery(this, new DataDomainQuery()).firstList();
if (response != null && response.size() > 0 && response.get(0) instanceof DataDomain) {
return (DataDomain) response.get(0);
}
return null;
}
/**
* Sets a DataContextDelegate for this context. Delegate is notified of
* certain events in the DataContext lifecycle and can customize DataContext
* behavior.
*
* @since 1.1
*/
public void setDelegate(DataContextDelegate delegate) {
this.delegate = delegate;
}
/**
* Returns a delegate currently associated with this DataContext.
*
* @since 1.1
*/
public DataContextDelegate getDelegate() {
return delegate;
}
/**
* @return a delegate instance if it is initialized, or a shared noop
* implementation the context has no delegate. Useful to prevent
* extra null checks and conditional logic in the code.
* @since 1.1
*/
DataContextDelegate nonNullDelegate() {
return (delegate != null) ? delegate : NoopDelegate.noopDelegate;
}
/**
* Returns ObjectStore associated with this DataContext.
*/
public ObjectStore getObjectStore() {
return objectStore;
}
/**
* Returns <code>true</code> if there are any modified, deleted or new
* objects registered with this DataContext, <code>false</code> otherwise.
*/
public boolean hasChanges() {
return getObjectStore().hasChanges();
}
/**
* Returns a list of objects that are registered with this DataContext and
* have a state PersistenceState.NEW
*/
@Override
public Collection<?> newObjects() {
return getObjectStore().objectsInState(PersistenceState.NEW);
}
/**
* Returns a list of objects that are registered with this DataContext and
* have a state PersistenceState.DELETED
*/
@Override
public Collection<?> deletedObjects() {
return getObjectStore().objectsInState(PersistenceState.DELETED);
}
/**
* Returns a list of objects that are registered with this DataContext and
* have a state PersistenceState.MODIFIED
*/
@Override
public Collection<?> modifiedObjects() {
return getObjectStore().objectsInState(PersistenceState.MODIFIED);
}
/**
* Returns a collection of all uncommitted registered objects.
*
* @since 1.2
*/
@Override
public Collection<?> uncommittedObjects() {
int len = getObjectStore().registeredObjectsCount();
if (len == 0) {
return Collections.EMPTY_LIST;
}
// guess target collection size
Collection<Object> objects = new ArrayList<>(len > 100 ? len / 2 : len);
Iterator it = getObjectStore().getObjectIterator();
while (it.hasNext()) {
Persistent object = (Persistent) it.next();
int state = object.getPersistenceState();
if (state == PersistenceState.MODIFIED || state == PersistenceState.NEW
|| state == PersistenceState.DELETED) {
objects.add(object);
}
}
return objects;
}
/**
* Returns a DataRow reflecting current, possibly uncommitted, object state.
* <p>
* <strong>Warning:</strong> This method will return a partial snapshot if
* an object or one of its related objects that propagate their keys to this
* object have temporary ids. DO NOT USE this method if you expect a DataRow
* to represent a complete object state.
* </p>
*
* @since 1.1
*/
public DataRow currentSnapshot(final Persistent object) {
// for a HOLLOW object return snapshot from cache
if (object.getPersistenceState() == PersistenceState.HOLLOW && object.getObjectContext() != null) {
return getObjectStore().getSnapshot(object.getObjectId());
}
ObjEntity entity = getEntityResolver().getObjEntity(object);
final ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(entity.getName());
final DataRow snapshot = new DataRow(10);
snapshot.setEntityName(entity.getName());
descriptor.visitProperties(new PropertyVisitor() {
public boolean visitAttribute(AttributeProperty property) {
ObjAttribute objAttr = property.getAttribute();
// processing compound attributes correctly
snapshot.put(objAttr.getDbAttributePath(), property.readPropertyDirectly(object));
return true;
}
public boolean visitToMany(ToManyProperty property) {
// do nothing
return true;
}
public boolean visitToOne(ToOneProperty property) {
ObjRelationship rel = property.getRelationship();
// if target doesn't propagates its key value, skip it
if (rel.isSourceIndependentFromTargetChange()) {
return true;
}
Object targetObject = property.readPropertyDirectly(object);
if (targetObject == null) {
return true;
}
// if target is Fault, get id attributes from stored snapshot
// to avoid unneeded fault triggering
if (targetObject instanceof Fault) {
DataRow storedSnapshot = getObjectStore().getSnapshot(object.getObjectId());
if (storedSnapshot == null) {
throw new CayenneRuntimeException("No matching objects found for ObjectId %s"
+ ". Object may have been deleted externally.", object.getObjectId());
}
DbRelationship dbRel = rel.getDbRelationships().get(0);
for (DbJoin join : dbRel.getJoins()) {
String key = join.getSourceName();
snapshot.put(key, storedSnapshot.get(key));
}
return true;
}
// target is resolved and we have an FK->PK to it,
// so extract it from target...
Persistent target = (Persistent) targetObject;
Map<String, Object> idParts = target.getObjectId().getIdSnapshot();
// this may happen in uncommitted objects - see the warning in
// the JavaDoc
// of
// this method.
if (idParts.isEmpty()) {
return true;
}
DbRelationship dbRel = rel.getDbRelationships().get(0);
Map<String, Object> fk = dbRel.srcFkSnapshotWithTargetSnapshot(idParts);
snapshot.putAll(fk);
return true;
}
});
// process object id map
// we should ignore any object id values if a corresponding attribute
// is a part of relationship "toMasterPK", since those values have been
// set above when db relationships where processed.
Map<String, Object> thisIdParts = object.getObjectId().getIdSnapshot();
if (thisIdParts != null) {
// put only those that do not exist in the map
for (Map.Entry<String, Object> entry : thisIdParts.entrySet()) {
String nextKey = entry.getKey();
if (!snapshot.containsKey(nextKey)) {
snapshot.put(nextKey, entry.getValue());
}
}
}
return snapshot;
}
/**
* Converts a list of DataRows to a List of DataObject registered with this
* DataContext.
*
* @since 3.0
*/
public List objectsFromDataRows(ClassDescriptor descriptor, List<? extends DataRow> dataRows) {
// TODO: If data row cache is not available it means that current data
// context is
// child. We need to redirect this method call to parent data context as
// an
// internal query. It is not obvious and has some overhead. Redesign for
// nested
// contexts should be done.
if (getObjectStore().getDataRowCache() == null) {
return objectsFromDataRowsFromParentContext(descriptor, dataRows);
}
return new ObjectResolver(this, descriptor, true).synchronizedObjectsFromDataRows(dataRows);
}
private List objectsFromDataRowsFromParentContext(ClassDescriptor descriptor, List<? extends DataRow> dataRows) {
return getChannel().onQuery(this, new ObjectsFromDataRowsQuery(descriptor, dataRows)).firstList();
}
/**
* Creates a DataObject from DataRow.
*
* @see DataRow
* @since 3.1
*/
public <T extends Persistent> T objectFromDataRow(Class<T> objectClass, DataRow dataRow) {
ObjEntity entity = this.getEntityResolver().getObjEntity(objectClass);
if (entity == null) {
throw new CayenneRuntimeException("Unmapped Java class: %s", objectClass);
}
ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(entity.getName());
List<T> list = objectsFromDataRows(descriptor, Collections.singletonList(dataRow));
return list.get(0);
}
/**
* Creates a DataObject from DataRow. This variety of the
* 'objectFromDataRow' method is normally used for generic classes.
*
* @see DataRow
* @since 3.1
*/
public DataObject objectFromDataRow(String entityName, DataRow dataRow) {
ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(entityName);
List<?> list = objectsFromDataRows(descriptor, Collections.singletonList(dataRow));
return (DataObject) list.get(0);
}
/**
* Creates and registers a new persistent object.
*
* @since 1.2
*/
@Override
public <T> T newObject(Class<T> persistentClass) {
if (persistentClass == null) {
throw new NullPointerException("Null 'persistentClass'");
}
ObjEntity entity = getEntityResolver().getObjEntity(persistentClass);
if (entity == null) {
throw new IllegalArgumentException("Class is not mapped with Cayenne: " + persistentClass.getName());
}
return (T) newObject(entity.getName());
}
/**
* Instantiates a new object and registers it with this context. Object
* class is determined from the mapped entity. Object class must have a
* default constructor.
* <p>
* <i>Note: in most cases {@link #newObject(Class)} method should be used,
* however this method is helpful when generic persistent classes are
* used.</i>
*
* @since 3.0
*/
public Persistent newObject(String entityName) {
ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(entityName);
if (descriptor == null) {
throw new IllegalArgumentException("Invalid entity name: " + entityName);
}
Persistent object;
try {
object = (Persistent) descriptor.createObject();
} catch (Exception ex) {
throw new CayenneRuntimeException("Error instantiating object.", ex);
}
// this will initialize to-many lists
descriptor.injectValueHolders(object);
ObjectId id = new ObjectId(entityName);
// note that the order of initialization of persistence artifacts below
// is
// important - do not change it lightly
object.setObjectId(id);
injectInitialValue(object);
return object;
}
/**
* Registers a transient object with the context, recursively registering
* all transient persistent objects attached to this object via
* relationships.
* <p>
* <i>Note that since 3.0 this method takes Object as an argument instead of
* a {@link DataObject}.</i>
*
* @param object
* new object that needs to be made persistent.
*/
@Override
public void registerNewObject(Object object) {
if (object == null) {
throw new NullPointerException("Can't register null object.");
}
ObjEntity entity = getEntityResolver().getObjEntity((Persistent) object);
if (entity == null) {
throw new IllegalArgumentException("Can't find ObjEntity for Persistent class: "
+ object.getClass().getName() + ", class is likely not mapped.");
}
final Persistent persistent = (Persistent) object;
// sanity check - maybe already registered
if (persistent.getObjectId() != null) {
if (persistent.getObjectContext() == this) {
// already registered, just ignore
return;
} else if (persistent.getObjectContext() != null) {
throw new IllegalStateException("Persistent is already registered with another DataContext. "
+ "Try using 'localObjects()' instead.");
}
} else {
persistent.setObjectId(new ObjectId(entity.getName()));
}
ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(entity.getName());
if (descriptor == null) {
throw new IllegalArgumentException("Invalid entity name: " + entity.getName());
}
injectInitialValue(object);
// now we need to find all arc changes, inject missing value holders and
// pull in
// all transient connected objects
descriptor.visitProperties(new PropertyVisitor() {
public boolean visitToMany(ToManyProperty property) {
property.injectValueHolder(persistent);
if (!property.isFault(persistent)) {
Object value = property.readProperty(persistent);
Collection<Map.Entry> collection = (value instanceof Map) ? ((Map) value).entrySet()
: (Collection) value;
Iterator<Map.Entry> it = collection.iterator();
while (it.hasNext()) {
Object target = it.next();
if (target instanceof Persistent) {
Persistent targetDO = (Persistent) target;
// make sure it is registered
registerNewObject(targetDO);
getObjectStore().arcCreated(persistent.getObjectId(), targetDO.getObjectId(),
property.getName());
}
}
}
return true;
}
public boolean visitToOne(ToOneProperty property) {
Object target = property.readPropertyDirectly(persistent);
if (target instanceof Persistent) {
Persistent targetDO = (Persistent) target;
// make sure it is registered
registerNewObject(targetDO);
getObjectStore().arcCreated(persistent.getObjectId(), targetDO.getObjectId(), property.getName());
}
return true;
}
public boolean visitAttribute(AttributeProperty property) {
return true;
}
});
}
/**
* Unregisters a Collection of DataObjects from the DataContext and the
* underlying ObjectStore. This operation also unsets DataContext for
* each object and changes its state to TRANSIENT.
*
* @see #invalidateObjects(Collection)
*/
public void unregisterObjects(Collection dataObjects) {
getObjectStore().objectsUnregistered(dataObjects);
}
/**
* 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,
* reverts all changes.
*
* @since 1.2
*/
@Override
public void rollbackChangesLocally() {
if (objectStore.hasChanges()) {
GraphDiff diff = getObjectStore().getChanges();
getObjectStore().objectsRolledBack();
fireDataChannelRolledback(this, diff);
}
}
/**
* Reverts any changes that have occurred to objects registered with
* DataContext; also performs cascading rollback of all parent DataContexts.
*/
@Override
public void rollbackChanges() {
if (objectStore.hasChanges()) {
GraphDiff diff = getObjectStore().getChanges();
// call channel with changes BEFORE reverting them, so that any
// interceptors
// could record them
if (channel != null) {
channel.onSync(this, diff, DataChannel.ROLLBACK_CASCADE_SYNC);
}
getObjectStore().objectsRolledBack();
fireDataChannelRolledback(this, diff);
} else {
if (channel != null) {
channel.onSync(this, new CompoundDiff(), DataChannel.ROLLBACK_CASCADE_SYNC);
}
}
}
/**
* "Flushes" the changes to the parent {@link DataChannel}. If the parent
* channel is a DataContext, it updates its objects with this context's
* changes, without a database update. If it is a DataDomain (the most
* common case), the changes are written to the database. To cause cascading
* commit all the way to the database, one must use {@link #commitChanges()}
* .
*
* @since 1.2
* @see #commitChanges()
*/
@Override
public void commitChangesToParent() {
flushToParent(false);
}
/**
* Synchronizes object graph with the database. Executes needed insert,
* update and delete queries (generated internally).
*/
@Override
public void commitChanges() throws CayenneRuntimeException {
flushToParent(true);
}
@Override
protected GraphDiff onContextFlush(ObjectContext originatingContext, GraphDiff changes, boolean cascade) {
boolean childContext = this != originatingContext && changes != null;
try {
if (childContext) {
getObjectStore().childContextSyncStarted();
changes.apply(new ChildDiffLoader(this));
fireDataChannelChanged(originatingContext, changes);
}
return (cascade) ? flushToParent(true) : new CompoundDiff();
} finally {
if (childContext) {
getObjectStore().childContextSyncStopped();
}
}
}
/**
* Synchronizes with the parent channel, performing a flush or a commit.
*
* @since 1.2
*/
GraphDiff flushToParent(boolean cascade) {
if (this.getChannel() == null) {
throw new CayenneRuntimeException("Cannot commit changes - channel is not set.");
}
int syncType = cascade ? DataChannel.FLUSH_CASCADE_SYNC : DataChannel.FLUSH_NOCASCADE_SYNC;
ObjectStore objectStore = getObjectStore();
GraphDiff parentChanges = null;
// prevent multiple commits occurring simultaneously
synchronized (objectStore) {
ObjectStoreGraphDiff changes = objectStore.getChanges();
boolean noop = isValidatingObjectsOnCommit() ? changes.validateAndCheckNoop() : changes.isNoop();
if (noop) {
// need to clear phantom changes
objectStore.postprocessAfterPhantomCommit();
} else {
try {
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,
// 'postprocessAfterCommit' is
// invoked by DataDomain.onSync(..). Unless the parent is
// DataContext,
// and
// this method is not invoked!! As a result, PostPersist
// will contain
// temp
// ObjectIds in nested contexts and perm ones in flat
// contexts.
// Pending better callback design .....
if (objectStore.hasChanges()) {
objectStore.postprocessAfterCommit(parentChanges);
}
// this event is caught by peer nested DataContexts to
// synchronize the
// state
fireDataChannelCommitted(this, changes);
}
// "catch" is needed to unwrap OptimisticLockExceptions
catch (CayenneRuntimeException ex) {
Throwable unwound = Util.unwindException(ex);
if (unwound instanceof CayenneRuntimeException) {
throw (CayenneRuntimeException) unwound;
} else {
throw new CayenneRuntimeException("Commit Exception", unwound);
}
}
}
// merge changes from parent as well as changes caused by lifecycle
// event
// callbacks/listeners...
CompoundDiff diff = new CompoundDiff();
diff.addAll(objectStore.getLifecycleEventInducedChanges());
if (parentChanges != null) {
diff.add(parentChanges);
}
// this event is caught by child DataContexts to update temporary
// ObjectIds with permanent
if (!diff.isNoop()) {
fireDataChannelCommitted(getChannel(), diff);
}
return diff;
}
}
@SuppressWarnings("unchecked")
@Override
public <T> ResultIterator<T> iterator(final Select<T> query) {
final ResultIterator<?> rows = performIteratedQuery(query);
final QueryMetadata md = query.getMetaData(getEntityResolver());
if (md.isFetchingDataRows() || isObjectArrayResult(md)) {
// no need to convert result
return (ResultIterator<T>) rows;
} else {
// this is a bit optimized version of 'objectFromDataRow' with
// resolver cached for reuse... still the rest is pretty suboptimal
final ObjectResolver resolver = new ObjectResolver(this, md.getClassDescriptor(), true);
return new DataRowResultIterator(rows, resolver);
}
}
/**
* This method repeats logic of DataDomainQueryAction.interceptObjectConversion() method.
* The difference is that iterator(or batchIterator) doesn't support "mixed" results.
*/
private boolean isObjectArrayResult(QueryMetadata md) {
List<Object> resultMapping = md.getResultSetMapping();
if(resultMapping == null) {
return false;
}
if (md.isSingleResultSetMapping()) {
return !(resultMapping.get(0) instanceof EntityResultSegment);
} else {
return true;
}
}
/**
* Performs a single database select query returning result as a
* ResultIterator. It is caller's responsibility to explicitly close the
* ResultIterator. A failure to do so will result in a database connection
* not being released. Another side effect of an open ResultIterator is that
* an internal Cayenne transaction that originated in this method stays open
* until the iterator is closed. So users should normally close the iterator
* within the same thread that opened it.
* <p>
* Note that 'performIteratedQuery' always returns ResultIterator over
* DataRows. Use
* {@link #iterate(Select, org.apache.cayenne.ResultIteratorCallback)} to
* get access to objects.
*/
// TODO: deprecate once all selecting queries start implementing Select<T>
// interface
@SuppressWarnings({ "rawtypes" })
public ResultIterator performIteratedQuery(Query query) {
if (BaseTransaction.getThreadTransaction() != null) {
return internalPerformIteratedQuery(query);
} else {
// can't use TransactionManger here as it would attempt to commit the transaction at the end...
// manually manage a transaction, so that a ResultIterator wrapper
// could close it when it is done.
Transaction tx = getTransactionFactory().createTransaction();
BaseTransaction.bindThreadTransaction(tx);
ResultIterator result;
try {
result = internalPerformIteratedQuery(query);
} catch (Exception e) {
tx.setRollbackOnly();
throw new CayenneRuntimeException(e);
} finally {
// unbind thread tx before returning to the caller. Transaction will be managed internally by the
// ResultIterator and should not get in the way of other DB operations that may be performed
// within the iterator....
// TODO: there was an older comment about Ingres breaking when we unbind thread transaction
// before closing the iterator. As we have no test environment for ingres, we can't
// confirm this here...
BaseTransaction.bindThreadTransaction(null);
if (tx.isRollbackOnly()) {
try {
tx.rollback();
} catch (Exception rollbackEx) {
}
}
}
return new TransactionResultIteratorDecorator(result, tx);
}
}
/**
* Runs an iterated query in a transactional context provided by the caller.
*/
ResultIterator internalPerformIteratedQuery(Query query) {
// note that for now DataChannel API does not support cursors (aka
// ResultIterator), so we have to go directly to the DataDomain.
IteratedSelectObserver observer = new IteratedSelectObserver();
getParentDataDomain().performQueries(Collections.singletonList(query), observer);
return observer.getResultIterator();
}
/**
* Executes a query returning a generic response.
*
* @since 1.2
*/
@Override
public QueryResponse performGenericQuery(Query query) {
query = nonNullDelegate().willPerformGenericQuery(this, query);
if (query == null) {
return new GenericResponse();
}
if (this.getChannel() == null) {
throw new CayenneRuntimeException("Can't run query - parent DataChannel is not set.");
}
return onQuery(this, query);
}
/**
* Performs a single selecting query. Various query setting control the
* behavior of this method and the results returned:
* <ul>
* <li>Query caching policy defines whether the results are retrieved from
* cache or fetched from the database. Note that queries that use caching
* must have a name that is used as a caching key.</li>
* <li>Query refreshing policy controls whether to refresh existing data
* objects and ignore any cached values.</li>
* <li>Query data rows policy defines whether the result should be returned
* as DataObjects or DataRows.</li>
* </ul>
* <p>
* <i>Since 1.2 takes any Query parameter, not just GenericSelectQuery</i>
* </p>
*
* @return A list of DataObjects or a DataRows, depending on the value
* returned by {@link QueryMetadata#isFetchingDataRows()}.
*/
@Override
@SuppressWarnings("unchecked")
public List performQuery(Query query) {
query = nonNullDelegate().willPerformQuery(this, query);
if (query == null) {
return new ArrayList<>(1);
}
List result = onQuery(this, query).firstList();
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) {
return new DataContextQueryAction(this, context, query).execute();
}
/**
* Performs a single database query that does not select rows. Returns an
* array of update counts.
*
* @since 1.1
*/
public int[] performNonSelectingQuery(Query query) {
int[] count = performGenericQuery(query).firstUpdateCount();
return count != null ? count : new int[0];
}
/**
* Performs a named mapped query that does not select rows. Returns an array
* of update counts.
*
* @since 1.1
*/
public int[] performNonSelectingQuery(String queryName) {
return performNonSelectingQuery(MappedExec.query(queryName));
}
/**
* Performs a named mapped non-selecting query using a map of parameters.
* Returns an array of update counts.
*
* @since 1.1
*/
public int[] performNonSelectingQuery(String queryName, Map<String, ?> parameters) {
return performNonSelectingQuery(MappedExec.query(queryName).params(parameters));
}
/**
* Returns a list of objects or DataRows for a named query stored in one of
* the DataMaps. Internally Cayenne uses a caching policy defined in the
* named query. If refresh flag is true, a refresh is forced no matter what
* the caching policy is.
*
* @param queryName
* a name of a GenericSelectQuery defined in one of the DataMaps.
* If no such query is defined, this method will throw a
* CayenneRuntimeException.
* @param expireCachedLists
* A flag that determines whether refresh of <b>cached lists</b>
* is required in case a query uses caching.
* @since 1.1
*/
public List<?> performQuery(String queryName, boolean expireCachedLists) {
return performQuery(queryName, Collections.EMPTY_MAP, expireCachedLists);
}
/**
* Returns a list of objects or DataRows for a named query stored in one of
* the DataMaps. Internally Cayenne uses a caching policy defined in the
* named query. If refresh flag is true, a refresh is forced no matter what
* the caching policy is.
*
* @param queryName
* a name of a GenericSelectQuery defined in one of the DataMaps.
* If no such query is defined, this method will throw a
* CayenneRuntimeException.
* @param parameters
* A map of parameters to use with stored query.
* @param expireCachedLists
* A flag that determines whether refresh of <b>cached lists</b>
* is required in case a query uses caching.
* @since 1.1
*/
public List<?> performQuery(String queryName, Map parameters, boolean expireCachedLists) {
return performQuery(expireCachedLists ?
MappedSelect.query(queryName).params(parameters).forceNoCache() :
MappedSelect.query(queryName).params(parameters));
}
/**
* Returns <code>true</code> if the ObjectStore uses shared cache of a
* parent DataDomain.
*
* @since 1.1
*/
public boolean isUsingSharedSnapshotCache() {
return usingSharedSnaphsotCache;
}
/**
* @since 3.1
*/
public void setUsingSharedSnapshotCache(boolean flag) {
this.usingSharedSnaphsotCache = flag;
}
// ---------------------------------------------
// Serialization Support
// ---------------------------------------------
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Serialize local snapshots cache
if (!isUsingSharedSnapshotCache()) {
out.writeObject(objectStore.getDataRowCache());
}
}
// serialization support
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// TODO: most of this should be in the superclass, especially the code
// connecting
// super transient ivars
// read non-transient properties
in.defaultReadObject();
// deserialize local snapshots cache
if (!isUsingSharedSnapshotCache()) {
DataRowStore cache = (DataRowStore) in.readObject();
objectStore.setDataRowCache(cache);
}
// CayenneDataObjects have a transient DataContext
// because at deserialize time the datacontext may need to be different
// than the one at serialize time (for programmer defined reasons).
// So, when a DataObject is resurrected because it's DataContext was
// serialized, it will then set the objects DataContext to the correct
// one
// If deserialized "otherwise", it will not have a DataContext.
synchronized (getObjectStore()) {
Iterator<?> it = objectStore.getObjectIterator();
while (it.hasNext()) {
Persistent object = (Persistent) it.next();
object.setObjectContext(this);
}
}
// ... deferring initialization of transient properties of this context
// till first
// access, so that it can attach to Cayenne runtime using appropriate
// thread
// injector.
}
/**
* Returns this context's ObjectStore.
*
* @since 1.2
*/
@Override
public GraphManager getGraphManager() {
return objectStore;
}
/**
* An internal version of {@link #localObject(Persistent)} that operates on
* ObjectId instead of Persistent, and wouldn't attempt to look up an object
* in the parent channel.
*
* @since 3.1
*/
Persistent findOrCreateObject(ObjectId id) {
if (id == null) {
throw new IllegalArgumentException("Null ObjectId");
}
// have to synchronize almost the entire method to prevent multiple
// threads from
// messing up dataobjects per CAY-845. Originally only parts of "else"
// were
// synchronized, but we had to expand the lock scope to ensure
// consistent
// behavior.
synchronized (getGraphManager()) {
Persistent cachedObject = (Persistent) getGraphManager().getNode(id);
// return an existing object
if (cachedObject != null) {
int state = cachedObject.getPersistenceState();
// TODO: Andrus, 1/24/2006 implement smart merge for modified
// objects...
if (state != PersistenceState.MODIFIED && state != PersistenceState.DELETED) {
ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(id.getEntityName());
descriptor.injectValueHolders(cachedObject);
}
return cachedObject;
}
// create and register a hollow object
ClassDescriptor descriptor = getEntityResolver().getClassDescriptor(id.getEntityName());
Persistent localObject = (Persistent) descriptor.createObject();
localObject.setObjectContext(this);
localObject.setObjectId(id);
getGraphManager().registerNode(id, localObject);
localObject.setPersistenceState(PersistenceState.HOLLOW);
return localObject;
}
}
// this completely meaningless override is needed to expose the method as
// package-private ... is there a better way?
@Override
protected void fireDataChannelChanged(Object postedBy, GraphDiff changes) {
super.fireDataChannelChanged(postedBy, changes);
}
private TransactionFactory getTransactionFactory() {
attachToRuntimeIfNeeded();
return transactionFactory;
}
/**
* @since 4.0
* @deprecated since 4.0 avoid using this directly. Transaction management
* at this level will be eventually removed
*/
@Deprecated
public void setTransactionFactory(TransactionFactory transactionFactory) {
this.transactionFactory = transactionFactory;
}
/**
* ResultIterator that can convert DataRow to Persistent object on the fly.
*/
static class DataRowResultIterator<T> implements ResultIterator<T> {
final ResultIterator<?> rows;
ObjectResolver resolver;
DataRowResultIterator(ResultIterator<?> rows, ObjectResolver resolver) {
this.rows = rows;
this.resolver = resolver;
}
@Override
public Iterator<T> iterator() {
return new ResultIteratorIterator<>(this);
}
@Override
public List<T> allRows() {
List<T> list = new ArrayList<>();
while (hasNextRow()) {
list.add(nextRow());
}
return list;
}
@Override
public boolean hasNextRow() {
return rows.hasNextRow();
}
@Override
@SuppressWarnings("unchecked")
public T nextRow() {
DataRow row = (DataRow) rows.nextRow();
List<T> objects = (List<T>) resolver.synchronizedObjectsFromDataRows(Collections.singletonList(row));
return objects.get(0);
}
@Override
public void skipRow() {
rows.skipRow();
}
@Override
public void close() {
rows.close();
}
}
}