blob: 9d72ddcf33eb867d9e776c47c41f308c6b0493f3 [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
*
* 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.DataRow;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.access.jdbc.ColumnDescriptor;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.parser.ASTPath;
import org.apache.cayenne.map.DbAttribute;
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.query.PrefetchTreeNode;
import org.apache.cayenne.reflect.ArcProperty;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* A specialized PrefetchTreeNode used for joint prefetch resolving.
*
* @since 1.2
*/
class PrefetchProcessorJointNode extends PrefetchProcessorNode {
ColumnDescriptor[] columns;
int[] idIndices;
int rowCapacity;
Map<Map, Persistent> resolved;
List<DataRow> resolvedRows;
PrefetchProcessorJointNode(PrefetchProcessorNode parent, String segmentPath) {
super(parent, segmentPath);
}
@Override
void afterInit() {
super.afterInit();
// as node will be resolved one row at a time, init objects array here
// list may shrink as a result of duplicates in flattened rows.. so don't
// allocate too much space
int capacity = dataRows != null ? dataRows.size() : 10;
if (capacity > 100) {
capacity = capacity / 2;
}
objects = new ArrayList<>(capacity);
resolved = new HashMap<>(capacity);
resolvedRows = new ArrayList<>(capacity);
buildRowMapping();
buildPKIndex();
}
List<DataRow> getResolvedRows() {
return resolvedRows;
}
void addObject(Persistent object, DataRow row) {
objects.add(object);
resolvedRows.add(row);
}
/**
* Returns an ObjectId map from the flat row.
*/
Map<String, Object> idFromFlatRow(DataRow flatRow) {
// TODO: should we also check for nulls in ID (and skip such rows) - this will
// likely be an indicator of an outer join ... and considering SQLTemplate,
// this is reasonable to expect...
Map<String, Object> id = new TreeMap<>();
for (int idIndex : idIndices) {
Object value = flatRow.get(columns[idIndex].getDataRowKey());
id.put(columns[idIndex].getName(), value);
}
return id;
}
/**
* Looks up a previously resolved object using an ObjectId map as a key. Returns null
* if no matching object exists.
*/
Persistent getResolved(Map id) {
return resolved.get(id);
}
/**
* Registers an object in a map of resolved objects, connects this object to parent if
* parent exists.
*/
void putResolved(Map id, Persistent object) {
resolved.put(id, object);
}
/**
* Returns a DataRow from the flat row.
*/
DataRow rowFromFlatRow(DataRow flatRow) {
DataRow row = new DataRow(rowCapacity);
// extract subset of flat row columns, recasting to the target keys
for (ColumnDescriptor column : columns) {
row.put(column.getName(), flatRow.get(column.getDataRowKey()));
}
// since JDBC row reader won't inject JOINED entity name, we have to detect it here...
ClassDescriptor descriptor = resolver.getDescriptor();
ObjEntity entity = descriptor.getEntityInheritanceTree().entityMatchingRow(row);
row.setEntityName(entity == null ? null : entity.getName());
return row;
}
/**
* Configures row columns mapping for this node entity.
*/
private void buildRowMapping() {
final Map<String, ColumnDescriptor> targetSource = new TreeMap<>();
// build a DB path .. find parent node that terminates the joint group...
PrefetchTreeNode jointRoot = this;
while (jointRoot.getParent() != null && !jointRoot.isDisjointPrefetch()
&& !jointRoot.isDisjointByIdPrefetch()) {
jointRoot = jointRoot.getParent();
}
final String prefix;
if (jointRoot != this) {
Expression objectPath = ExpressionFactory.exp(getPath(jointRoot));
ASTPath translated = (ASTPath) ((PrefetchProcessorNode) jointRoot)
.getResolver()
.getEntity()
.translateToDbPath(objectPath);
// make sure we do not include "db:" prefix
prefix = translated.getOperand(0) + ".";
} else {
prefix = "";
}
// find propagated keys, assuming that only one-step joins
// share their column(s) with parent
if (getParent() != null
&& !getParent().isPhantom()
&& getIncoming() != null
&& !getIncoming().getRelationship().isFlattened()) {
DbRelationship r = getIncoming()
.getRelationship()
.getDbRelationships()
.get(0);
for (final DbJoin join : r.getJoins()) {
appendColumn(targetSource, join.getTargetName(), prefix
+ join.getTargetName());
}
}
ClassDescriptor descriptor = resolver.getDescriptor();
descriptor.visitAllProperties(new PropertyVisitor() {
public boolean visitAttribute(AttributeProperty property) {
String target = property.getAttribute().getDbAttributePath();
if(!property.getAttribute().isLazy()) {
appendColumn(targetSource, target, prefix + target);
}
return true;
}
public boolean visitToMany(ToManyProperty property) {
return visitRelationship(property);
}
public boolean visitToOne(ToOneProperty property) {
return visitRelationship(property);
}
private boolean visitRelationship(ArcProperty arc) {
DbRelationship dbRel = arc.getRelationship().getDbRelationships().get(0);
for (DbAttribute attribute : dbRel.getSourceAttributes()) {
String target = attribute.getName();
appendColumn(targetSource, target, prefix + target);
}
return true;
}
});
// append id columns ... (some may have been appended already via relationships)
for (String pkName : descriptor.getEntity().getPrimaryKeyNames()) {
appendColumn(targetSource, pkName, prefix + pkName);
}
// append inheritance discriminator columns...
for (ObjAttribute column : descriptor.getDiscriminatorColumns()) {
String target = column.getDbAttributePath();
appendColumn(targetSource, target, prefix + target);
}
int size = targetSource.size();
this.rowCapacity = (int) Math.ceil(size / 0.75);
this.columns = new ColumnDescriptor[size];
targetSource.values().toArray(columns);
}
private ColumnDescriptor appendColumn(
Map<String, ColumnDescriptor> map,
String name,
String label) {
ColumnDescriptor column = map.get(name);
if (column == null) {
column = new ColumnDescriptor();
column.setName(name);
column.setDataRowKey(label);
map.put(name, column);
}
return column;
}
/**
* Creates an internal index of PK columns in the result.
*/
private void buildPKIndex() {
// index PK
Collection<DbAttribute> pks = getResolver()
.getEntity()
.getDbEntity()
.getPrimaryKeys();
this.idIndices = new int[pks.size()];
// this is needed for checking that a valid index is made
Arrays.fill(idIndices, -1);
Iterator<DbAttribute> it = pks.iterator();
for (int i = 0; i < idIndices.length; i++) {
DbAttribute pk = it.next();
for (int j = 0; j < columns.length; j++) {
if (pk.getName().equals(columns[j].getName())) {
idIndices[i] = j;
break;
}
}
// sanity check
if (idIndices[i] == -1) {
throw new CayenneRuntimeException("PK column is not part of result row: %s", pk.getName());
}
}
}
}