CAY-2255 ObjectSelect: columns as full entities CAY-2271 ColumnSelect: support for prefetch and limit
Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/e098f236 Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/e098f236 Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/e098f236 Branch: refs/heads/master Commit: e098f236032ee8cdd695993f7b71d1b44388318f Parents: 05a7725 Author: Nikita Timofeev <[email protected]> Authored: Tue Mar 21 11:58:35 2017 +0300 Committer: Nikita Timofeev <[email protected]> Committed: Tue Mar 21 11:58:35 2017 +0300 ---------------------------------------------------------------------- .../cayenne/access/DataContextQueryAction.java | 38 +- .../cayenne/access/DataDomainQueryAction.java | 13 +- .../cayenne/access/IncrementalFaultList.java | 45 +- .../access/MixedResultIncrementalFaultList.java | 279 ++++++++++ .../jdbc/reader/DefaultRowReaderFactory.java | 4 +- .../cayenne/access/jdbc/reader/IdRowReader.java | 13 +- .../select/DefaultSelectTranslator.java | 131 ++++- .../translator/select/QualifierTranslator.java | 8 +- .../translator/select/QueryAssemblerHelper.java | 36 +- .../java/org/apache/cayenne/exp/Expression.java | 5 + .../apache/cayenne/exp/ExpressionFactory.java | 9 + .../java/org/apache/cayenne/exp/Property.java | 42 ++ .../cayenne/exp/parser/ASTFullObject.java | 63 +++ .../org/apache/cayenne/query/ObjectSelect.java | 4 +- .../cayenne/query/SelectQueryMetadata.java | 180 ++++++- .../org/apache/cayenne/CayenneCompoundIT.java | 24 + .../apache/cayenne/query/ColumnSelectIT.java | 508 ++++++++++++++++++- .../cayenne/query/ObjectSelect_RunIT.java | 141 ----- docs/doc/src/main/resources/RELEASE-NOTES.txt | 2 + 19 files changed, 1311 insertions(+), 234 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java index 948671f..0ef2c0c 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextQueryAction.java @@ -28,6 +28,7 @@ import org.apache.cayenne.ObjectContext; import org.apache.cayenne.PersistenceState; import org.apache.cayenne.Persistent; import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.query.EntityResultSegment; import org.apache.cayenne.query.ObjectIdQuery; import org.apache.cayenne.query.Query; import org.apache.cayenne.query.RefreshQuery; @@ -75,8 +76,7 @@ class DataContextQueryAction extends ObjectContextQueryAction { ObjectIdQuery oidQuery = (ObjectIdQuery) query; if (!oidQuery.isFetchMandatory()) { - Object object = polymorphicObjectFromCache( - oidQuery.getObjectId()); + Object object = polymorphicObjectFromCache(oidQuery.getObjectId()); if (object != null) { // TODO: andrus, 10/14/2006 - obtaining a row from an object is the @@ -104,23 +104,27 @@ class DataContextQueryAction extends ObjectContextQueryAction { @Override protected boolean interceptPaginatedQuery() { if (metadata.getPageSize() > 0) { - - DbEntity dbEntity = metadata.getDbEntity(); - Integer maxIdQualifierSize = actingDataContext - .getParentDataDomain() - .getMaxIdQualifierSize(); + Integer maxIdQualifierSize = actingDataContext.getParentDataDomain().getMaxIdQualifierSize(); List<?> paginatedList; - if (dbEntity != null && dbEntity.getPrimaryKeys().size() == 1) { - paginatedList = new SimpleIdIncrementalFaultList<Object>( - actingDataContext, - query, - maxIdQualifierSize); + List<Object> rsMapping = metadata.getResultSetMapping(); + boolean mixedResults = false; + if(rsMapping != null) { + if(rsMapping.size() > 1) { + mixedResults = true; + } else if(rsMapping.size() == 1) { + mixedResults = !(rsMapping.get(0) instanceof EntityResultSegment); + } } - else { - paginatedList = new IncrementalFaultList<Object>( - actingDataContext, - query, - maxIdQualifierSize); + + if(mixedResults) { + paginatedList = new MixedResultIncrementalFaultList<>(actingDataContext, query, maxIdQualifierSize); + } else { + DbEntity dbEntity = metadata.getDbEntity(); + if (dbEntity != null && dbEntity.getPrimaryKeys().size() == 1) { + paginatedList = new SimpleIdIncrementalFaultList<Object>(actingDataContext, query, maxIdQualifierSize); + } else { + paginatedList = new IncrementalFaultList<Object>(actingDataContext, query, maxIdQualifierSize); + } } response = new ListResponse(paginatedList); http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java index 933d616..6e9b9f8 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataDomainQueryAction.java @@ -672,9 +672,18 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver { @Override void convert(List<DataRow> mainRows) { - ClassDescriptor descriptor = metadata.getClassDescriptor(); PrefetchTreeNode prefetchTree = metadata.getPrefetchTree(); + List<Object> rsMapping = metadata.getResultSetMapping(); + EntityResultSegment resultSegment = null; + if(rsMapping != null && !rsMapping.isEmpty()) { + resultSegment = (EntityResultSegment)rsMapping.get(0); + } + + ClassDescriptor descriptor = resultSegment == null + ? metadata.getClassDescriptor() + : resultSegment.getClassDescriptor(); + PrefetchProcessorNode node = toResultsTree(descriptor, prefetchTree, mainRows); List<Persistent> objects = node.getObjects(); updateResponse(mainRows, objects != null ? objects : new ArrayList<>(1)); @@ -714,7 +723,7 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver { prefetchTreeNode = new PrefetchTreeNode(); } PrefetchTreeNode addPath = prefetchTreeNode.addPath(prefetch.getPath()); - addPath.setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS); + addPath.setSemantics(prefetch.getSemantics()); addPath.setPhantom(false); } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java b/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java index 1e13ea8..bdfce69 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/IncrementalFaultList.java @@ -59,7 +59,7 @@ import java.util.NoSuchElementException; public class IncrementalFaultList<E> implements List<E>, Serializable { protected int pageSize; - protected List elements; + protected final List elements; protected DataContext dataContext; protected ObjEntity rootEntity; protected SelectQuery<?> internalQuery; @@ -71,7 +71,7 @@ public class IncrementalFaultList<E> implements List<E>, Serializable { */ protected int idWidth; - private IncrementalListHelper helper; + IncrementalListHelper helper; /** * Defines the upper limit on the size of fetches. This is needed to avoid @@ -156,12 +156,11 @@ public class IncrementalFaultList<E> implements List<E>, Serializable { * * @since 3.0 */ - protected void fillIn(final Query query, List elementsList) { + protected void fillIn(final Query query, List<Object> elementsList) { elementsList.clear(); - try (ResultIterator it = dataContext.performIteratedQuery(query);) { - + try (ResultIterator it = dataContext.performIteratedQuery(query)) { while (it.hasNextRow()) { elementsList.add(it.nextRow()); } @@ -236,7 +235,6 @@ public class IncrementalFaultList<E> implements List<E>, Serializable { } // fetch the range of objects in fetchSize chunks - boolean fetchesDataRows = internalQuery.isFetchingDataRows(); List<Object> objects = new ArrayList<>(qualsSize); int fetchSize = maxFetchSize > 0 ? maxFetchSize : Integer.MAX_VALUE; @@ -244,15 +242,7 @@ public class IncrementalFaultList<E> implements List<E>, Serializable { int fetchEnd = Math.min(qualsSize, fetchSize); int fetchBegin = 0; while (fetchBegin < qualsSize) { - SelectQuery<Object> query = new SelectQuery<>(rootEntity, ExpressionFactory.joinExp( - Expression.OR, quals.subList(fetchBegin, fetchEnd))); - - query.setFetchingDataRows(fetchesDataRows); - - if (!query.isFetchingDataRows()) { - query.setPrefetchTree(internalQuery.getPrefetchTree()); - } - + SelectQuery<Object> query = createSelectQuery(quals.subList(fetchBegin, fetchEnd)); objects.addAll(dataContext.performQuery(query)); fetchBegin = fetchEnd; fetchEnd += Math.min(fetchSize, qualsSize - fetchEnd); @@ -262,13 +252,28 @@ public class IncrementalFaultList<E> implements List<E>, Serializable { checkPageResultConsistency(objects, ids); // replace ids in the list with objects - Iterator it = objects.iterator(); - while (it.hasNext()) { - helper.updateWithResolvedObjectInRange(it.next(), fromIndex, toIndex); - } + updatePageWithResults(objects, fromIndex, toIndex); + } + } - unfetchedObjects -= objects.size(); + void updatePageWithResults(List<Object> objects, int fromIndex, int toIndex) { + for (Object object : objects) { + helper.updateWithResolvedObjectInRange(object, fromIndex, toIndex); } + + unfetchedObjects -= objects.size(); + } + + SelectQuery<Object> createSelectQuery(List<Expression> expressions) { + SelectQuery<Object> query = new SelectQuery<>(rootEntity, + ExpressionFactory.joinExp(Expression.OR, expressions)); + + query.setFetchingDataRows(internalQuery.isFetchingDataRows()); + if (!query.isFetchingDataRows()) { + query.setPrefetchTree(internalQuery.getPrefetchTree()); + } + + return query; } /** http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java b/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java new file mode 100644 index 0000000..9ca2136 --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/MixedResultIncrementalFaultList.java @@ -0,0 +1,279 @@ +/***************************************************************** + * 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.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.access.translator.select.DefaultSelectTranslator + * @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 (helper.unresolvedSuspect(object[dataIdx])) { + quals.add(buildIdQualifier(dataIdx, object)); + } + } + + 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)this.helper; + 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 { + 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 { + 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; + } + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java index b9cb766..73a29e7 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java @@ -96,7 +96,7 @@ public class DefaultRowReaderFactory implements RowReaderFactory { EntityResultSegment resultMetadata, PostprocessorFactory postProcessorFactory) { if (queryMetadata.getPageSize() > 0) { - return new IdRowReader<Object>(descriptor, queryMetadata, postProcessorFactory.get()); + return new IdRowReader<Object>(descriptor, queryMetadata, resultMetadata, postProcessorFactory.get()); } else if (resultMetadata.getClassDescriptor() != null && resultMetadata.getClassDescriptor().hasSubclasses()) { return new InheritanceAwareEntityRowReader(descriptor, resultMetadata, postProcessorFactory.get()); } else { @@ -108,7 +108,7 @@ public class DefaultRowReaderFactory implements RowReaderFactory { PostprocessorFactory postProcessorFactory) { if (queryMetadata.getPageSize() > 0) { - return new IdRowReader<Object>(descriptor, queryMetadata, postProcessorFactory.get()); + return new IdRowReader<Object>(descriptor, queryMetadata, null, postProcessorFactory.get()); } else if (queryMetadata.getClassDescriptor() != null && queryMetadata.getClassDescriptor().hasSubclasses()) { return new InheritanceAwareRowReader(descriptor, queryMetadata, postProcessorFactory.get()); } else { http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java index a108dd7..7ad539d 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java @@ -26,6 +26,7 @@ import org.apache.cayenne.access.jdbc.ColumnDescriptor; import org.apache.cayenne.access.jdbc.RowDescriptor; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.query.EntityResultSegment; import org.apache.cayenne.query.QueryMetadata; import org.apache.cayenne.util.Util; @@ -36,10 +37,12 @@ class IdRowReader<T> extends BaseRowReader<T> { protected int[] pkIndices; - public IdRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, DataRowPostProcessor postProcessor) { + public IdRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, EntityResultSegment resultMetadata, DataRowPostProcessor postProcessor) { super(descriptor, queryMetadata, postProcessor); - DbEntity dbEntity = queryMetadata.getDbEntity(); + DbEntity dbEntity = resultMetadata == null + ? queryMetadata.getDbEntity() + : resultMetadata.getClassDescriptor().getEntity().getDbEntity(); if (dbEntity == null) { throw new CayenneRuntimeException("Null root DbEntity, can't index PK"); } @@ -99,13 +102,9 @@ class IdRowReader<T> extends BaseRowReader<T> { DataRow idRow = new DataRow(2); idRow.setEntityName(entityName); - int len = pkIndices.length; - - for (int i = 0; i < len; i++) { + for (int index : pkIndices) { // dereference column index - int index = pkIndices[i]; - // note: jdbc column indexes start from 1, not 0 as in arrays Object val = converters[index].materializeObject(resultSet, index + 1, types[index]); idRow.put(labels[index], val); http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java index affbe7a..42ac0b8 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java @@ -19,6 +19,7 @@ package org.apache.cayenne.access.translator.select; import org.apache.cayenne.CayenneRuntimeException; +import org.apache.cayenne.Persistent; import org.apache.cayenne.access.jdbc.ColumnDescriptor; import org.apache.cayenne.access.translator.DbAttributeBinding; import org.apache.cayenne.dba.DbAdapter; @@ -104,6 +105,11 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra boolean haveAggregate; Map<ColumnDescriptor, List<DbAttributeBinding>> groupByColumns; + /** + * Callback for joins creation + */ + AddJoinListener joinListener; + public DefaultSelectTranslator(Query query, DbAdapter adapter, EntityResolver entityResolver) { super(query, adapter, entityResolver); @@ -278,14 +284,22 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra * @since 4.0 */ protected void appendGroupByColumn(StringBuilder buffer, Map.Entry<ColumnDescriptor, List<DbAttributeBinding>> entry) { + String fullName; + if(entry.getKey().isExpression()) { + fullName = entry.getKey().getDataRowKey(); + } else { + QuotingStrategy strategy = getAdapter().getQuotingStrategy(); + fullName = strategy.quotedIdentifier(queryMetadata.getDataMap(), + entry.getKey().getNamePrefix(), entry.getKey().getName()); + } + + buffer.append(fullName); + if(entry.getKey().getDataRowKey().equals(entry.getKey().getName())) { - buffer.append(entry.getKey().getName()); - for (DbAttributeBinding binding : entry.getValue()) { - addToParamList(binding.getAttribute(), binding.getValue()); - } - } else { - buffer.append(entry.getKey().getDataRowKey()); - } + for (DbAttributeBinding binding : entry.getValue()) { + addToParamList(binding.getAttribute(), binding.getValue()); + } + } } /** @@ -360,9 +374,9 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra } else if (query.getRoot() instanceof DbEntity) { appendDbEntityColumns(columns, query); } else if (getQueryMetadata().getPageSize() > 0) { - appendIdColumns(columns, query); + appendIdColumns(columns, queryMetadata.getClassDescriptor().getEntity()); } else { - appendQueryColumns(columns, query); + appendQueryColumns(columns, query, queryMetadata.getClassDescriptor(), null); } return columns; @@ -376,12 +390,60 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra QualifierTranslator qualifierTranslator = adapter.getQualifierTranslator(this); AccumulatingBindingListener bindingListener = new AccumulatingBindingListener(); + final String[] joinTableAliasForProperty = {null}; + joinListener = new AddJoinListener() { + @Override + public void joinAdded() { + // capture last alias for joined table, will use it to resolve object columns + joinTableAliasForProperty[0] = getCurrentAlias(); + } + }; setAddBindingListener(bindingListener); for(Property<?> property : query.getColumns()) { + int expressionType = property.getExpression().getType(); + boolean objectProperty = expressionType == Expression.FULL_OBJECT; + // evaluate ObjPath with Persistent type as toOne relations and use it as full object + if(Persistent.class.isAssignableFrom(property.getType())) { + if(expressionType == Expression.OBJ_PATH) { + objectProperty = true; + } else { + // should we warn or throw an error? + } + } + + // forbid direct selection of toMany relationships columns + if((expressionType == Expression.OBJ_PATH || expressionType == Expression.DB_PATH) && + (Collection.class.isAssignableFrom(property.getType()) || + Map.class.isAssignableFrom(property.getType()))) { + throw new CayenneRuntimeException("Can't directly select toMany relationship columns. " + + "Either select it with aggregate functions like count() or with flat() function to select full related objects."); + } + + // Qualifier Translator in case of Object Columns have side effect - + // it will create required joins, that we catch with listener above. + // And we force created join alias for all columns of Object we select. qualifierTranslator.setQualifier(property.getExpression()); + qualifierTranslator.setForceJoinForRelations(objectProperty); StringBuilder builder = qualifierTranslator.appendPart(new StringBuilder()); + // If we want full object, use appendQueryColumns method, to fully process class descriptor + if(objectProperty) { + List<ColumnDescriptor> classColumns = new ArrayList<>(); + ObjEntity entity = entityResolver.getObjEntity(property.getType()); + if(getQueryMetadata().getPageSize() > 0) { + appendIdColumns(classColumns, entity); + } else { + ClassDescriptor classDescriptor = entityResolver.getClassDescriptor(entity.getName()); + appendQueryColumns(classColumns, query, classDescriptor, joinTableAliasForProperty[0]); + } + for(ColumnDescriptor descriptor : classColumns) { + columns.add(descriptor); + groupByColumns.put(descriptor, Collections.<DbAttributeBinding>emptyList()); + } + continue; + } + int type = TypesMapping.getSqlTypeByJava(property.getType()); String alias = property.getAlias(); @@ -402,6 +464,8 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra } setAddBindingListener(null); + qualifierTranslator.setForceJoinForRelations(false); + joinListener = null; if(!haveAggregate) { // if no expression with aggregation function found, we don't need this information @@ -442,9 +506,9 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra * Appends columns needed for object SelectQuery to the provided columns * list. */ - <T> List<ColumnDescriptor> appendQueryColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query) { + <T> List<ColumnDescriptor> appendQueryColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query, ClassDescriptor descriptor, final String tableAlias) { - final Set<ColumnTracker> attributes = new HashSet<ColumnTracker>(); + final Set<ColumnTracker> attributes = new HashSet<>(); // fetched attributes include attributes that are either: // @@ -452,8 +516,6 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra // * PK // * FK used in relationship // * joined prefetch PK - - ClassDescriptor descriptor = queryMetadata.getClassDescriptor(); ObjEntity oe = descriptor.getEntity(); PropertyVisitor visitor = new PropertyVisitor() { @@ -474,7 +536,7 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra } else if (pathPart instanceof DbAttribute) { DbAttribute dbAttr = (DbAttribute) pathPart; - appendColumn(columns, oa, dbAttr, attributes, null); + appendColumn(columns, oa, dbAttr, attributes, null, tableAlias); } } return true; @@ -499,7 +561,7 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra List<DbJoin> joins = dbRel.getJoins(); for (DbJoin join : joins) { DbAttribute src = join.getSource(); - appendColumn(columns, null, src, attributes, null); + appendColumn(columns, null, src, attributes, null, tableAlias); } } }; @@ -511,9 +573,9 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra resetJoinStack(); // add remaining needed attrs from DbEntity - DbEntity table = getQueryMetadata().getDbEntity(); + DbEntity table = oe.getDbEntity(); for (DbAttribute dba : table.getPrimaryKeys()) { - appendColumn(columns, null, dba, attributes, null); + appendColumn(columns, null, dba, attributes, null, tableAlias); } // special handling of a disjoint query... @@ -571,6 +633,12 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra // handle joint prefetches directly attached to this query... if (query.getPrefetchTree() != null) { + // Set entity name, in case MixedConversionStrategy will be used to select objects from this query + // Note: all prefetch nodes will point to query root, it is not a problem until select query can't + // perform some sort of union or sub-queries. + for(PrefetchTreeNode prefetch : query.getPrefetchTree().getChildren()) { + prefetch.setEntityName(oe.getName()); + } for (PrefetchTreeNode prefetch : query.getPrefetchTree().adjacentJointNodes()) { @@ -633,14 +701,12 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra return columns; } - <T> List<ColumnDescriptor> appendIdColumns(final List<ColumnDescriptor> columns, SelectQuery<T> query) { + <T> List<ColumnDescriptor> appendIdColumns(final List<ColumnDescriptor> columns, ObjEntity objEntity) { - Set<ColumnTracker> skipSet = new HashSet<ColumnTracker>(); + Set<ColumnTracker> skipSet = new HashSet<>(); - ClassDescriptor descriptor = queryMetadata.getClassDescriptor(); - ObjEntity oe = descriptor.getEntity(); - DbEntity dbEntity = oe.getDbEntity(); - for (ObjAttribute attribute : oe.getPrimaryKeys()) { + DbEntity dbEntity = objEntity.getDbEntity(); + for (ObjAttribute attribute : objEntity.getPrimaryKeys()) { // synthetic objattributes can't reliably lookup their DbAttribute, // so do it manually.. @@ -652,9 +718,17 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra } private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute objAttribute, DbAttribute attribute, - Set<ColumnTracker> skipSet, String label) { + Set<ColumnTracker> skipSet, String label) { + appendColumn(columns, objAttribute, attribute, skipSet, label, null); + } + + private void appendColumn(List<ColumnDescriptor> columns, ObjAttribute objAttribute, DbAttribute attribute, + Set<ColumnTracker> skipSet, String label, String alias) { + + if(alias == null) { + alias = getCurrentAlias(); + } - String alias = getCurrentAlias(); if (skipSet.add(new ColumnTracker(alias, attribute))) { ColumnDescriptor column = (objAttribute != null) ? new ColumnDescriptor(objAttribute, attribute, alias) @@ -712,6 +786,9 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra } getJoinStack().pushJoin(relationship, joinType, joinSplitAlias); + if(joinListener != null) { + joinListener.joinAdded(); + } } /** @@ -788,4 +865,8 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra return new ArrayList<>(bindings); } } + + interface AddJoinListener { + void joinAdded(); + } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java index deb7d52..51ebf52 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java @@ -142,9 +142,7 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers } } - /** - * Attaching root Db entity's qualifier - */ + // Attaching root Db entity's qualifier if (getDbEntity() != null) { Expression dbQualifier = getDbEntity().getQualifier(); if (dbQualifier != null) { @@ -425,6 +423,10 @@ public class QualifierTranslator extends QueryAssemblerHelper implements Travers return; } + if(node.getType() == Expression.FULL_OBJECT && parentNode != null) { + throw new CayenneRuntimeException("Expression is not supported in where clause."); + } + int count = node.getOperandCount(); if (count == 2) { http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java index decdaa7..2fc6078 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QueryAssemblerHelper.java @@ -46,6 +46,12 @@ public abstract class QueryAssemblerHelper { protected QuotingStrategy strategy; /** + * Force joining tables for all relations, not only for toMany + * @since 4.0 + */ + private boolean forceJoinForRelations; + + /** * Creates QueryAssemblerHelper initializing with parent * {@link QueryAssembler} and output buffer object. */ @@ -444,7 +450,7 @@ public abstract class QueryAssemblerHelper { */ protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) { - if (rel.isToMany()) { + if (forceJoinForRelations || rel.isToMany()) { // append joins queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias); } @@ -452,33 +458,39 @@ public abstract class QueryAssemblerHelper { // get last DbRelationship on the list List<DbJoin> joins = rel.getJoins(); if (joins.size() != 1) { - StringBuilder msg = new StringBuilder(); - msg.append("OBJ_PATH expressions are only supported ").append("for a single-join relationships. ") - .append("This relationship has ").append(joins.size()).append(" joins."); + String msg = "OBJ_PATH expressions are only supported for a single-join relationships. " + + "This relationship has " + joins.size() + " joins."; - throw new CayenneRuntimeException(msg.toString()); + throw new CayenneRuntimeException(msg); } DbJoin join = joins.get(0); - DbAttribute attribute = null; + DbAttribute attribute; if (rel.isToMany()) { - DbEntity ent = (DbEntity) join.getRelationship().getTargetEntity(); + DbEntity ent = join.getRelationship().getTargetEntity(); Collection<DbAttribute> pk = ent.getPrimaryKeys(); if (pk.size() != 1) { - StringBuilder msg = new StringBuilder(); - msg.append("DB_NAME expressions can only support ").append("targets with a single column PK. ") - .append("This entity has ").append(pk.size()).append(" columns in primary key."); + String msg = "DB_NAME expressions can only support targets with a single column PK. " + + "This entity has " + pk.size() + " columns in primary key."; - throw new CayenneRuntimeException(msg.toString()); + throw new CayenneRuntimeException(msg); } attribute = pk.iterator().next(); } else { - attribute = join.getSource(); + attribute = forceJoinForRelations ? join.getTarget() : join.getSource(); } processColumn(attribute); } + + /** + * Force joining tables for all relations, not only for toMany + * @since 4.0 + */ + protected void setForceJoinForRelations(boolean forceJoinForRelations) { + this.forceJoinForRelations = forceJoinForRelations; + } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java index 8491e23..0d35cea 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java @@ -161,6 +161,11 @@ public abstract class Expression implements Serializable, XMLSerializable { */ public static final int ASTERISK = 46; + /** + * @since 4.0 + */ + public static final int FULL_OBJECT = 47; + protected int type; /** http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java index e9fa6b0..b2770bc 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java @@ -33,6 +33,7 @@ import org.apache.cayenne.exp.parser.ASTDbPath; import org.apache.cayenne.exp.parser.ASTDivide; import org.apache.cayenne.exp.parser.ASTEqual; import org.apache.cayenne.exp.parser.ASTFalse; +import org.apache.cayenne.exp.parser.ASTFullObject; import org.apache.cayenne.exp.parser.ASTGreater; import org.apache.cayenne.exp.parser.ASTGreaterOrEqual; import org.apache.cayenne.exp.parser.ASTIn; @@ -1245,6 +1246,14 @@ public class ExpressionFactory { return joinExp(Expression.OR, pairs); } + public static Expression fullObjectExp() { + return new ASTFullObject(); + } + + public static Expression fullObjectExp(Expression exp) { + return new ASTFullObject(exp); + } + /** * @since 4.0 */ http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java index 59eb17d..9ceaf6f 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java @@ -18,6 +18,8 @@ ****************************************************************/ package org.apache.cayenne.exp; +import org.apache.cayenne.CayenneRuntimeException; +import org.apache.cayenne.Persistent; import org.apache.cayenne.exp.parser.ASTPath; import org.apache.cayenne.query.Ordering; import org.apache.cayenne.query.PrefetchTreeNode; @@ -27,6 +29,7 @@ import org.apache.cayenne.reflect.PropertyUtils; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; /** * <p> @@ -788,6 +791,24 @@ public class Property<E> { return new Property<>(alias, this.getExpression(), this.getType()); } + /** + * <p>Create new "flat" property for toMany relationship.</p> + * <p> + * Example: + * <pre>{@code + * List<Object[]> result = ObjectSelect + * .columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.PAINTING_ARRAY.flat(Painting.class)) + * .select(context); + * }</pre> + * </p> + */ + public <T extends Persistent> Property<T> flat(Class<? super T> tClass) { + if(!Collection.class.isAssignableFrom(type) && !Map.class.isAssignableFrom(type)) { + throw new CayenneRuntimeException("Can use flat() function only on Property mapped on toMany relationship."); + } + return create(ExpressionFactory.fullObjectExp(getExpression()), tClass); + } + public Class<? super E> getType() { return type; } @@ -820,6 +841,27 @@ public class Property<E> { } /** + * <p> + * Creates "self" Property for persistent class. + * This property can be used to select full object along with some of it properties (or + * properties that can be resolved against query root) + * </p> + * <p> + * Here is sample code, that will select all Artists and count of their Paintings: + * <pre>{@code + * Property<Artist> artistFull = Property.createSelf(Artist.class); + * List<Object[]> result = ObjectSelect + * .columnQuery(Artist.class, artistFull, Artist.PAINTING_ARRAY.count()) + * .select(context); + * } + * </pre> + * </p> + */ + public static <T extends Persistent> Property<T> createSelf(Class<? super T> type) { + return new Property<>(null, ExpressionFactory.fullObjectExp(), type); + } + + /** * Since Expression is mutable we need to provide clean Expression for every getter call. * So to keep Property itself immutable we use ExpressionProvider. * @see Property#Property(String, Class) http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java new file mode 100644 index 0000000..25f6d97 --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTFullObject.java @@ -0,0 +1,63 @@ +/***************************************************************** + * 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.exp.parser; + +import org.apache.cayenne.exp.Expression; + +/** + * @since 4.0 + */ +public class ASTFullObject extends SimpleNode { + + public ASTFullObject(Expression expression) { + this(); + Node node = wrapChild(expression); + jjtAddChild(node, 0); + node.jjtSetParent(this); + } + + public ASTFullObject() { + this(0); + } + + protected ASTFullObject(int i) { + super(i); + } + + @Override + protected String getExpressionOperator(int index) { + throw new UnsupportedOperationException(); + } + + @Override + protected Object evaluateNode(Object o) throws Exception { + return o; + } + + @Override + public Expression shallowCopy() { + return new ASTFullObject(id); + } + + @Override + public int getType() { + return Expression.FULL_OBJECT; + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java index 262f18c..da03ccb 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java @@ -125,7 +125,7 @@ public class ObjectSelect<T> extends FluentSelect<T, ObjectSelect<T>> { * @param entityType base persistent class that will be used as a root for this query * @param column single column to select */ - protected static <E> ColumnSelect<E> columnQuery(Class<?> entityType, Property<E> column) { + public static <E> ColumnSelect<E> columnQuery(Class<?> entityType, Property<E> column) { return new ColumnSelect<>().entityType(entityType).column(column); } @@ -136,7 +136,7 @@ public class ObjectSelect<T> extends FluentSelect<T, ObjectSelect<T>> { * @param firstColumn column to select * @param otherColumns columns to select */ - protected static ColumnSelect<Object[]> columnQuery(Class<?> entityType, Property<?> firstColumn, Property<?>... otherColumns) { + public static ColumnSelect<Object[]> columnQuery(Class<?> entityType, Property<?> firstColumn, Property<?>... otherColumns) { return new ColumnSelect<Object[]>().entityType(entityType).columns(firstColumn, otherColumns); } http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java index aba36c5..158fbea 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQueryMetadata.java @@ -21,14 +21,34 @@ package org.apache.cayenne.query; import java.io.IOException; import java.util.Collections; import java.util.HashMap; +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.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.exp.Property; +import org.apache.cayenne.exp.parser.ASTDbPath; +import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.DbJoin; +import org.apache.cayenne.map.DbRelationship; import org.apache.cayenne.map.EntityResolver; +import org.apache.cayenne.map.EntityResult; +import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.map.ObjRelationship; +import org.apache.cayenne.map.PathComponent; import org.apache.cayenne.map.SQLResult; +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.util.CayenneMapEntry; /** * @since 3.0 @@ -185,6 +205,7 @@ class SelectQueryMetadata extends BaseQueryMetadata { } /** + * Build DB result descriptor, that will be used to read and convert raw result of ColumnSelect * @since 4.0 */ private void buildResultSetMappingForColumns(SelectQuery<?> query, EntityResolver resolver) { @@ -194,13 +215,168 @@ class SelectQueryMetadata extends BaseQueryMetadata { SQLResult result = new SQLResult(); for(Property<?> column : query.getColumns()) { - String name = column.getName() == null ? column.getExpression().expName() : column.getName(); - result.addColumnResult(name); + Expression exp = column.getExpression(); + String name = column.getName() == null ? exp.expName() : column.getName(); + boolean fullObject = false; + if(exp.getType() == Expression.OBJ_PATH) { + // check if this is toOne relation + Expression dbPath = this.getObjEntity().translateToDbPath(exp); + DbRelationship rel = findRelationByPath(dbEntity, dbPath); + if(rel != null && !rel.isToMany()) { + // it this path is toOne relation, than select full object for it + fullObject = true; + } + } else if(exp.getType() == Expression.FULL_OBJECT) { + fullObject = true; + } + + if(fullObject) { + // detected full object column + if(getPageSize() > 0) { + // for paginated queries keep only IDs + result.addEntityResult(buildEntityIdResultForColumn(column, resolver)); + } else { + // will unwrap to full set of db-columns (with join prefetch optionally) + result.addEntityResult(buildEntityResultForColumn(query, column, resolver)); + } + } else { + // scalar column + result.addColumnResult(name); + } } resultSetMapping = result.getResolvedComponents(resolver); } /** + * Collect metadata for result with ObjectId (used for paginated queries with FullObject columns) + * + * @param column full object column + * @param resolver entity resolver + * @return Entity result + */ + private EntityResult buildEntityIdResultForColumn(Property<?> column, EntityResolver resolver) { + EntityResult result = new EntityResult(column.getType()); + DbEntity entity = resolver.getObjEntity(column.getType()).getDbEntity(); + for(DbAttribute attribute : entity.getPrimaryKeys()) { + result.addDbField(attribute.getName(), attribute.getName()); + } + return result; + } + + private DbRelationship findRelationByPath(DbEntity entity, Expression exp) { + DbRelationship r = null; + for (PathComponent<DbAttribute, DbRelationship> component : entity.resolvePath(exp, getPathSplitAliases())) { + r = component.getRelationship(); + } + return r; + } + + /** + * Collect metadata for column that will be unwrapped to full entity in the final SQL + * (possibly including joint prefetch). + * This information will be used to correctly create Persistent object back from raw result. + * + * This method is actually repeating logic of + * {@link org.apache.cayenne.access.translator.select.DefaultSelectTranslator#appendQueryColumns}. + * Here we don't care about intermediate joins and few other things so it's shorter. + * Logic of these methods should be unified and simplified, possibly to a single source of metadata, + * generated only once and used everywhere. + * + * @param query original query + * @param column full object column + * @param resolver entity resolver to get ObjEntity and ClassDescriptor + * @return Entity result + */ + private EntityResult buildEntityResultForColumn(SelectQuery<?> query, Property<?> column, EntityResolver resolver) { + final EntityResult result = new EntityResult(column.getType()); + + // Collecting visitor for ObjAttributes and toOne relationships + PropertyVisitor visitor = new PropertyVisitor() { + public boolean visitAttribute(AttributeProperty property) { + ObjAttribute oa = property.getAttribute(); + Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator(); + while (dbPathIterator.hasNext()) { + CayenneMapEntry pathPart = dbPathIterator.next(); + if (pathPart instanceof DbAttribute) { + result.addDbField(pathPart.getName(), pathPart.getName()); + } + } + return true; + } + + public boolean visitToMany(ToManyProperty property) { + return true; + } + + public boolean visitToOne(ToOneProperty property) { + DbRelationship dbRel = property.getRelationship().getDbRelationships().get(0); + List<DbJoin> joins = dbRel.getJoins(); + for (DbJoin join : joins) { + if(!join.getSource().isPrimaryKey()) { + result.addDbField(join.getSource().getName(), join.getSource().getName()); + } + } + return true; + } + }; + + ObjEntity oe = resolver.getObjEntity(column.getType()); + DbEntity table = oe.getDbEntity(); + + // Additionally collect PKs + for (DbAttribute dba : table.getPrimaryKeys()) { + result.addDbField(dba.getName(), dba.getName()); + } + + ClassDescriptor descriptor = resolver.getClassDescriptor(oe.getName()); + descriptor.visitAllProperties(visitor); + + // Collection columns for joint prefetch + if(query.getPrefetchTree() != null) { + for (PrefetchTreeNode prefetch : query.getPrefetchTree().adjacentJointNodes()) { + // for each prefetch add columns from the target entity + Expression prefetchExp = ExpressionFactory.exp(prefetch.getPath()); + ASTDbPath dbPrefetch = (ASTDbPath) oe.translateToDbPath(prefetchExp); + DbRelationship r = findRelationByPath(table, dbPrefetch); + if (r == null) { + throw new CayenneRuntimeException("Invalid joint prefetch '" + prefetch + "' for entity: " + + oe.getName()); + } + + // go via target OE to make sure that Java types are mapped correctly... + ObjRelationship targetRel = (ObjRelationship) prefetchExp.evaluate(oe); + ObjEntity targetEntity = targetRel.getTargetEntity(); + prefetch.setEntityName(targetRel.getSourceEntity().getName()); + + String labelPrefix = dbPrefetch.getPath(); + Set<String> seenNames = new HashSet<>(); + for (ObjAttribute oa : targetEntity.getAttributes()) { + Iterator<CayenneMapEntry> dbPathIterator = oa.getDbPathIterator(); + while (dbPathIterator.hasNext()) { + Object pathPart = dbPathIterator.next(); + if (pathPart instanceof DbAttribute) { + DbAttribute attribute = (DbAttribute) pathPart; + if(seenNames.add(attribute.getName())) { + result.addDbField(labelPrefix + '.' + attribute.getName(), labelPrefix + '.' + attribute.getName()); + } + } + } + } + + // append remaining target attributes such as keys + DbEntity targetDbEntity = r.getTargetEntity(); + for (DbAttribute attribute : targetDbEntity.getAttributes()) { + if(seenNames.add(attribute.getName())) { + result.addDbField(labelPrefix + '.' + attribute.getName(), labelPrefix + '.' + attribute.getName()); + } + } + } + } + + return result; + } + + /** * @since 4.0 */ @Override http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java b/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java index 83913b6..191c725 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/CayenneCompoundIT.java @@ -20,6 +20,8 @@ package org.apache.cayenne; import org.apache.cayenne.di.Inject; +import org.apache.cayenne.exp.Property; +import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.query.SelectQuery; import org.apache.cayenne.test.jdbc.DBHelper; import org.apache.cayenne.test.jdbc.TableHelper; @@ -65,6 +67,12 @@ public class CayenneCompoundIT extends ServerCase { tCompoundPKTest.insert("PK1", "PK2", "BBB"); } + private void createCompoundPKs(int size) throws Exception { + for(int i=0; i<size; i++) { + tCompoundPKTest.insert("PK"+i, "PK"+(2*i), "BBB"+i); + } + } + private void createOneCharPK() throws Exception { tCharPKTest.insert("CPK", "AAAA"); } @@ -157,4 +165,20 @@ public class CayenneCompoundIT extends ServerCase { assertEquals("CPK", Cayenne.pkForObject(object)); } + + @Test + public void testPaginatedColumnSelect() throws Exception { + createCompoundPKs(20); + + List<Object[]> result = ObjectSelect.query(CompoundPkTestEntity.class) + .columns(CompoundPkTestEntity.NAME, Property.createSelf(CompoundPkTestEntity.class)) + .pageSize(7) + .select(context); + assertEquals(20, result.size()); + for(Object[] next : result) { + assertEquals(2, next.length); + assertEquals(String.class, next[0].getClass()); + assertEquals(CompoundPkTestEntity.class, next[1].getClass()); + } + } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/e098f236/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java index 4319398..02102c5 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java @@ -23,15 +23,24 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Types; import java.text.DateFormat; +import java.util.List; import java.util.Locale; import org.apache.cayenne.CayenneRuntimeException; +import org.apache.cayenne.Fault; +import org.apache.cayenne.PersistenceState; +import org.apache.cayenne.ResultBatchIterator; +import org.apache.cayenne.ResultIteratorCallback; import org.apache.cayenne.access.DataContext; import org.apache.cayenne.di.Inject; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.exp.FunctionExpressionFactory; import org.apache.cayenne.exp.Property; import org.apache.cayenne.test.jdbc.DBHelper; import org.apache.cayenne.test.jdbc.TableHelper; import org.apache.cayenne.testdo.testmap.Artist; +import org.apache.cayenne.testdo.testmap.Gallery; import org.apache.cayenne.testdo.testmap.Painting; import org.apache.cayenne.unit.PostgresUnitDbAdapter; import org.apache.cayenne.unit.UnitDbAdapter; @@ -42,8 +51,10 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import static org.apache.cayenne.exp.FunctionExpressionFactory.substringExp; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** @@ -333,4 +344,499 @@ public class ColumnSelectIT extends ServerCase { .selectOne(context); assertEquals(count2, count3); } + + @Test + public void testSelectFirst_MultiColumns() throws Exception { + Object[] a = ObjectSelect.query(Artist.class) + .columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH) + .columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH) + .columns(Artist.ARTIST_NAME.alias("newName")) + .where(Artist.ARTIST_NAME.like("artist%")) + .orderBy("db:ARTIST_ID") + .selectFirst(context); + assertNotNull(a); + assertEquals("artist1", a[0]); + assertEquals("artist1", a[4]); + } + + @Test + public void testSelectFirst_SingleValueInColumns() throws Exception { + Object[] a = ObjectSelect.query(Artist.class) + .columns(Artist.ARTIST_NAME) + .where(Artist.ARTIST_NAME.like("artist%")) + .orderBy("db:ARTIST_ID") + .selectFirst(context); + assertNotNull(a); + assertEquals("artist1", a[0]); + } + + @Test + public void testSelectFirst_SubstringName() throws Exception { + Expression exp = FunctionExpressionFactory.substringExp(Artist.ARTIST_NAME.path(), 5, 3); + Property<String> substrName = Property.create("substrName", exp, String.class); + Object[] a = ObjectSelect.query(Artist.class) + .columns(Artist.ARTIST_NAME, substrName) + .where(substrName.eq("st3")) + .selectFirst(context); + + assertNotNull(a); + assertEquals("artist3", a[0]); + assertEquals("st3", a[1]); + } + + @Test + public void testSelectFirst_RelColumns() throws Exception { + // set shorter than painting_array.paintingTitle alias as some DBs doesn't support dot in alias + Property<String> paintingTitle = Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).alias("paintingTitle"); + + Object[] a = ObjectSelect.query(Artist.class) + .columns(Artist.ARTIST_NAME, paintingTitle) + .orderBy(paintingTitle.asc()) + .selectFirst(context); + assertNotNull(a); + assertEquals("painting1", a[1]); + } + + @Test + public void testSelectFirst_RelColumn() throws Exception { + // set shorter than painting_array.paintingTitle alias as some DBs doesn't support dot in alias + Property<String> paintingTitle = Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).alias("paintingTitle"); + + String a = ObjectSelect.query(Artist.class) + .column(paintingTitle) + .orderBy(paintingTitle.asc()) + .selectFirst(context); + assertNotNull(a); + assertEquals("painting1", a); + } + + @Test + public void testSelectFirst_RelColumnWithFunction() throws Exception { + Property<String> altTitle = Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE) + .substring(7, 3).concat(" ", Artist.ARTIST_NAME) + .alias("altTitle"); + + String a = ObjectSelect.query(Artist.class) + .column(altTitle) + .where(altTitle.like("ng1%")) + .and(Artist.ARTIST_NAME.like("%ist1")) +// .orderBy(altTitle.asc()) // unsupported for now + .selectFirst(context); + assertNotNull(a); + assertEquals("ng1 artist1", a); + } + + /* + * Test iterated select + */ + + @Test + public void testIterationSingleColumn() throws Exception { + ColumnSelect<String> columnSelect = ObjectSelect.query(Artist.class).column(Artist.ARTIST_NAME); + + final int[] count = new int[1]; + columnSelect.iterate(context, new ResultIteratorCallback<String>() { + @Override + public void next(String object) { + count[0]++; + assertTrue(object.startsWith("artist")); + } + }); + + assertEquals(20, count[0]); + } + + @Test + public void testBatchIterationSingleColumn() throws Exception { + ColumnSelect<String> columnSelect = ObjectSelect.query(Artist.class).column(Artist.ARTIST_NAME); + + try(ResultBatchIterator<String> it = columnSelect.batchIterator(context, 10)) { + List<String> next = it.next(); + assertEquals(10, next.size()); + assertTrue(next.get(0).startsWith("artist")); + } + } + + @Test + public void testIterationMultiColumns() throws Exception { + ColumnSelect<Object[]> columnSelect = ObjectSelect.query(Artist.class).columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH); + + final int[] count = new int[1]; + columnSelect.iterate(context, new ResultIteratorCallback<Object[]>() { + @Override + public void next(Object[] object) { + count[0]++; + assertTrue(object[0] instanceof String); + assertTrue(object[1] instanceof java.util.Date); + } + }); + + assertEquals(20, count[0]); + } + + @Test + public void testBatchIterationMultiColumns() throws Exception { + ColumnSelect<Object[]> columnSelect = ObjectSelect.query(Artist.class).columns(Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH); + + try(ResultBatchIterator<Object[]> it = columnSelect.batchIterator(context, 10)) { + List<Object[]> next = it.next(); + assertEquals(10, next.size()); + assertTrue(next.get(0)[0] instanceof String); + assertTrue(next.get(0)[1] instanceof java.util.Date); + } + } + + /* + * Test select with page size + */ + + @Test + public void testPageSizeOneScalar() { + List<String> a = ObjectSelect.query(Artist.class) + .column(Artist.ARTIST_NAME.trim()) + .pageSize(10) + .select(context); + assertNotNull(a); + assertEquals(20, a.size()); + int idx = 0; + for(String next : a) { + assertNotNull(""+idx, next); + idx++; + } + } + + @Test + public void testPageSizeScalars() { + List<Object[]> a = ObjectSelect.query(Artist.class) + .columns(Artist.ARTIST_NAME.trim(), Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY.count()) + .pageSize(10) + .select(context); + assertNotNull(a); + assertEquals(5, a.size()); + int idx = 0; + for(Object[] next : a) { + assertNotNull(next); + assertTrue("" + idx, next[0] instanceof String); + assertTrue("" + idx, next[1] instanceof java.util.Date); + assertTrue("" + idx, next[2] instanceof Long); + idx++; + } + } + + @Test + public void testPageSizeOneObject() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + List<Artist> a = ObjectSelect.query(Artist.class) + .column(artistFull) + .pageSize(10) + .select(context); + assertNotNull(a); + assertEquals(20, a.size()); + for(Artist next : a){ + assertNotNull(next); + } + } + + @Test + public void testPageSizeObjectAndScalars() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + List<Object[]> a = ObjectSelect.query(Artist.class) + .columns(Artist.ARTIST_NAME, artistFull, Artist.PAINTING_ARRAY.count()) + .pageSize(10) + .select(context); + assertNotNull(a); + assertEquals(5, a.size()); + int idx = 0; + for(Object[] next : a) { + assertNotNull(next); + assertEquals("" + idx, String.class, next[0].getClass()); + assertEquals("" + idx, Artist.class, next[1].getClass()); + assertEquals("" + idx, Long.class, next[2].getClass()); + idx++; + } + } + + @Test + public void testPageSizeObjects() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + List<Object[]> a = ObjectSelect.query(Artist.class) + .columns(Artist.ARTIST_NAME, artistFull, Artist.PAINTING_ARRAY.flat(Painting.class)) + .pageSize(10) + .select(context); + assertNotNull(a); + assertEquals(21, a.size()); + int idx = 0; + for(Object[] next : a) { + assertNotNull(next); + assertEquals("" + idx, String.class, next[0].getClass()); + assertEquals("" + idx, Artist.class, next[1].getClass()); + assertEquals("" + idx, Painting.class, next[2].getClass()); + idx++; + } + } + + /* + * Test prefetch + */ + + @Test + public void testObjectColumnWithJointPrefetch() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + + List<Object[]> result = ObjectSelect.query(Artist.class) + .columns(artistFull, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE)) + .prefetch(Artist.PAINTING_ARRAY.joint()) + .select(context); + + checkPrefetchResults(result); + } + + @Test + public void testObjectColumnWithDisjointPrefetch() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + + List<Object[]> result = ObjectSelect.query(Artist.class) + .columns(artistFull, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE)) + .prefetch(Artist.PAINTING_ARRAY.disjoint()) + .select(context); + + checkPrefetchResults(result); + } + + @Test + public void testObjectColumnWithDisjointByIdPrefetch() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + + List<Object[]> result = ObjectSelect.query(Artist.class) + .columns(artistFull, Artist.DATE_OF_BIRTH, Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE)) + .prefetch(Artist.PAINTING_ARRAY.disjointById()) + .select(context); + + checkPrefetchResults(result); + } + + private void checkPrefetchResults(List<Object[]> result) { + assertEquals(21, result.size()); + for(Object[] next : result) { + assertTrue(next[0] instanceof Artist); + assertTrue(next[1] instanceof java.util.Date); + assertTrue(next[2] instanceof String); + Artist artist = (Artist)next[0]; + assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState()); + + Object paintingsArr = artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName()); + assertFalse(paintingsArr instanceof Fault); + assertTrue(((List)paintingsArr).size() > 0); + } + } + + @Test + public void testAggregateColumnWithJointPrefetch() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + + List<Object[]> result = ObjectSelect.query(Artist.class) + .columns(artistFull, Artist.PAINTING_ARRAY.count()) + .prefetch(Artist.PAINTING_ARRAY.joint()) + .select(context); + + checkAggregatePrefetchResults(result); + } + + @Test + public void testAggregateColumnWithDisjointPrefetch() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + + List<Object[]> result = ObjectSelect.query(Artist.class) + .columns(artistFull, Artist.PAINTING_ARRAY.count()) + .prefetch(Artist.PAINTING_ARRAY.disjoint()) + .select(context); + + checkAggregatePrefetchResults(result); + } + + @Test + public void testAggregateColumnWithDisjointByIdPrefetch() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + + List<Object[]> result = ObjectSelect.query(Artist.class) + .columns(artistFull, Artist.PAINTING_ARRAY.count()) + .prefetch(Artist.PAINTING_ARRAY.disjointById()) + .select(context); + + checkAggregatePrefetchResults(result); + } + + private void checkAggregatePrefetchResults(List<Object[]> result) { + assertEquals(5, result.size()); + for(Object[] next : result) { + assertTrue(next[0] instanceof Artist); + assertTrue(next[1] instanceof Long); + Artist artist = (Artist)next[0]; + assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState()); + + Object paintingsArr = artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName()); + assertFalse(paintingsArr instanceof Fault); + assertTrue(((List)paintingsArr).size() == (long)next[1]); + } + } + + @Test + public void testObjectSelectWithJointPrefetch() { + List<Artist> result = ObjectSelect.query(Artist.class) + .column(Property.createSelf(Artist.class)) + .prefetch(Artist.PAINTING_ARRAY.joint()) + .select(context); + assertEquals(20, result.size()); + + for(Artist artist : result) { + assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState()); + + Object paintingsArr = artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName()); + assertFalse(paintingsArr instanceof Fault); + } + } + + @Test + public void testObjectWithDisjointPrefetch() { + List<Artist> result = ObjectSelect.query(Artist.class) + .column(Property.createSelf(Artist.class)) + .prefetch(Artist.PAINTING_ARRAY.disjoint()) + .select(context); + assertEquals(20, result.size()); + for(Artist artist : result) { + assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState()); + + Object paintingsArr = artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName()); + assertFalse(paintingsArr instanceof Fault); + } + } + + @Test + public void testObjectWithDisjointByIdPrefetch() { + List<Artist> result = ObjectSelect.query(Artist.class) + .column(Property.createSelf(Artist.class)) + .prefetch(Artist.PAINTING_ARRAY.disjointById()) + .select(context); + assertEquals(20, result.size()); + for(Artist artist : result) { + assertEquals(PersistenceState.COMMITTED, artist.getPersistenceState()); + + Object paintingsArr = artist.readPropertyDirectly(Artist.PAINTING_ARRAY.getName()); + assertFalse(paintingsArr instanceof Fault); + } + } + + /* + * Test Persistent object select + */ + + @Test + public void testObjectColumn() { + Property<Artist> artistFull = Property.createSelf(Artist.class); + + List<Object[]> result = ObjectSelect.query(Artist.class) + .columns(artistFull, Artist.ARTIST_NAME, Artist.PAINTING_ARRAY.count()) + .select(context); + assertEquals(5, result.size()); + + for(Object[] next : result) { + assertTrue(next[0] instanceof Artist); + assertTrue(next[1] instanceof String); + assertTrue(next[2] instanceof Long); + assertEquals(PersistenceState.COMMITTED, ((Artist)next[0]).getPersistenceState()); + } + } + + @Test + public void testObjectColumnToOne() { + Property<Artist> artistFull = Property.create(ExpressionFactory.fullObjectExp(Painting.TO_ARTIST.getExpression()), Artist.class); + Property<Gallery> galleryFull = Property.create(ExpressionFactory.fullObjectExp(Painting.TO_GALLERY.getExpression()), Gallery.class); + + List<Object[]> result = ObjectSelect.query(Painting.class) + .columns(Painting.PAINTING_TITLE, artistFull, galleryFull) + .select(context); + assertEquals(21, result.size()); + + for(Object[] next : result) { + assertTrue(next[0] instanceof String); + assertTrue(next[1] instanceof Artist); + assertTrue(next[2] instanceof Gallery); + assertEquals(PersistenceState.COMMITTED, ((Artist)next[1]).getPersistenceState()); + } + } + + @Test + public void testObjectColumnToOneAsObjPath() { + + List<Object[]> result = ObjectSelect.query(Painting.class) + .columns(Painting.PAINTING_TITLE, Painting.TO_ARTIST, Painting.TO_GALLERY) + .select(context); + assertEquals(21, result.size()); + + for(Object[] next : result) { + assertTrue(next[0] instanceof String); + assertTrue(next[1] instanceof Artist); + assertTrue(next[2] instanceof Gallery); + assertEquals(PersistenceState.COMMITTED, ((Artist)next[1]).getPersistenceState()); + } + } + + @Test + public void testObjectColumnToMany() throws Exception { + Property<Artist> artist = Property.createSelf(Artist.class); + + List<Object[]> result = ObjectSelect.query(Artist.class) + .columns(artist, Artist.PAINTING_ARRAY.flat(Painting.class), Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY)) + .select(context); + assertEquals(21, result.size()); + + for(Object[] next : result) { + assertTrue(next[0] instanceof Artist); + assertTrue(next[1] instanceof Painting); + assertTrue(next[2] instanceof Gallery); + assertEquals(PersistenceState.COMMITTED, ((Artist)next[0]).getPersistenceState()); + assertEquals(PersistenceState.COMMITTED, ((Painting)(next[1])).getPersistenceState()); + assertEquals(PersistenceState.COMMITTED, ((Gallery)(next[2])).getPersistenceState()); + } + } + + @Test(expected = CayenneRuntimeException.class) + public void testDirectRelationshipSelect() { + // We should fail here as actual result will be just distinct paintings' ids. + List<List<Painting>> result = ObjectSelect.query(Artist.class) + .column(Artist.PAINTING_ARRAY).select(context); + assertEquals(21, result.size()); + } + + @Test(expected = CayenneRuntimeException.class) + public void testSelfPropertyInOrderBy() { + Property<Artist> artistProperty = Property.createSelf(Artist.class); + ObjectSelect.query(Artist.class) + .column(artistProperty) + .orderBy(artistProperty.desc()) + .select(context); + } + + @Test(expected = CayenneRuntimeException.class) + public void testSelfPropertyInWhere() { + Artist artist = ObjectSelect.query(Artist.class).selectFirst(context); + Property<Artist> artistProperty = Property.createSelf(Artist.class); + List<Artist> result = ObjectSelect.query(Artist.class) + .column(artistProperty) + .where(artistProperty.eq(artist)) + .select(context); + } + + @Test + public void testObjPropertyInWhere() { + Artist artist = ObjectSelect.query(Artist.class, Artist.ARTIST_NAME.eq("artist1")) + .selectFirst(context); + Property<Painting> paintingProperty = Property.createSelf(Painting.class); + List<Painting> result = ObjectSelect.query(Painting.class) + .column(paintingProperty) + .where(Painting.TO_ARTIST.eq(artist)) + .select(context); + assertEquals(4, result.size()); + } + }
