blob: df145cb590c7b482c0c1be2404eb2f72a64b4b40 [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 java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.ResultIterator;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.query.ColumnSelect;
import org.apache.cayenne.query.EntityResultSegment;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.QueryMetadata;
import org.apache.cayenne.query.SelectQuery;
import org.apache.cayenne.util.Util;
/**
* FaultList that is used for paginated {@link ColumnSelect} queries.
* It expects data as Object[] where ids are stored instead of Persistent objects (as raw value for single PK
* or Map for compound PKs).
* Scalar values that were fetched from ColumnSelect not processed in any way,
* if there is no Persistent objects in the result Collection it will be iterated as is, without faulting anything.
*
* @see QueryMetadata#getPageSize()
* @see org.apache.cayenne.query.SelectQueryMetadata
*
* @since 4.0
*/
class MixedResultIncrementalFaultList<E> extends IncrementalFaultList<E> {
/**
* Cached positions for entity results in elements array
*/
private Map<Integer, ObjEntity> indexToEntity;
/**
* Whether result contains only scalars
*/
private boolean scalarResult;
/**
* Creates a new IncrementalFaultList using a given DataContext and query.
*
* @param dataContext DataContext used by IncrementalFaultList to fill itself with
* objects.
* @param query Main query used to retrieve data. Must have "pageSize"
* property set to a value greater than zero.
*/
MixedResultIncrementalFaultList(DataContext dataContext, Query query, int maxFetchSize) {
super(dataContext, query, maxFetchSize);
// this should generally be true, and may be it worth to do something if it's not
if(query instanceof ColumnSelect) {
this.internalQuery.setColumns(((ColumnSelect<?>) query).getColumns());
}
}
@Override
IncrementalListHelper createHelper(QueryMetadata metadata) {
// first compile some meta data about results
indexToEntity = new HashMap<>();
scalarResult = true;
for(Object next : metadata.getResultSetMapping()) {
if(next instanceof EntityResultSegment) {
EntityResultSegment resultSegment = (EntityResultSegment)next;
ObjEntity entity = resultSegment.getClassDescriptor().getEntity();
// store entity's PK position in result
indexToEntity.put(resultSegment.getColumnOffset(), entity);
scalarResult = false;
}
}
// if there is no entities in this results,
// than all data is already there and we don't need to resolve any objects
if(indexToEntity.isEmpty()) {
return new ScalarArrayListHelper();
} else {
return new MixedArrayListHelper();
}
}
@Override
protected void fillIn(final Query query, List<Object> elementsList) {
elementsList.clear();
try (ResultIterator it = dataContext.performIteratedQuery(query)) {
while (it.hasNextRow()) {
elementsList.add(it.nextRow());
}
}
unfetchedObjects = elementsList.size();
}
@Override
protected void resolveInterval(int fromIndex, int toIndex) {
if (fromIndex >= toIndex || scalarResult) {
return;
}
synchronized (elements) {
if (elements.size() == 0) {
return;
}
// perform bound checking
if (fromIndex < 0) {
fromIndex = 0;
}
if (toIndex > elements.size()) {
toIndex = elements.size();
}
for(Map.Entry<Integer, ObjEntity> entry : indexToEntity.entrySet()) {
List<Expression> quals = new ArrayList<>(pageSize);
int dataIdx = entry.getKey();
for (int i = fromIndex; i < toIndex; i++) {
Object[] object = (Object[])elements.get(i);
if (getHelper().unresolvedSuspect(object[dataIdx])) {
Expression nextQualifier = buildIdQualifier(dataIdx, object);
if(nextQualifier != null) {
quals.add(nextQualifier);
}
}
}
int qualsSize = quals.size();
if (qualsSize == 0) {
continue;
}
// fetch the range of objects in fetchSize chunks
List<Persistent> objects = new ArrayList<>(qualsSize);
int fetchSize = maxFetchSize > 0 ? maxFetchSize : Integer.MAX_VALUE;
int fetchEnd = Math.min(qualsSize, fetchSize);
int fetchBegin = 0;
while (fetchBegin < qualsSize) {
SelectQuery<Persistent> query = createSelectQuery(entry.getValue(), quals.subList(fetchBegin, fetchEnd));
objects.addAll(dataContext.performQuery(query));
fetchBegin = fetchEnd;
fetchEnd += Math.min(fetchSize, qualsSize - fetchEnd);
}
// replace ids in the list with objects
updatePageWithResults(objects, dataIdx);
}
}
}
void updatePageWithResults(List<Persistent> objects, int dataIndex) {
MixedArrayListHelper helper = (MixedArrayListHelper)getHelper();
for (Persistent object : objects) {
helper.updateWithResolvedObject(object, dataIndex);
}
}
SelectQuery<Persistent> createSelectQuery(ObjEntity entity, List<Expression> expressions) {
SelectQuery<Persistent> query = new SelectQuery<>(entity, ExpressionFactory.joinExp(Expression.OR, expressions));
if (entity.equals(rootEntity)) {
query.setPrefetchTree(internalQuery.getPrefetchTree());
}
return query;
}
Expression buildIdQualifier(int index, Object[] data) {
Map<String, Object> map;
if(data[index] instanceof Map) {
map = (Map<String, Object>) data[index];
} else if(data[index] == null) {
return null;
} else {
map = new HashMap<>();
int i = 0;
for (ObjAttribute attribute : indexToEntity.get(index).getPrimaryKeys()) {
map.put(attribute.getDbAttributeName(), data[index + i++]);
}
}
return ExpressionFactory.matchAllDbExp(map, Expression.EQUAL_TO);
}
/**
* Helper that operates on Object[] and checks for Persistent objects' presence in it.
*/
class MixedArrayListHelper extends IncrementalListHelper {
@Override
boolean unresolvedSuspect(Object object) {
return !(object instanceof Persistent);
}
@Override
boolean objectsAreEqual(Object object, Object objectInTheList) {
if(!(object instanceof Object[])){
return false;
}
return Arrays.equals((Object[])object, (Object[])objectInTheList);
}
@Override
boolean replacesObject(Object object, Object objectInTheList) {
throw new UnsupportedOperationException();
}
boolean replacesObject(Persistent object, Object[] dataInTheList, int dataIdx) {
Map<?, ?> map = object.getObjectId().getIdSnapshot();
if(dataInTheList[dataIdx] instanceof Map) {
Map<?, ?> id = (Map<?, ?>) dataInTheList[dataIdx];
if (id.size() != map.size()) {
return false;
}
for (Map.Entry<?, ?> entry : id.entrySet()) {
if (!Util.nullSafeEquals(entry.getValue(), map.get(entry.getKey()))) {
return false;
}
}
} else if(dataInTheList[dataIdx] == null) {
return false;
} else {
for(Object id : map.values()) {
if (!dataInTheList[dataIdx++].equals(id)) {
return false;
}
}
}
return true;
}
void updateWithResolvedObject(Persistent object, int dataIdx) {
synchronized (elements) {
for (Object element : elements) {
Object[] data = (Object[]) element;
if (replacesObject(object, data, dataIdx)) {
data[dataIdx] = object;
}
}
}
}
}
/**
* Helper that actually does nothing
*/
class ScalarArrayListHelper extends IncrementalListHelper {
@Override
boolean unresolvedSuspect(Object object) {
return false;
}
@Override
boolean objectsAreEqual(Object object, Object objectInTheList) {
return objectInTheList.equals(object);
}
@Override
boolean replacesObject(Object object, Object objectInTheList) {
return false;
}
}
}