diff --git a/cayenne-server/src/main/java/org/apache/cayenne/QueryResponse.java b/cayenne-server/src/main/java/org/apache/cayenne/QueryResponse.java index c7a481d90c..4c4c282ee5 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/QueryResponse.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/QueryResponse.java @@ -68,12 +68,26 @@ public interface QueryResponse { */ boolean isList(); + /** + * Returns whether current response is an iterator + * + * @since 5.0 + */ + boolean isIterator(); + /** * Returns a List under the current iterator position. Use {@link #isList()} to check * the result type before calling this method. */ List currentList(); + /** + * Returns a current iterator. + * + * @since 5.0 + */ + ResultIterator currentIterator(); + /** * Returns an update count under the current iterator position. Returned value is an * int[] to accommodate batch queries. For a regular update result, the value will be @@ -100,6 +114,15 @@ public interface QueryResponse { @SuppressWarnings("rawtypes") List firstList(); + /** + * A utility method for quickly retrieving the Iterator in the response. Returns + * null if the query has no iterator. + * + * @since 5.0 + */ + @SuppressWarnings("rawtypes") + ResultIterator firstIterator(); + /** * A utility method for quickly retrieving the first update count from the response. * Note that this method resets current iterator to an undefined state. diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContext.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContext.java index e6d91cfa6e..347eaf7bff 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContext.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContext.java @@ -65,7 +65,6 @@ import org.apache.cayenne.tx.TransactionFactory; import org.apache.cayenne.util.EventUtil; import org.apache.cayenne.util.GenericResponse; -import org.apache.cayenne.util.ResultIteratorIterator; import org.apache.cayenne.util.Util; /** @@ -180,7 +179,7 @@ public DataDomain getParentDataDomain() { return (DataDomain) channel; } - List response = channel.onQuery(this, new DataDomainQuery()).firstList(); + List response = channel.onQuery(this, new DataDomainQuery()).firstList(); if (response != null && response.size() > 0 && response.get(0) instanceof DataDomain) { return (DataDomain) response.get(0); @@ -277,7 +276,7 @@ public Collection uncommittedObjects() { // guess target collection size Collection objects = new ArrayList<>(len > 100 ? len / 2 : len); - Iterator it = getObjectStore().getObjectIterator(); + Iterator it = getObjectStore().getObjectIterator(); while (it.hasNext()) { Persistent object = (Persistent) it.next(); int state = object.getPersistenceState(); @@ -420,7 +419,7 @@ public List objectsFromDataRows(ClassDescriptor descriptor, List dataRows) { + private List objectsFromDataRowsFromParentContext(ClassDescriptor descriptor, List dataRows) { return getChannel().onQuery(this, new ObjectsFromDataRowsQuery(descriptor, dataRows)).firstList(); } @@ -568,9 +567,9 @@ public boolean visitToMany(ToManyProperty property) { Object value = property.readProperty(persistent); @SuppressWarnings("unchecked") - Collection collection = (value instanceof Map) + Collection> collection = (value instanceof Map) ? ((Map) value).entrySet() - : (Collection) value; + : (Collection>) value; for (Object target : collection) { if (target instanceof Persistent) { @@ -612,7 +611,7 @@ public boolean visitAttribute(AttributeProperty property) { * * @see #invalidateObjects(Collection) */ - public void unregisterObjects(Collection dataObjects) { + public void unregisterObjects(Collection dataObjects) { getObjectStore().objectsUnregistered(dataObjects); } @@ -796,36 +795,12 @@ GraphDiff flushToParent(boolean cascade) { @SuppressWarnings("unchecked") @Override public ResultIterator iterator(final Select query) { - final ResultIterator rows = performIteratedQuery(query); - final QueryMetadata md = query.getMetaData(getEntityResolver()); - - if (md.isFetchingDataRows() || isObjectArrayResult(md)) { - // no need to convert result - return (ResultIterator) rows; - } else { - // this is a bit optimized version of 'objectFromDataRow' with - // resolver cached for reuse... still the rest is pretty suboptimal - final ObjectResolver resolver = new ObjectResolver(this, md.getClassDescriptor(), true); - return new DataRowResultIterator(rows, resolver); - } + IteratedQueryDecorator queryDecorator = new IteratedQueryDecorator(query); + Query queryToRun = nonNullDelegate().willPerformQuery(this, queryDecorator); + QueryResponse queryResponse = onQuery(this, queryToRun); + return (ResultIterator) queryResponse.currentIterator(); } - /** - * This method repeats logic of DataDomainQueryAction.interceptObjectConversion() method. - * The difference is that iterator(or batchIterator) doesn't support "mixed" results. - */ - private boolean isObjectArrayResult(QueryMetadata md) { - List resultMapping = md.getResultSetMapping(); - if(resultMapping == null) { - return false; - } - - if (md.isSingleResultSetMapping()) { - return !(resultMapping.get(0) instanceof EntityResultSegment); - } else { - return true; - } - } /** * Performs a single database select query returning result as a @@ -841,8 +816,7 @@ private boolean isObjectArrayResult(QueryMetadata md) { * {@link #iterate(Select, org.apache.cayenne.ResultIteratorCallback)} to * get access to objects. */ - // TODO: deprecate once all selecting queries start implementing Select - // interface + // TODO: deprecate once all selecting queries start implementing Select interface @SuppressWarnings({ "rawtypes" }) public ResultIterator performIteratedQuery(Query query) { @@ -879,7 +853,7 @@ public ResultIterator performIteratedQuery(Query query) { if (tx.isRollbackOnly()) { try { tx.rollback(); - } catch (Exception rollbackEx) { + } catch (Exception ignored) { } } } @@ -891,7 +865,7 @@ public ResultIterator performIteratedQuery(Query query) { /** * Runs an iterated query in a transactional context provided by the caller. */ - ResultIterator internalPerformIteratedQuery(Query query) { + ResultIterator internalPerformIteratedQuery(Query query) { // note that for now DataChannel API does not support cursors (aka // ResultIterator), so we have to go directly to the DataDomain. IteratedSelectObserver observer = new IteratedSelectObserver(); @@ -937,16 +911,16 @@ public QueryResponse performGenericQuery(Query query) { * * @return A list of DataObjects or a DataRows, depending on the value * returned by {@link QueryMetadata#isFetchingDataRows()}. + * Сan also return an iterator if the query is an instance of iteratedQuery. */ @Override - @SuppressWarnings("unchecked") public List performQuery(Query query) { query = nonNullDelegate().willPerformQuery(this, query); if (query == null) { return new ArrayList<>(1); } - List result = onQuery(this, query).firstList(); + List result = onQuery(this, query).firstList(); return result != null ? result : new ArrayList<>(1); } @@ -1027,8 +1001,8 @@ public List performQuery(String queryName, boolean expireCachedLists) { * is required in case a query uses caching. * @since 1.1 */ - public List performQuery(String queryName, Map parameters, boolean expireCachedLists) { - return performQuery(expireCachedLists ? + public List performQuery(String queryName, Map parameters, boolean expireCachedLists) { + return (List) performQuery(expireCachedLists ? MappedSelect.query(queryName).params(parameters).forceNoCache() : MappedSelect.query(queryName).params(parameters)); } @@ -1176,7 +1150,7 @@ protected void fireDataChannelChanged(Object postedBy, GraphDiff changes) { super.fireDataChannelChanged(postedBy, changes); } - private TransactionFactory getTransactionFactory() { + TransactionFactory getTransactionFactory() { attachToRuntimeIfNeeded(); return transactionFactory; } @@ -1191,55 +1165,4 @@ public void setTransactionFactory(TransactionFactory transactionFactory) { this.transactionFactory = transactionFactory; } - /** - * ResultIterator that can convert DataRow to Persistent object on the fly. - */ - static class DataRowResultIterator implements ResultIterator { - - final ResultIterator rows; - ObjectResolver resolver; - - DataRowResultIterator(ResultIterator rows, ObjectResolver resolver) { - this.rows = rows; - this.resolver = resolver; - } - - @Override - public Iterator iterator() { - return new ResultIteratorIterator<>(this); - } - - @Override - public List allRows() { - List list = new ArrayList<>(); - while (hasNextRow()) { - list.add(nextRow()); - } - return list; - } - - @Override - public boolean hasNextRow() { - return rows.hasNextRow(); - } - - @Override - @SuppressWarnings("unchecked") - public T nextRow() { - DataRow row = (DataRow) rows.nextRow(); - List objects = (List) resolver.synchronizedObjectsFromDataRows(Collections.singletonList(row)); - return objects.get(0); - } - - @Override - public void skipRow() { - rows.skipRow(); - } - - @Override - public void close() { - rows.close(); - } - } - } 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 5ec153e55a..235f424279 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 @@ -39,6 +39,7 @@ import org.apache.cayenne.map.ObjRelationship; import org.apache.cayenne.query.EmbeddableResultSegment; import org.apache.cayenne.query.EntityResultSegment; +import org.apache.cayenne.query.IteratedQueryDecorator; import org.apache.cayenne.query.ObjectIdQuery; import org.apache.cayenne.query.PrefetchSelectQuery; import org.apache.cayenne.query.PrefetchTreeNode; @@ -50,9 +51,14 @@ import org.apache.cayenne.query.RelationshipQuery; import org.apache.cayenne.reflect.ClassDescriptor; import org.apache.cayenne.reflect.LifecycleCallbackRegistry; +import org.apache.cayenne.tx.BaseTransaction; +import org.apache.cayenne.tx.Transaction; import org.apache.cayenne.util.GenericResponse; +import org.apache.cayenne.util.IteratedQueryResponse; import org.apache.cayenne.util.ListResponse; import org.apache.cayenne.util.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; @@ -74,21 +80,24 @@ */ class DataDomainQueryAction implements QueryRouter, OperationObserver { - static final boolean DONE = true; + private static final Logger LOGGER = LoggerFactory.getLogger(DataDomainQueryAction.class); - final DataContext context; + private static final boolean DONE = true; + + private final DataContext context; final DataDomain domain; final Query query; - final QueryMetadata metadata; - final AdhocObjectFactory objectFactory; - - DataRowStore cache; - QueryResponse response; - GenericResponse fullResponse; - Map> prefetchResultsByPath; - Map> queriesByNode; - Map queriesByExecutedQueries; - boolean noObjectConversion; + private final QueryMetadata metadata; + private final AdhocObjectFactory objectFactory; + + private DataRowStore cache; + private QueryResponse response; + private GenericResponse fullResponse; + private Map> prefetchResultsByPath; + private Map> queriesByNode; + private Map queriesByExecutedQueries; + private boolean noObjectConversion; + private IteratedQueryResponse iteratedQueryResponse; /* * A constructor for the "new" way of performing a query via 'execute' with @@ -119,12 +128,14 @@ class DataDomainQueryAction implements QueryRouter, OperationObserver { QueryResponse execute() { // run chain... - if (interceptOIDQuery() != DONE) { - if (interceptRelationshipQuery() != DONE) { - if (interceptRefreshQuery() != DONE) { - if (interceptSharedCache() != DONE) { - if (interceptDataDomainQuery() != DONE) { - runQueryInTransaction(); + if (interceptIteratedQuery() != DONE) { + if (interceptOIDQuery() != DONE) { + if (interceptRelationshipQuery() != DONE) { + if (interceptRefreshQuery() != DONE) { + if (interceptSharedCache() != DONE) { + if (interceptDataDomainQuery() != DONE) { + runQueryInTransaction(); + } } } } @@ -138,6 +149,54 @@ QueryResponse execute() { return response; } + private boolean interceptIteratedQuery() { + if (query instanceof IteratedQueryDecorator) { + validateIteratedQuery(); + performIteratedQuery(); + return DONE; + } + return !DONE; + } + + private void validateIteratedQuery() { + if (metadata.getPageSize() > 0){ + throw new CayenneRuntimeException("Pagination is not supported with iterator"); + } + if (metadata.getPrefetchTree() != null) { + for (PrefetchTreeNode prefetchTreeNode : metadata.getPrefetchTree().getChildren()) { + if (prefetchTreeNode.isDisjointPrefetch()) { + throw new CayenneRuntimeException("\"Disjoint\" semantic doesn't work with iterator. Use \"Joint\" instead"); + } + if (prefetchTreeNode.isDisjointByIdPrefetch()){ + LOGGER.warn("A separate select query will be created for each iterated item"); + } + } + } + } + + private void performIteratedQuery() { + Transaction tx = BaseTransaction.getThreadTransaction(); + if (tx != null) { + runIteratedQuery(tx); + } else { + tx = context.getTransactionFactory().createTransaction(); + BaseTransaction.bindThreadTransaction(tx); + try { + runIteratedQuery(tx); + } catch (Exception e) { + throw new CayenneRuntimeException(e); + } finally { + BaseTransaction.bindThreadTransaction(null); + if (tx.isRollbackOnly()) { + try { + tx.rollback(); + } catch (Exception ignored) { + } + } + } + } + } + private boolean interceptDataDomainQuery() { if (query instanceof DataDomainQuery) { response = new ListResponse(domain); @@ -471,45 +530,95 @@ private void runQuery() { } } + private void runIteratedQuery(Transaction tx){ + // reset + this.fullResponse = null; + this.response = null; + this.queriesByNode = null; + this.queriesByExecutedQueries = null; + + // whether this is null or not will driver further decisions on how to process prefetched rows + this.prefetchResultsByPath = metadata.getPrefetchTree() != null && !metadata.isFetchingDataRows() + ? new HashMap<>() : null; + + // categorize queries by node and by "executable" query... + query.route(this, domain.getEntityResolver(), null); + + // run categorized queries + if (queriesByNode != null) { + for (Map.Entry> entry : queriesByNode.entrySet()) { + QueryEngine nextNode = entry.getKey(); + Collection nodeQueries = entry.getValue(); + nextNode.performQueries(nodeQueries, this); + } + } + + wrapResponseIteratorWithTransactionDecorator(tx); + this.fullResponse = iteratedQueryResponse; + this.response = iteratedQueryResponse; + } + + private void wrapResponseIteratorWithTransactionDecorator(Transaction tx){ + ResultIterator iterator = iteratedQueryResponse.currentIterator(); + TransactionResultIteratorDecorator decorator = new TransactionResultIteratorDecorator<>(iterator, tx); + iteratedQueryResponse.setIterator(decorator); + } + + private void wrapResponseIteratorWithConverterDecorator(ObjectConversionStrategy converter) { + ResultIterator iterator = response.currentIterator(); + ResultIteratorConverterDecorator decorator = new ResultIteratorConverterDecorator(iterator, converter); + iteratedQueryResponse.setIterator(decorator); + } + @SuppressWarnings("unchecked") private void interceptObjectConversion() { - if (context != null) { - List mainRows = response.firstList(); // List or List - if (mainRows != null && !mainRows.isEmpty()) { + ObjectConversionStrategy converter = getConverter(); + if (response.isIterator()) { + wrapResponseIteratorWithConverterDecorator(converter); + } else { + List mainRows = response.firstList(); // List or List + if (mainRows != null && !mainRows.isEmpty()) { + converter.convert(mainRows); + rewindResponseAfterFirstListCall(); + } + } + } + } - ObjectConversionStrategy converter; - if(metadata.isFetchingDataRows()) { - converter = new IdentityConversionStrategy(); - } else { - List rsMapping = metadata.getResultSetMapping(); - if (rsMapping == null) { + private void rewindResponseAfterFirstListCall() { + response.reset(); + } + + private ObjectConversionStrategy getConverter() { + ObjectConversionStrategy converter; + + if(metadata.isFetchingDataRows()) { + converter = new IdentityConversionStrategy(); + } else { + List rsMapping = metadata.getResultSetMapping(); + if (rsMapping == null) { + converter = new SingleObjectConversionStrategy(); + } else { + if (metadata.isSingleResultSetMapping()) { + if (rsMapping.get(0) instanceof EntityResultSegment) { converter = new SingleObjectConversionStrategy(); + } else if(rsMapping.get(0) instanceof EmbeddableResultSegment) { + converter = new SingleEmbeddableConversionStrategy(); } else { - if (metadata.isSingleResultSetMapping()) { - if (rsMapping.get(0) instanceof EntityResultSegment) { - converter = new SingleObjectConversionStrategy(); - } else if(rsMapping.get(0) instanceof EmbeddableResultSegment) { - converter = new SingleEmbeddableConversionStrategy(); - } else { - converter = new SingleScalarConversionStrategy(); - } - } else { - converter = new MixedConversionStrategy(); - } + converter = new SingleScalarConversionStrategy(); } + } else { + converter = new MixedConversionStrategy(); } - - if(metadata.getResultMapper() != null) { - converter = new MapperConversionStrategy(converter); - } - - converter.convert(mainRows); - // rewind response after firstList() call - response.reset(); } } + + if(metadata.getResultMapper() != null) { + converter = new MapperConversionStrategy(converter); + } + return converter; } @Override @@ -604,16 +713,21 @@ public void nextRows(Query query, List dataRows) { @Override public void nextRows(Query q, ResultIterator it) { - throw new CayenneRuntimeException("Invalid attempt to fetch a cursor."); + iteratedQueryResponse = new IteratedQueryResponse(it); + // exclude prefetched rows in the main result + if (prefetchResultsByPath != null && query instanceof PrefetchSelectQuery) { + PrefetchSelectQuery prefetchQuery = (PrefetchSelectQuery) query; + prefetchResultsByPath.put(prefetchQuery.getPrefetchPath(), (List) it); + } else { + this.fullResponse = iteratedQueryResponse; + } } @Override public void nextGeneratedRows(Query query, ResultIterator keys, List idsToUpdate) { if (keys != null) { - try { + try (keys) { nextRows(query, keys.allRows()); - } finally { - keys.close(); } } } @@ -630,13 +744,14 @@ public void nextGlobalException(Exception e) { @Override public boolean isIteratedResult() { - return false; + return (query instanceof IteratedQueryDecorator); } - abstract class ObjectConversionStrategy { - + abstract class ObjectConversionStrategy { abstract void convert(List mainRows); + abstract R convert(T t); + protected PrefetchProcessorNode toResultsTree(ClassDescriptor descriptor, PrefetchTreeNode prefetchTree, List normalizedRows) { @@ -676,11 +791,30 @@ protected void performPostLoadCallbacks(PrefetchProcessorNode node, LifecycleCal } } - class SingleObjectConversionStrategy extends ObjectConversionStrategy { + class SingleObjectConversionStrategy extends ObjectConversionStrategy { @Override - void convert(List mainRows) { + public void convert(List mainRows) { + + PrefetchProcessorNode node = getPrefetchProcessorNode(mainRows); + List objects = node.getObjects(); + updateResponse(mainRows, objects != null ? objects : new ArrayList<>(1)); + + // apply POST_LOAD callback + LifecycleCallbackRegistry callbackRegistry = context.getEntityResolver().getCallbackRegistry(); + + if (!callbackRegistry.isEmpty(LifecycleEvent.POST_LOAD)) { + performPostLoadCallbacks(node, callbackRegistry); + } + } + + @Override + Object convert(DataRow dataRow) { + PrefetchProcessorNode node = getPrefetchProcessorNode(Collections.singletonList(dataRow)); + return node.getObjects().get(0); + } + private PrefetchProcessorNode getPrefetchProcessorNode(List mainRows) { PrefetchTreeNode prefetchTree = metadata.getPrefetchTree(); List rsMapping = metadata.getResultSetMapping(); @@ -693,28 +827,25 @@ void convert(List mainRows) { ? metadata.getClassDescriptor() : resultSegment.getClassDescriptor(); - PrefetchProcessorNode node = toResultsTree(descriptor, prefetchTree, mainRows); - List objects = node.getObjects(); - updateResponse(mainRows, objects != null ? objects : new ArrayList<>(1)); - - // apply POST_LOAD callback - LifecycleCallbackRegistry callbackRegistry = context.getEntityResolver().getCallbackRegistry(); - - if (!callbackRegistry.isEmpty(LifecycleEvent.POST_LOAD)) { - performPostLoadCallbacks(node, callbackRegistry); - } + return toResultsTree(descriptor, prefetchTree, mainRows); } } - class SingleScalarConversionStrategy extends ObjectConversionStrategy { + class SingleScalarConversionStrategy extends ObjectConversionStrategy { @Override void convert(List mainRows) { // noop... scalars require no further processing } + + @Override + Object convert(Object o) { + return o; + // noop... scalars require no further processing + } } - class SingleEmbeddableConversionStrategy extends ObjectConversionStrategy { + class SingleEmbeddableConversionStrategy extends ObjectConversionStrategy { @Override void convert(List mainRows) { @@ -734,9 +865,15 @@ void convert(List mainRows) { }); updateResponse(mainRows, result); } + + @Override + Object convert(DataRow dataRow) { + convert(Collections.singletonList(dataRow)); + return dataRow; + } } - class MixedConversionStrategy extends ObjectConversionStrategy { + class MixedConversionStrategy extends ObjectConversionStrategy { protected PrefetchProcessorNode toResultsTree(ClassDescriptor descriptor, PrefetchTreeNode prefetchTree, List rows, int position) { @@ -828,24 +965,38 @@ void convert(List mainRows) { } } } + + @Override + Object[] convert(Object[] objectsArray) { + List objects = new ArrayList<>(); + objects.add(objectsArray); + convert(objects); + return objectsArray; + } } - private class IdentityConversionStrategy extends ObjectConversionStrategy { + private class IdentityConversionStrategy extends ObjectConversionStrategy { @Override void convert(List mainRows) { + //noop + } + + @Override + public Object convert(Object object) { + return object; } } /** * Conversion strategy that uses mapper function to map raw result */ - private class MapperConversionStrategy extends ObjectConversionStrategy { + private class MapperConversionStrategy extends ObjectConversionStrategy { private final Function mapper; - private final ObjectConversionStrategy parentStrategy; + private final ObjectConversionStrategy parentStrategy; @SuppressWarnings({"unchecked", "rawtypes"}) - MapperConversionStrategy(ObjectConversionStrategy parentStrategy) { + MapperConversionStrategy(ObjectConversionStrategy parentStrategy) { this.mapper = (Function)metadata.getResultMapper(); this.parentStrategy = (ObjectConversionStrategy)parentStrategy; } @@ -855,6 +1006,11 @@ void convert(List mainRows) { parentStrategy.convert(mainRows); mainRows.replaceAll(mapper::apply); } + + @Override + Object convert(Object object) { + return mapper.apply(parentStrategy.convert(object)); + } } } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ResultIteratorConverterDecorator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ResultIteratorConverterDecorator.java new file mode 100644 index 0000000000..9c26e62eef --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ResultIteratorConverterDecorator.java @@ -0,0 +1,76 @@ +/***************************************************************** + * 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.ResultIterator; + +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + + class ResultIteratorConverterDecorator implements ResultIterator { + private final ResultIterator iterator; + private final DataDomainQueryAction.ObjectConversionStrategy converter; + + ResultIteratorConverterDecorator(ResultIterator iterator, DataDomainQueryAction.ObjectConversionStrategy converter) { + this.iterator = Objects.requireNonNull(iterator); + this.converter = Objects.requireNonNull(converter); + } + + @Override + public List allRows() { + return iterator.allRows(); + } + + @Override + public boolean hasNextRow() { + return iterator.hasNextRow(); + } + + @Override + public Object nextRow() { + return converter.convert(iterator.nextRow()); + } + + @Override + public void skipRow() { + iterator.skipRow(); + } + + @Override + public void close() { + iterator.close(); + } + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNextRow(); + } + + @Override + public Object next() { + return converter.convert(iterator.nextRow()); + } + }; + } + } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/IteratedQueryDecorator.java b/cayenne-server/src/main/java/org/apache/cayenne/query/IteratedQueryDecorator.java new file mode 100644 index 0000000000..9093bd7f93 --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/query/IteratedQueryDecorator.java @@ -0,0 +1,50 @@ +/***************************************************************** + * 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.query; + +import org.apache.cayenne.map.EntityResolver; + +public class IteratedQueryDecorator implements Query{ + + private final Query query; + + public IteratedQueryDecorator(Query query) { + this.query = query; + } + + @Override + public QueryMetadata getMetaData(EntityResolver resolver) { + return query.getMetaData(resolver); + } + + @Override + public void route(QueryRouter router, EntityResolver resolver, Query substitutedQuery) { + query.route(router,resolver,substitutedQuery); + } + + @Override + public SQLAction createSQLAction(SQLActionVisitor visitor) { + return query.createSQLAction(visitor); + } + + public Query getQuery() { + return query; + } +} diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/GenericResponse.java b/cayenne-server/src/main/java/org/apache/cayenne/util/GenericResponse.java index 01d352d939..018233357a 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/util/GenericResponse.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/util/GenericResponse.java @@ -24,6 +24,7 @@ import java.util.List; import org.apache.cayenne.QueryResponse; +import org.apache.cayenne.ResultIterator; /** * A simple serializable implementation of QueryResponse. @@ -79,6 +80,11 @@ public List firstList() { return null; } + @Override + public ResultIterator firstIterator() { + return null; + } + public int[] firstUpdateCount() { for (reset(); next();) { if (!isList()) { @@ -93,6 +99,11 @@ public List currentList() { return (List) results.get(currentIndex - 1); } + @Override + public ResultIterator currentIterator() { + return null; + } + public int[] currentUpdateCount() { return (int[]) results.get(currentIndex - 1); } @@ -101,6 +112,11 @@ public boolean isList() { return results.get(currentIndex - 1) instanceof List; } + @Override + public boolean isIterator() { + return false; + } + public boolean next() { return ++currentIndex <= results.size(); } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/IteratedQueryResponse.java b/cayenne-server/src/main/java/org/apache/cayenne/util/IteratedQueryResponse.java new file mode 100644 index 0000000000..961af918e6 --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/util/IteratedQueryResponse.java @@ -0,0 +1,90 @@ +/***************************************************************** + * 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.util; + +import org.apache.cayenne.ResultIterator; + +import java.util.List; + +/** + * Implementation of QueryResponse for iterated query. + * + * @since 5.0 + */ +public class IteratedQueryResponse extends GenericResponse { + private ResultIterator iterator; + public IteratedQueryResponse(ResultIterator iterator) { + this.iterator = iterator; + } + + public void setIterator(ResultIterator iterator) { + this.iterator = iterator; + } + + @Override + public int size() { + return -1; + } + + @Override + public boolean isList() { + return false; + } + + @Override + public boolean isIterator() { + return true; + } + + @Override + public List currentList() { + return null; + } + + @Override + public ResultIterator currentIterator() { + return iterator; + } + + @Override + public int[] currentUpdateCount() { + return new int[0]; + } + + @Override + public boolean next() { + return false; + } + + @Override + public List firstList() { + return null; + } + + @Override + public ResultIterator firstIterator() { + return iterator; + } + + @Override + public int[] firstUpdateCount() { + return new int[0]; + } +} diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/ListResponse.java b/cayenne-server/src/main/java/org/apache/cayenne/util/ListResponse.java index 9944ab82fe..30f878375c 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/util/ListResponse.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/util/ListResponse.java @@ -25,6 +25,7 @@ import java.util.List; import org.apache.cayenne.QueryResponse; +import org.apache.cayenne.ResultIterator; /** * A QueryResponse optimized to hold a single object or data row list. @@ -64,6 +65,11 @@ public boolean isList() { return true; } + @Override + public boolean isIterator() { + return false; + } + public List currentList() { if (currentIndex != 1) { throw new IndexOutOfBoundsException("Past iteration end: " + currentIndex); @@ -72,6 +78,11 @@ public List currentList() { return objectList; } + @Override + public ResultIterator currentIterator() { + return null; + } + public int[] currentUpdateCount() { throw new IllegalStateException("Current object is not an update count"); } @@ -89,6 +100,11 @@ public List firstList() { return objectList; } + @Override + public ResultIterator firstIterator() { + return null; + } + public int[] firstUpdateCount() { return new int[0]; } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java index 7d4c7cc1eb..5487a4b08f 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectContextQueryAction.java @@ -34,12 +34,7 @@ import org.apache.cayenne.cache.QueryCache; import org.apache.cayenne.cache.QueryCacheEntryFactory; import org.apache.cayenne.map.EntityInheritanceTree; -import org.apache.cayenne.query.EntityResultSegment; -import org.apache.cayenne.query.ObjectIdQuery; -import org.apache.cayenne.query.Query; -import org.apache.cayenne.query.QueryCacheStrategy; -import org.apache.cayenne.query.QueryMetadata; -import org.apache.cayenne.query.RelationshipQuery; +import org.apache.cayenne.query.*; import org.apache.cayenne.reflect.ArcProperty; import org.apache.cayenne.reflect.ClassDescriptor; @@ -87,12 +82,13 @@ public ObjectContextQueryAction(ObjectContext actingContext, * Worker method that performs internal query. */ public QueryResponse execute() { - - if (interceptOIDQuery() != DONE) { - if (interceptRelationshipQuery() != DONE) { - if (interceptRefreshQuery() != DONE) { - if (interceptLocalCache() != DONE) { - executePostCache(); + if (interceptIteratedQuery() != DONE) { + if (interceptOIDQuery() != DONE) { + if (interceptRelationshipQuery() != DONE) { + if (interceptRefreshQuery() != DONE) { + if (interceptLocalCache() != DONE) { + executePostCache(); + } } } } @@ -102,6 +98,14 @@ public QueryResponse execute() { return response; } + private boolean interceptIteratedQuery() { + if (query instanceof IteratedQueryDecorator) { + runQuery(); + return DONE; + } + return !DONE; + } + private void executePostCache() { if (interceptInternalQuery() != DONE) { if (interceptPaginatedQuery() != DONE) { diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIteratedQueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIteratedQueryIT.java index 8853c8c0d7..6aef0419d7 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIteratedQueryIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIteratedQueryIT.java @@ -185,6 +185,20 @@ public void testPerformIteratedQuery_resolve() throws Exception { } } + @Test + public void testContextIterator() throws Exception { + createArtistsAndPaintingsDataSet(); + try (ResultIterator it = context + .iterator(ObjectSelect.query(Artist.class))) { + while (it.hasNextRow()) { + Artist artist = it.nextRow(); + List paintings = artist.getPaintingArray(); + assertNotNull(paintings); + assertEquals("Expected one painting for artist: " + artist, 1, paintings.size()); + } + } + } + @Test public void testPerformIteratedQuery_CommitWithinIterator() throws Exception { createArtistsAndPaintingsDataSet(); diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/EmbeddingIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/EmbeddingIT.java index c01f36380d..e02f218f71 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/access/EmbeddingIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/access/EmbeddingIT.java @@ -21,6 +21,7 @@ import org.apache.cayenne.DataRow; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.PersistenceState; +import org.apache.cayenne.ResultBatchIterator; import org.apache.cayenne.di.Inject; import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.test.jdbc.DBHelper; @@ -360,4 +361,14 @@ public void testInsertWithInheritance() { context.commitChanges(); } + + @Test + public void testQueryWithBatchIterator() throws Exception { + createSelectDataSet2(); + try (ResultBatchIterator iterator = ObjectSelect.query(EmbedEntity1.class) + .batchIterator(context, 2)) { + assertNotNull(iterator.next().get(0).getEmbedded2()); + } + + } } diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/MappedQueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/MappedQueryIT.java index 03f95a5457..9fe1254578 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/query/MappedQueryIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/query/MappedQueryIT.java @@ -20,8 +20,10 @@ import org.apache.cayenne.DataRow; import org.apache.cayenne.QueryResponse; +import org.apache.cayenne.ResultBatchIterator; import org.apache.cayenne.access.DataContext; import org.apache.cayenne.di.Inject; +import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.log.JdbcEventLogger; import org.apache.cayenne.test.jdbc.DBHelper; import org.apache.cayenne.test.jdbc.TableHelper; @@ -78,6 +80,29 @@ public void testSelectQuery() throws Exception { assertEquals("artist14", a.getArtistName()); } + @Test + public void testButchIterator() throws Exception { + createArtistsDataSet(); + + try (ResultBatchIterator iterator = MappedSelect + .query("ParameterizedQueryWithSharedCache", Artist.class) + .param("name", "artist14") + .batchIterator(context, 1)) { + int count = 0; + while (iterator.hasNext()) { + count++; + List artists = iterator.next(); + for (Artist artist : artists) { + //noinspection ConstantConditions + assertTrue(artist instanceof Artist); + assertEquals("artist14", artist.readPropertyDirectly("artistName")); + } + } + assertEquals(1,count); + } + + } + @Test public void testSQLTemplateSelect() throws Exception { createArtistsDataSet(); diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectIteratedQueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectIteratedQueryIT.java new file mode 100644 index 0000000000..1f40c02057 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectIteratedQueryIT.java @@ -0,0 +1,376 @@ +/***************************************************************** + * 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.query; + +import org.apache.cayenne.CayenneRuntimeException; +import org.apache.cayenne.DataRow; +import org.apache.cayenne.ResultBatchIterator; +import org.apache.cayenne.ResultIterator; +import org.apache.cayenne.access.DataContext; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.test.jdbc.DBHelper; +import org.apache.cayenne.test.jdbc.TableHelper; +import org.apache.cayenne.testdo.testmap.Painting; +import org.apache.cayenne.unit.di.server.CayenneProjects; +import org.apache.cayenne.unit.di.server.ServerCase; +import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) +public class ObjectSelectIteratedQueryIT extends ServerCase { + + @Inject + private DataContext context; + + @Inject + private DBHelper dbHelper; + + private TableHelper tPainting; + + private TableHelper tArtist; + + + @Before + public void before() throws Exception { + tPainting = new TableHelper(dbHelper, "PAINTING") + .setColumns("PAINTING_ID", "PAINTING_TITLE", "ESTIMATED_PRICE", "ARTIST_ID") + .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.DECIMAL, Types.INTEGER); + + tArtist = new TableHelper(dbHelper, "ARTIST") + .setColumns("ARTIST_ID", "ARTIST_NAME"); + + createArtistDataSet(); + createPaintingsDataSet(); + + } + + private void createPaintingsDataSet() throws Exception { + for (int i = 1; i <= 20; i++) { + tPainting.insert(i, "painting" + i, 10000. * i, 1); + } + } + + private void createArtistDataSet() throws SQLException { + tArtist.insert(1, "Test1"); + tArtist.insert(2, "Test2"); + } + + @Test + public void prefetchWithBatchIterator() { + try (ResultBatchIterator iterator = ObjectSelect + .query(Painting.class) + .prefetch(Painting.TO_ARTIST.joint()) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .batchIterator(context, 10)) { + int count = 0; + while (iterator.hasNext()) { + count++; + List paintings = iterator.next(); + for (Painting painting : paintings) { + //noinspection ConstantConditions + assertTrue(painting instanceof Painting); + assertEquals("Test1", painting.getToArtist().readPropertyDirectly("artistName")); + } + } + assertEquals(2, count); + } + } + + @Test + public void queryPrefetchJointWithIterator() { + try (ResultIterator iterator = ObjectSelect + .query(Painting.class) + .prefetch(Painting.TO_ARTIST.joint()) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .iterator(context)) { + int count = 0; + while (iterator.hasNextRow()) { + count++; + Painting painting = iterator.nextRow(); + //noinspection ConstantConditions + assertTrue(painting instanceof Painting); + assertEquals("Test1", painting.getToArtist().readPropertyDirectly("artistName")); + } + assertEquals(20, count); + } + } + + @Test(expected = CayenneRuntimeException.class) + public void queryPrefetchDisjointWithIterator() { + try (ResultIterator iterator = ObjectSelect + .query(Painting.class) + .prefetch(Painting.TO_ARTIST.disjoint()) + .iterator(context)) { + iterator.nextRow(); + } + } + + + @Test(expected = CayenneRuntimeException.class) + public void queryPrefetchDisjointWithBatchIterator() { + try (ResultBatchIterator iterator = ObjectSelect + .query(Painting.class) + .prefetch(Painting.TO_ARTIST.disjoint()) + .batchIterator(context, 3)) { + iterator.next(); + } + } + + @Ignore + @Test(expected = CayenneRuntimeException.class) + public void queryPaginationWithBatchIterator() { + try (ResultBatchIterator iterator = ObjectSelect + .query(Painting.class) + .pageSize(2) + .batchIterator(context, 3)) { + iterator.next(); + } + } + + @Test + public void queryPrefetchDisjointByIdWithBIterator() { + try (ResultIterator iterator = ObjectSelect + .query(Painting.class) + .prefetch(Painting.TO_ARTIST.disjointById()) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .iterator(context)) { + int count = 0; + while (iterator.hasNextRow()) { + count++; + Painting painting = iterator.nextRow(); + //noinspection ConstantConditions + assertTrue(painting instanceof Painting); + assertEquals("Test1", painting.getToArtist().readPropertyDirectly("artistName")); + } + assertEquals(20, count); + } + } + + @Test + public void queryPrefetchJointWithBatchIterator() { + try (ResultBatchIterator iterator = ObjectSelect + .query(Painting.class, "Painting") + .prefetch(Painting.TO_ARTIST.joint()) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .batchIterator(context, 5)) { + int count = 0; + int paintingCounter = 0; + while (iterator.hasNext()) { + count++; + List paintingList = iterator.next(); + for (Painting painting : paintingList) { + paintingCounter++; + assertEquals("painting" + paintingCounter, painting.getPaintingTitle()); + assertEquals("Test1", painting.getToArtist().readPropertyDirectly("artistName")); + } + } + assertEquals(4, count); + } + } + + @Test + public void QueryWithIterator() { + try (ResultIterator iterator = ObjectSelect + .query(Painting.class, "Painting") + .prefetch(Painting.TO_ARTIST.joint()) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .iterator(context)) { + int count = 0; + int paintingCounter = 0; + while (iterator.hasNextRow()) { + count++; + Painting painting = iterator.nextRow(); + paintingCounter++; + assertEquals("painting" + paintingCounter, painting.getPaintingTitle()); + assertEquals("Test1", painting.getToArtist().readPropertyDirectly("artistName")); + } + assertEquals(20, count); + } + } + + @Test + public void mappingWithBatchIterator() { + try (ResultBatchIterator iterator = ObjectSelect + .columnQuery(Painting.class, Painting.PAINTING_TITLE, Painting.ESTIMATED_PRICE) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .map(this::toDto) + .batchIterator(context, 5)) { + int count = 0; + while (iterator.hasNext()) { + count++; + List dtos = iterator.next(); + for (DTO dto : dtos) { + //noinspection ConstantConditions + assertTrue(dto instanceof DTO); + assertTrue(dto.getTitle().contains("dto_painting")); + } + } + assertEquals(5, iterator.getBatchSize()); + assertEquals(4, count); + } + } + + @Test + public void mappingWithIterator() { + try (ResultIterator iterator = ObjectSelect + .columnQuery(Painting.class, Painting.PAINTING_TITLE, Painting.ESTIMATED_PRICE) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .map(this::toDto) + .iterator(context)) { + int count = 0; + while (iterator.hasNextRow()) { + count++; + DTO dto = iterator.nextRow(); + //noinspection ConstantConditions + assertTrue(dto instanceof DTO); + assertTrue(dto.getTitle().contains("dto_painting")); + } + assertEquals(20, count); + } + } + + @Test + public void dataRowQueryWithBatchIterator() { + try (ResultBatchIterator iterator = ObjectSelect + .dataRowQuery(Painting.class) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .batchIterator(context, 5)) { + int count = 0; + int paintingCounter = 0; + while (iterator.hasNext()) { + count++; + List rows = iterator.next(); + for (Object row : rows) { + paintingCounter++; + Painting painting = context.objectFromDataRow(Painting.class, (DataRow) row); + assertEquals("painting" + paintingCounter, painting.getPaintingTitle()); + } + } + assertEquals(4, count); + } + } + + @Test + public void dataRowQueryWithIterator() { + try (ResultIterator iterator = ObjectSelect + .dataRowQuery(Painting.class) + .orderBy(Painting.PAINTING_ID_PK_PROPERTY.asc()) + .iterator(context)) { + int count = 0; + int paintingCounter = 0; + while (iterator.hasNextRow()) { + count++; + paintingCounter++; + Object row = iterator.nextRow(); + Painting painting = context.objectFromDataRow(Painting.class, (DataRow) row); + assertEquals("painting" + paintingCounter, painting.getPaintingTitle()); + } + assertEquals(20, count); + } + } + + @Test + public void dbQueryWithIterator() { + try (ResultIterator iterator = ObjectSelect + .dbQuery("PAINTING") + .orderBy("db:" + Painting.PAINTING_ID_PK_COLUMN) + .iterator(context)) { + int count = 0; + int paintingCounter = 0; + while (iterator.hasNextRow()) { + count++; + paintingCounter++; + Object row = iterator.nextRow(); + Painting painting = context.objectFromDataRow(Painting.class, (DataRow) row); + assertEquals("painting" + paintingCounter, painting.getPaintingTitle()); + } + assertEquals(20, count); + } + } + + @Test + public void dbQueryWithBatchIterator() { + try (ResultBatchIterator iterator = ObjectSelect + .dbQuery("PAINTING") + .orderBy("db:" + Painting.PAINTING_ID_PK_COLUMN) + .batchIterator(context, 5)) { + int count = 0; + int paintingCounter = 0; + while (iterator.hasNext()) { + count++; + List rows = iterator.next(); + for (Object row : rows) { + paintingCounter++; + Painting painting = context.objectFromDataRow(Painting.class, (DataRow) row); + assertEquals("painting" + paintingCounter, painting.getPaintingTitle()); + } + } + assertEquals(4, count); + } + } + + + @Test + public void unclosedTransactionsTest() { + for (int i = 0; i < 10; i++) { + mappingWithBatchIterator(); + } + } + + DTO toDto(Object[] data) { + return new DTO(data); + } + + + static class DTO { + private final String title; + private final Long estimatedPrice; + + public DTO(Object[] data) { + this.title = "dto_" + data[0]; + this.estimatedPrice = ((Number) data[1]).longValue(); + } + + public String getTitle() { + return title; + } + + public Long getEstimatedPrice() { + return estimatedPrice; + } + + @Override + public String toString() { + return "DTO{" + + "title='" + title + '\'' + + ", estimatedPrice=" + estimatedPrice + + '}'; + } + } +} diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SQLSelectIteratedQueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLSelectIteratedQueryIT.java new file mode 100644 index 0000000000..09fa51ab37 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SQLSelectIteratedQueryIT.java @@ -0,0 +1,267 @@ +/***************************************************************** + * 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.query; + +import org.apache.cayenne.DataRow; +import org.apache.cayenne.ResultBatchIterator; +import org.apache.cayenne.ResultIterator; +import org.apache.cayenne.access.DataContext; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.test.jdbc.DBHelper; +import org.apache.cayenne.test.jdbc.TableHelper; +import org.apache.cayenne.testdo.testmap.Painting; +import org.apache.cayenne.unit.di.server.CayenneProjects; +import org.apache.cayenne.unit.di.server.ServerCase; +import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.junit.Before; +import org.junit.Test; + +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) +public class SQLSelectIteratedQueryIT extends ServerCase { + + @Inject + private DataContext context; + + @Inject + private DBHelper dbHelper; + + private TableHelper tPainting; + + private TableHelper tArtist; + + + @Before + public void before() throws Exception { + tPainting = new TableHelper(dbHelper, "PAINTING") + .setColumns("PAINTING_ID", "PAINTING_TITLE", "ESTIMATED_PRICE", "ARTIST_ID") + .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.DECIMAL, Types.INTEGER); + + tArtist = new TableHelper(dbHelper, "ARTIST") + .setColumns("ARTIST_ID", "ARTIST_NAME"); + + createArtistDataSet(); + createPaintingsDataSet(); + + } + + private void createPaintingsDataSet() throws Exception { + for (int i = 1; i <= 20; i++) { + tPainting.insert(i, "painting" + i, 10000. * i, 1); + } + } + + private void createArtistDataSet() throws SQLException { + tArtist.insert(1, "Test1"); + tArtist.insert(2, "Test2"); + } + + @Test + public void scalarQueryWithBatchIterator() { + try (ResultBatchIterator iterator = SQLSelect + .scalarQuery("SELECT PAINTING_ID FROM PAINTING ORDER BY PAINTING_ID", Integer.class) + .batchIterator(context, 5)) { + int count = 0; + Integer expectedId = 0; + while (iterator.hasNext()) { + count++; + List ids = iterator.next(); + for (Integer id : ids) { + expectedId++; + assertEquals(expectedId, id); + } + } + assertEquals(4, count); + } + } + + @Test + public void scalarQueryWithIterator() { + try (ResultIterator iterator = SQLSelect + .scalarQuery("SELECT PAINTING_ID FROM PAINTING ORDER BY PAINTING_ID", Integer.class) + .iterator(context)) { + int count = 0; + Integer expectedId = 0; + while (iterator.hasNextRow()) { + count++; + expectedId++; + Integer id = iterator.nextRow(); + assertEquals(expectedId, id); + } + assertEquals(20, count); + } + } + + @Test + public void dataRowQueryWithBatchIterator() { + try (ResultBatchIterator iterator = SQLSelect + .dataRowQuery("SELECT * FROM PAINTING ORDER BY PAINTING_ID") + .upperColumnNames() + .batchIterator(context, 5)) { + int count = 0; + while (iterator.hasNext()) { + count++; + List rows = iterator.next(); + for (DataRow row : rows) { + assertEquals("painting" + row.get(Painting.PAINTING_ID_PK_COLUMN), + row.get("PAINTING_TITLE")); + } + } + assertEquals(4, count); + } + } + + @Test + public void dataRowQueryWithIterator() { + try (ResultIterator iterator = SQLSelect + .dataRowQuery("SELECT * FROM PAINTING ORDER BY PAINTING_ID") + .upperColumnNames() + .iterator(context)) { + int count = 0; + while (iterator.hasNextRow()) { + count++; + DataRow row = iterator.nextRow(); + assertEquals("painting" + row.get(Painting.PAINTING_ID_PK_COLUMN), + row.get("PAINTING_TITLE")); + } + assertEquals(20, count); + } + } + + @Test + public void queryWithBatchIterator() { + try (ResultBatchIterator iterator = SQLSelect + .query(Painting.class,"SELECT * FROM PAINTING ORDER BY PAINTING_ID") + .upperColumnNames() + .batchIterator(context, 5)) { + int count = 0; + int paintingCounter = 0; + while (iterator.hasNext()) { + count++; + List paintingList = iterator.next(); + for (Painting painting : paintingList) { + paintingCounter++; + assertEquals("painting" + paintingCounter, painting.getPaintingTitle()); + } + } + assertEquals(4, count); + } + } + + @Test + public void queryWithIterator() { + try (ResultIterator iterator = SQLSelect + .query(Painting.class, "SELECT * FROM PAINTING ORDER BY PAINTING_ID") + .upperColumnNames() + .iterator(context)) { + int count = 0; + int paintingCounter = 0; + while (iterator.hasNextRow()) { + count++; + Painting painting = iterator.nextRow(); + paintingCounter++; + assertEquals("painting" + paintingCounter, painting.getPaintingTitle()); + } + assertEquals(20, count); + } + } + + //MapperConversationStrategy + //MixedConversationStrategy + @Test + public void mappingWithBatchIterator() { + try (ResultBatchIterator iterator = SQLSelect + .columnQuery( "SELECT PAINTING_TITLE, ESTIMATED_PRICE FROM PAINTING ORDER BY PAINTING_ID") + .map(this::toDto) + .batchIterator(context, 5)) { + int count = 0; + while (iterator.hasNext()) { + count++; + List dtoList = iterator.next(); + for (DTO dto : dtoList) { + //noinspection ConstantConditions + assertTrue(dto instanceof DTO); + assertTrue(dto.getTitle().contains("dto_painting")); + } + } + assertEquals(5, iterator.getBatchSize()); + assertEquals(4,count); + } + } + + //MapperConversationStrategy + //MixedConversationStrategy + @Test + public void mappingWithIterator() { + try (ResultIterator iterator = SQLSelect + .columnQuery("SELECT PAINTING_TITLE, ESTIMATED_PRICE FROM PAINTING ORDER BY PAINTING_ID") + .upperColumnNames() + .map(this::toDto) + .iterator(context)) { + int count = 0; + while (iterator.hasNextRow()) { + count++; + DTO dto = iterator.nextRow(); + //noinspection ConstantConditions + assertTrue(dto instanceof DTO); + assertTrue(dto.getTitle().contains("dto_painting")); + } + assertEquals(20, count); + } + } + + + DTO toDto(Object[] data) { + return new DTO(data); + } + + + static class DTO { + private final String title; + private final Long estimatedPrice; + + public DTO(Object[] data) { + this.title = "dto_" + data[0]; + this.estimatedPrice = ((Number) data[1]).longValue(); + } + + public String getTitle() { + return title; + } + + public Long getEstimatedPrice() { + return estimatedPrice; + } + + @Override + public String toString() { + return "DTO{" + + "title='" + title + '\'' + + ", estimatedPrice=" + estimatedPrice + + '}'; + } + } +} diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/SelectByIdIteratedQueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectByIdIteratedQueryIT.java new file mode 100644 index 0000000000..6011f5213b --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/query/SelectByIdIteratedQueryIT.java @@ -0,0 +1,151 @@ +/***************************************************************** + * 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.query; + +import org.apache.cayenne.Cayenne; +import org.apache.cayenne.DataRow; +import org.apache.cayenne.ResultBatchIterator; +import org.apache.cayenne.ResultIterator; +import org.apache.cayenne.access.DataContext; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.test.jdbc.DBHelper; +import org.apache.cayenne.test.jdbc.TableHelper; +import org.apache.cayenne.testdo.testmap.Painting; +import org.apache.cayenne.unit.di.server.CayenneProjects; +import org.apache.cayenne.unit.di.server.ServerCase; +import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.junit.Before; +import org.junit.Test; + +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; + + +@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) +public class SelectByIdIteratedQueryIT extends ServerCase { + + @Inject + private DataContext context; + + @Inject + private DBHelper dbHelper; + + private TableHelper tPainting; + + private TableHelper tArtist; + + + @Before + public void before() throws Exception { + tPainting = new TableHelper(dbHelper, "PAINTING") + .setColumns("PAINTING_ID", "PAINTING_TITLE", "ESTIMATED_PRICE", "ARTIST_ID") + .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.DECIMAL, Types.INTEGER); + + tArtist = new TableHelper(dbHelper, "ARTIST") + .setColumns("ARTIST_ID", "ARTIST_NAME"); + + createArtistDataSet(); + createPaintingsDataSet(); + + } + + private void createPaintingsDataSet() throws Exception { + for (int i = 1; i <= 20; i++) { + tPainting.insert(i, "painting" + i, 10000. * i, 1); + } + } + + private void createArtistDataSet() throws SQLException { + tArtist.insert(1, "Test1"); + tArtist.insert(2, "Test2"); + } + + @Test + public void queryWithButchIterator() { + try (ResultBatchIterator iterator = SelectById + .query(Painting.class, Arrays.asList(1,2,3,4,5,6)) + .batchIterator(context, 4)){ + int count = 0; + while (iterator.hasNext()) { + count++; + List paintingList = iterator.next(); + for (Painting painting : paintingList) { + assertEquals("painting" + Cayenne.longPKForObject(painting), + painting.getPaintingTitle()); + } + } + assertEquals(2, count); + } + } + + @Test + public void queryWithIterator() { + try (ResultIterator iterator = SelectById + .query(Painting.class, Arrays.asList(1,2,3,4,5,6)) + .iterator(context)){ + int count = 0; + while (iterator.hasNextRow()) { + count++; + Painting painting = iterator.nextRow(); + assertEquals("painting" + Cayenne.longPKForObject(painting), + painting.getPaintingTitle()); + } + assertEquals(6, count); + } + } + + @Test + public void dataRowQueryWithBatchIterator() { + try (ResultBatchIterator iterator = SelectById + .dataRowQuery(Painting.class, 1, 2,3,4,5,6) + .batchIterator(context, 4)) { + int count = 0; + while (iterator.hasNext()) { + count++; + List rows = iterator.next(); + for (DataRow row : rows) { + assertEquals("painting" + row.get(Painting.PAINTING_ID_PK_COLUMN), + row.get("PAINTING_TITLE")); + } + } + assertEquals(2, count); + } + } + + @Test + public void dataRowQueryWithIterator() { + try (ResultIterator iterator = SelectById + .dataRowQuery(Painting.class, 1, 2,3,4,5,6) + .iterator(context)) { + int count = 0; + while (iterator.hasNextRow()) { + count++; + DataRow row = iterator.nextRow(); + assertEquals("painting" + row.get(Painting.PAINTING_ID_PK_COLUMN), + row.get("PAINTING_TITLE")); + } + assertEquals(6, count); + } + } + +}