blob: ec45d7c0fc1c6903b080e2c21c61d96bf5c5dd9f [file] [log] [blame]
/*****************************************************************
* 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.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.query.PrefetchProcessor;
import org.apache.cayenne.query.PrefetchSelectQuery;
import org.apache.cayenne.query.PrefetchTreeNode;
import org.apache.cayenne.query.QueryMetadata;
import org.apache.cayenne.reflect.ClassDescriptor;
/**
* Processes a number of DataRow sets corresponding to a given prefetch tree, resolving
* DataRows to an object tree. Can process any combination of joint and disjoint sets, per
* prefetch tree.
*/
class HierarchicalObjectResolver {
DataContext context;
QueryMetadata queryMetadata;
DataRowStore cache;
ClassDescriptor descriptor;
boolean needToSaveDuplicates;
HierarchicalObjectResolver(DataContext context, QueryMetadata queryMetadata) {
this.queryMetadata = queryMetadata;
this.context = context;
this.cache = context.getObjectStore().getDataRowCache();
}
HierarchicalObjectResolver(DataContext context, QueryMetadata metadata,
ClassDescriptor descriptor, boolean needToSaveDuplicates) {
this(context, metadata);
this.descriptor = descriptor;
this.needToSaveDuplicates = needToSaveDuplicates;
}
/**
* Properly synchronized version of 'resolveObjectTree'.
*/
PrefetchProcessorNode synchronizedRootResultNodeFromDataRows(
PrefetchTreeNode tree,
List mainResultRows,
Map extraResultsByPath) {
synchronized (context.getObjectStore()) {
return resolveObjectTree(tree, mainResultRows, extraResultsByPath);
}
}
private PrefetchProcessorNode resolveObjectTree(
PrefetchTreeNode tree,
List mainResultRows,
Map extraResultsByPath) {
// create a copy of the tree using DecoratedPrefetchNodes and then traverse it
// resolving objects...
PrefetchProcessorNode decoratedTree = new PrefetchProcessorTreeBuilder(
this,
mainResultRows,
extraResultsByPath).buildTree(tree);
// do a single path for disjoint prefetches, joint subtrees will be processed at
// each disjoint node that is a parent of joint prefetches.
decoratedTree.traverse(new DisjointProcessor());
// connect related objects
decoratedTree.traverse(new PostProcessor());
return decoratedTree;
}
final class DisjointProcessor implements PrefetchProcessor {
public boolean startDisjointPrefetch(PrefetchTreeNode node) {
PrefetchProcessorNode processorNode = (PrefetchProcessorNode) node;
// this means something bad happened during fetch
if (processorNode.getDataRows() == null) {
return false;
}
// ... continue with processing even if the objects list is empty to handle
// multi-step prefetches.
if (processorNode.getDataRows().isEmpty()) {
return true;
}
List<Persistent> objects = processorNode.getResolver().objectsFromDataRows(processorNode.getDataRows());
processorNode.setObjects(objects);
return true;
}
public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
PrefetchProcessorNode processorNode = (PrefetchProcessorNode) node;
if (node.getParent().isPhantom()) {
// TODO: doing nothing in current implementation if parent node is phantom
return true;
}
PrefetchProcessorNode parentProcessorNode = (PrefetchProcessorNode) processorNode
.getParent();
ObjRelationship relationship = processorNode.getIncoming().getRelationship();
List<DbRelationship> dbRelationships = relationship.getDbRelationships();
DbRelationship lastDbRelationship = dbRelationships.get(0);
String pathPrefix = "";
if (dbRelationships.size() > 1) {
// we need path prefix for flattened relationships
StringBuilder buffer = new StringBuilder();
for (int i = dbRelationships.size() - 1; i >= 1; i--) {
if (buffer.length() > 0) {
buffer.append(".");
}
buffer.append(dbRelationships
.get(i)
.getReverseRelationship()
.getName());
}
pathPrefix = buffer.append(".").toString();
}
List<?> parentDataRows;
// note that a disjoint prefetch that has adjacent joint prefetches
// will be a PrefetchProcessorJointNode, so here check for
// semantics, not node type
if (parentProcessorNode.getSemantics() == PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS) {
parentDataRows = ((PrefetchProcessorJointNode) parentProcessorNode).getResolvedRows();
} else {
parentDataRows = parentProcessorNode.getDataRows();
}
int maxIdQualifierSize = context
.getParentDataDomain()
.getMaxIdQualifierSize();
List<PrefetchSelectQuery> queries = new ArrayList<PrefetchSelectQuery>();
int qualifiersCount = 0;
PrefetchSelectQuery currentQuery = null;
for (Object dataRow : parentDataRows) {
Expression allJoinsQualifier = null;
List<DbJoin> joins = lastDbRelationship.getJoins();
// handling too big qualifiers
if (currentQuery == null
|| (maxIdQualifierSize > 0 && qualifiersCount + joins.size() > maxIdQualifierSize)) {
currentQuery = new PrefetchSelectQuery(node.getPath(), relationship);
queries.add(currentQuery);
qualifiersCount = 0;
}
for (DbJoin join : joins) {
Object targetValue = ((DataRow) dataRow).get(join.getSourceName());
Expression joinQualifier = ExpressionFactory.matchDbExp(pathPrefix
+ join.getTargetName(), targetValue);
if (allJoinsQualifier == null) {
allJoinsQualifier = joinQualifier;
}
else {
allJoinsQualifier = allJoinsQualifier.andExp(joinQualifier);
}
}
currentQuery.orQualifier(allJoinsQualifier);
qualifiersCount += joins.size();
}
PrefetchTreeNode jointSubtree = node.cloneJointSubtree();
List dataRows = new ArrayList();
for (PrefetchSelectQuery query : queries) {
// need to pass the remaining tree to make joint prefetches work
if (jointSubtree.hasChildren()) {
query.setPrefetchTree(jointSubtree);
}
query.setFetchingDataRows(true);
if (relationship.isSourceIndependentFromTargetChange()) {
// setup extra result columns to be able to relate result rows to the
// parent result objects.
query.addResultPath("db:"
+ relationship.getReverseDbRelationshipPath());
}
dataRows.addAll(context.performQuery(query));
}
processorNode.setDataRows(dataRows);
return startDisjointPrefetch(node);
}
public boolean startJointPrefetch(PrefetchTreeNode node) {
// delegate processing of the top level joint prefetch to a joint processor,
// skip non-top joint nodes
if (node.getParent() != null && !node.getParent().isJointPrefetch()) {
PrefetchProcessorJointNode processorNode = (PrefetchProcessorJointNode) node;
JointProcessor subprocessor = new JointProcessor(processorNode);
PrefetchProcessorNode parent = (PrefetchProcessorNode) processorNode
.getParent();
while (parent != null && parent.isPhantom()) {
parent = (PrefetchProcessorNode) parent.getParent();
}
if (parent == null) {
return false;
}
List parentRows = parent.getDataRows();
// phantom node?
if (parentRows == null || parentRows.size() == 0) {
return false;
}
List parentObjects = parent.getObjects();
int size = parentRows.size();
for (int i = 0; i < size; i++) {
subprocessor.setCurrentFlatRow((DataRow) parentRows.get(i));
parent.setLastResolved((Persistent) parentObjects.get(i));
processorNode.traverse(subprocessor);
}
List objects = processorNode.getObjects();
cache.snapshotsUpdatedForObjects(
objects,
processorNode.getResolvedRows(),
queryMetadata.isRefreshingObjects());
}
return true;
}
public boolean startPhantomPrefetch(PrefetchTreeNode node) {
return true;
}
public boolean startUnknownPrefetch(PrefetchTreeNode node) {
throw new CayenneRuntimeException("Unknown prefetch node: " + node);
}
public void finishPrefetch(PrefetchTreeNode node) {
// now that all the children are processed, we can clear the dupes
// TODO: see TODO in ObjectResolver.relatedObjectsFromDataRows
if ((node.isDisjointPrefetch() || node.isDisjointByIdPrefetch())
&& !needToSaveDuplicates) {
PrefetchProcessorNode processorNode = (PrefetchProcessorNode) node;
if (processorNode.isJointChildren()) {
List<Persistent> objects = processorNode.getObjects();
if (objects != null && objects.size() > 1) {
Set<Persistent> seen = new HashSet<Persistent>(objects.size());
Iterator<Persistent> it = objects.iterator();
while (it.hasNext()) {
if (!seen.add(it.next())) {
it.remove();
}
}
}
}
}
}
}
// a processor of a single joint result set that walks a subtree of prefetch nodes
// that use this result set.
final class JointProcessor implements PrefetchProcessor {
DataRow currentFlatRow;
PrefetchProcessorNode rootNode;
JointProcessor(PrefetchProcessorJointNode rootNode) {
this.rootNode = rootNode;
}
void setCurrentFlatRow(DataRow currentFlatRow) {
this.currentFlatRow = currentFlatRow;
}
public boolean startDisjointPrefetch(PrefetchTreeNode node) {
// disjoint prefetch that is not the root terminates the walk...
// don't process the root node itself..
return node == rootNode;
}
public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
return startDisjointPrefetch(node);
}
public boolean startJointPrefetch(PrefetchTreeNode node) {
PrefetchProcessorJointNode processorNode = (PrefetchProcessorJointNode) node;
Persistent object = null;
// find existing object, if found skip further processing
Map id = processorNode.idFromFlatRow(currentFlatRow);
object = processorNode.getResolved(id);
DataRow row = null;
if (object == null) {
row = processorNode.rowFromFlatRow(currentFlatRow);
object = processorNode.getResolver().objectFromDataRow(row);
// LEFT OUTER JOIN produced no matches...
if (object == null) {
return false;
}
processorNode.putResolved(id, object);
processorNode.addObject(object, row);
}
// linking by parent needed even if an object is already there
// (many-to-many case)
processorNode.getParentAttachmentStrategy().linkToParent(row, object);
processorNode.setLastResolved(object);
return processorNode.isJointChildren();
}
public boolean startPhantomPrefetch(PrefetchTreeNode node) {
return ((PrefetchProcessorNode) node).isJointChildren();
}
public boolean startUnknownPrefetch(PrefetchTreeNode node) {
throw new CayenneRuntimeException("Unknown prefetch node: " + node);
}
public void finishPrefetch(PrefetchTreeNode node) {
// noop
}
}
// processor that converts temporary associations between DataObjects to Cayenne
// relationships and also fires snapshot update events
final class PostProcessor implements PrefetchProcessor {
public void finishPrefetch(PrefetchTreeNode node) {
}
public boolean startDisjointPrefetch(PrefetchTreeNode node) {
((PrefetchProcessorNode) node).connectToParents();
return true;
}
public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
return startDisjointPrefetch(node);
}
public boolean startJointPrefetch(PrefetchTreeNode node) {
PrefetchProcessorJointNode processorNode = (PrefetchProcessorJointNode) node;
if (!processorNode.getObjects().isEmpty()) {
cache.snapshotsUpdatedForObjects(
processorNode.getObjects(),
processorNode.getResolvedRows(),
queryMetadata.isRefreshingObjects());
}
// run 'connectToParents' even if the object list is empty. This is needed to
// refresh stale relationships e.g. when some related objects got deleted.
processorNode.connectToParents();
return true;
}
public boolean startPhantomPrefetch(PrefetchTreeNode node) {
return true;
}
public boolean startUnknownPrefetch(PrefetchTreeNode node) {
throw new CayenneRuntimeException("Unknown prefetch node: " + node);
}
}
}