CAY-1959 Chainable API for SelectQuery
Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/a0f941a0 Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/a0f941a0 Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/a0f941a0 Branch: refs/heads/CAY-1946 Commit: a0f941a02bdc3320e38fd830983b5f034a4a306f Parents: 5f7b6dd Author: aadamchik <aadamc...@apache.org> Authored: Sat Nov 15 13:08:20 2014 +0300 Committer: aadamchik <aadamc...@apache.org> Committed: Sat Nov 15 13:25:11 2014 +0300 ---------------------------------------------------------------------- .../java/org/apache/cayenne/exp/Property.java | 19 +- .../apache/cayenne/query/BaseQueryMetadata.java | 2 +- .../org/apache/cayenne/query/ObjectSelect.java | 668 ++++++++++++++++++ .../java/org/apache/cayenne/query/Ordering.java | 696 ++++++++++--------- .../apache/cayenne/query/PrefetchTreeNode.java | 15 + .../org/apache/cayenne/query/SelectQuery.java | 10 +- .../cayenne/remote/IncrementalSelectQuery.java | 5 +- .../apache/cayenne/query/ObjectSelectTest.java | 482 +++++++++++++ .../cayenne/query/ObjectSelect_CompileIT.java | 168 +++++ .../cayenne/query/ObjectSelect_RunIT.java | 94 +++ docs/doc/src/main/resources/RELEASE-NOTES.txt | 1 + 11 files changed, 1805 insertions(+), 355 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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 48f1549..b42e890 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 @@ -385,9 +385,7 @@ public class Property<E> { * prefetch semantics. */ public PrefetchTreeNode joint() { - PrefetchTreeNode node = prefetch(); - node.setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS); - return node.getRoot(); + return PrefetchTreeNode.withPath(name, PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS); } /** @@ -396,9 +394,7 @@ public class Property<E> { * "disjoint" prefetch semantics. */ public PrefetchTreeNode disjoint() { - PrefetchTreeNode node = prefetch(); - node.setSemantics(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS); - return node.getRoot(); + return PrefetchTreeNode.withPath(name, PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS); } /** @@ -407,16 +403,7 @@ public class Property<E> { * "disjoint by id" prefetch semantics. */ public PrefetchTreeNode disjointById() { - PrefetchTreeNode node = prefetch(); - node.setSemantics(PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS); - return node.getRoot(); - } - - PrefetchTreeNode prefetch() { - PrefetchTreeNode root = new PrefetchTreeNode(); - PrefetchTreeNode node = root.addPath(name); - node.setPhantom(false); - return node; + return PrefetchTreeNode.withPath(name, PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS); } /** http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java b/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java index 1f17aac..d85b85a 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java @@ -44,7 +44,7 @@ import org.apache.cayenne.util.XMLSerializable; class BaseQueryMetadata implements QueryMetadata, XMLSerializable, Serializable { private static final long serialVersionUID = 5129792493303459115L; - + int fetchLimit = QueryMetadata.FETCH_LIMIT_DEFAULT; int fetchOffset = QueryMetadata.FETCH_OFFSET_DEFAULT; http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/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 new file mode 100644 index 0000000..7618bcc --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java @@ -0,0 +1,668 @@ +/***************************************************************** + * 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.query; + +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.cayenne.CayenneRuntimeException; +import org.apache.cayenne.DataRow; +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.EntityResolver; +import org.apache.cayenne.map.ObjEntity; + +/** + * A selecting query providing chainable API. Can be viewed as an alternative to + * {@link SelectQuery}. + * + * @since 4.0 + */ +public class ObjectSelect<T> extends IndirectQuery implements Select<T> { + + private static final long serialVersionUID = -156124021150949227L; + + private boolean fetchingDataRows; + + private Class<?> entityType; + private String entityName; + private String dbEntityName; + private Expression exp; + private Collection<Ordering> orderings; + private PrefetchTreeNode prefetches; + private int limit; + private int offset; + private int pageSize; + private int statementFetchSize; + private QueryCacheStrategy cacheStrategy; + private String[] cacheGroups; + + /** + * Creates a ObjectSelect that selects objects of a given persistent class. + */ + public static <T> ObjectSelect<T> query(Class<T> entityType) { + return new ObjectSelect<T>().entityType(entityType); + } + + /** + * Creates a ObjectSelect that selects objects of a given persistent class + * and uses provided expression for its qualifier. + */ + public static <T> ObjectSelect<T> query(Class<T> entityType, Expression expression) { + return new ObjectSelect<T>().entityType(entityType).exp(expression); + } + + /** + * Creates a ObjectSelect that selects objects of a given persistent class + * and uses provided expression for its qualifier. + */ + public static <T> ObjectSelect<T> query(Class<T> entityType, Expression expression, List<Ordering> orderings) { + return new ObjectSelect<T>().entityType(entityType).exp(expression).orderBy(orderings); + } + + /** + * Creates a ObjectSelect that fetches data for an {@link ObjEntity} + * determined from a provided class. + */ + public static ObjectSelect<DataRow> dataRowQuery(Class<?> entityType) { + return query(entityType).fetchDataRows(); + } + + /** + * Creates a ObjectSelect that fetches data for an {@link ObjEntity} + * determined from a provided class and uses provided expression for its + * qualifier. + */ + public static ObjectSelect<DataRow> dataRowQuery(Class<?> entityType, Expression expression) { + return query(entityType).fetchDataRows().exp(expression); + } + + /** + * Creates a ObjectSelect that fetches data for {@link ObjEntity} determined + * from provided "entityName", but fetches the result of a provided type. + * This factory method is most often used with generic classes that by + * themselves are not enough to resolve the entity to fetch. + */ + public static <T> ObjectSelect<T> query(Class<T> resultType, String entityName) { + return new ObjectSelect<T>().entityName(entityName); + } + + /** + * Creates a ObjectSelect that fetches DataRows for a {@link DbEntity} + * determined from provided "dbEntityName". + */ + public static ObjectSelect<DataRow> dbQuery(String dbEntityName) { + return new ObjectSelect<Object>().fetchDataRows().dbEntityName(dbEntityName); + } + + /** + * Creates a ObjectSelect that fetches DataRows for a {@link DbEntity} + * determined from provided "dbEntityName" and uses provided expression for + * its qualifier. + * + * @return this object + */ + public static ObjectSelect<DataRow> dbQuery(String dbEntityName, Expression expression) { + return new ObjectSelect<Object>().fetchDataRows().dbEntityName(dbEntityName).exp(expression); + } + + protected ObjectSelect() { + } + + /** + * Translates self to a SelectQuery. + */ + @SuppressWarnings({ "deprecation", "unchecked" }) + @Override + protected Query createReplacementQuery(EntityResolver resolver) { + + @SuppressWarnings("rawtypes") + SelectQuery replacement = new SelectQuery(); + + if (entityType != null) { + replacement.setRoot(entityType); + } else if (entityName != null) { + + ObjEntity entity = resolver.getObjEntity(entityName); + if (entity == null) { + throw new CayenneRuntimeException("Unrecognized ObjEntity name: " + entityName); + } + + replacement.setRoot(entity); + } else if (dbEntityName != null) { + + DbEntity entity = resolver.getDbEntity(dbEntityName); + if (entity == null) { + throw new CayenneRuntimeException("Unrecognized DbEntity name: " + dbEntityName); + } + + replacement.setRoot(entity); + } else { + throw new CayenneRuntimeException("Undefined root entity of the query"); + } + + replacement.setFetchingDataRows(fetchingDataRows); + replacement.setQualifier(exp); + replacement.addOrderings(orderings); + replacement.setPrefetchTree(prefetches); + replacement.setCacheStrategy(cacheStrategy); + replacement.setCacheGroups(cacheGroups); + replacement.setFetchLimit(limit); + replacement.setFetchOffset(offset); + replacement.setPageSize(pageSize); + replacement.setStatementFetchSize(statementFetchSize); + + return replacement; + } + + /** + * Sets the type of the entity to fetch without changing the return type of + * the query. + * + * @return this object + */ + public ObjectSelect<T> entityType(Class<?> entityType) { + return resetEntity(entityType, null, null); + } + + /** + * Sets the {@link ObjEntity} name to fetch without changing the return type + * of the query. This form is most often used for generic entities that + * don't map to a distinct class. + * + * @return this object + */ + public ObjectSelect<T> entityName(String entityName) { + return resetEntity(null, entityName, null); + } + + /** + * Sets the {@link DbEntity} name to fetch without changing the return type + * of the query. This form is most often used for generic entities that + * don't map to a distinct class. + * + * @return this object + */ + public ObjectSelect<T> dbEntityName(String dbEntityName) { + return resetEntity(null, null, dbEntityName); + } + + private ObjectSelect<T> resetEntity(Class<?> entityType, String entityName, String dbEntityName) { + this.entityType = entityType; + this.entityName = entityName; + this.dbEntityName = dbEntityName; + return this; + } + + /** + * Forces query to fetch DataRows. This automatically changes whatever + * result type was set previously to "DataRow". + * + * @return this object + */ + @SuppressWarnings("unchecked") + public ObjectSelect<DataRow> fetchDataRows() { + this.fetchingDataRows = true; + return (ObjectSelect<DataRow>) this; + } + + /** + * Initializes or resets a qualifier expression of this query. + * + * @return this object + */ + public ObjectSelect<T> exp(Expression expression) { + this.exp = expression; + return this; + } + + /** + * Initializes or resets a qualifier expression of this query, using + * provided expression String and an array of position parameters. + * + * @return this object + */ + public ObjectSelect<T> exp(String expressionString, Object... parameters) { + this.exp = ExpressionFactory.exp(expressionString, parameters); + return this; + } + + /** + * AND's provided expressions to the existing qualifier expression. + * + * @return this object + */ + public ObjectSelect<T> and(Expression... expressions) { + if (expressions == null || expressions.length == 0) { + return this; + } + + return and(Arrays.asList(expressions)); + } + + /** + * AND's provided expressions to the existing qualifier expression. + * + * @return this object + */ + public ObjectSelect<T> and(Collection<Expression> expressions) { + + if (expressions == null || expressions.isEmpty()) { + return this; + } + + Collection<Expression> all; + + if (exp != null) { + all = new ArrayList<Expression>(expressions.size() + 1); + all.add(exp); + all.addAll(expressions); + } else { + all = expressions; + } + + exp = ExpressionFactory.and(all); + return this; + } + + /** + * OR's provided expressions to the existing qualifier expression. + * + * @return this object + */ + public ObjectSelect<T> or(Expression... expressions) { + if (expressions == null || expressions.length == 0) { + return this; + } + + return or(Arrays.asList(expressions)); + } + + /** + * OR's provided expressions to the existing qualifier expression. + * + * @return this object + */ + public ObjectSelect<T> or(Collection<Expression> expressions) { + if (expressions == null || expressions.isEmpty()) { + return this; + } + + Collection<Expression> all; + + if (exp != null) { + all = new ArrayList<Expression>(expressions.size() + 1); + all.add(exp); + all.addAll(expressions); + } else { + all = expressions; + } + + exp = ExpressionFactory.or(all); + return this; + } + + /** + * Initializes ordering clause of this query with a single ascending + * ordering on a given property. + * + * @return this object + */ + public ObjectSelect<T> orderBy(String property) { + return orderBy(new Ordering(property)); + } + + /** + * Initializes ordering clause of this query with a single ordering on a + * given property. + * + * @return this object + */ + public ObjectSelect<T> orderBy(String property, SortOrder sortOrder) { + return orderBy(new Ordering(property, sortOrder)); + } + + /** + * Initializes or resets a list of orderings of this query. + * + * @return this object + */ + public ObjectSelect<T> orderBy(Ordering... orderings) { + + if (this.orderings != null) { + this.orderings.clear(); + } + + return addOrderBy(orderings); + } + + /** + * Initializes or resets a list of orderings of this query. + * + * @return this object + */ + public ObjectSelect<T> orderBy(Collection<Ordering> orderings) { + + if (this.orderings != null) { + this.orderings.clear(); + } + + return addOrderBy(orderings); + } + + /** + * Adds a single ascending ordering on a given property to the existing + * ordering clause of this query. + * + * @return this object + */ + public ObjectSelect<T> addOrderBy(String property) { + return addOrderBy(new Ordering(property)); + } + + /** + * Adds a single ordering on a given property to the existing ordering + * clause of this query. + * + * @return this object + */ + public ObjectSelect<T> addOrderBy(String property, SortOrder sortOrder) { + return addOrderBy(new Ordering(property, sortOrder)); + } + + /** + * Adds new orderings to the list of the existing orderings. + * + * @return this object + */ + public ObjectSelect<T> addOrderBy(Ordering... orderings) { + + if (orderings == null || orderings == null) { + return this; + } + + if (this.orderings == null) { + this.orderings = new ArrayList<Ordering>(orderings.length); + } + + for (Ordering o : orderings) { + this.orderings.add(o); + } + + return this; + } + + /** + * Adds new orderings to the list of orderings. + * + * @return this object + */ + public ObjectSelect<T> addOrderBy(Collection<Ordering> orderings) { + + if (orderings == null || orderings == null) { + return this; + } + + if (this.orderings == null) { + this.orderings = new ArrayList<Ordering>(orderings.size()); + } + + this.orderings.addAll(orderings); + + return this; + } + + /** + * Resets internal prefetches to the new value, which is a single prefetch + * with specified semantics. + * + * @return this object + */ + public ObjectSelect<T> prefetch(String path, int semantics) { + this.prefetches = PrefetchTreeNode.withPath(path, semantics); + return this; + } + + /** + * Resets internal prefetches to the new value. + * + * @return this object + */ + public ObjectSelect<T> prefetch(PrefetchTreeNode prefetch) { + this.prefetches = prefetch; + return this; + } + + /** + * Merges prefetch into the query prefetch tree. + * + * @return this object + */ + public ObjectSelect<T> addPrefetch(PrefetchTreeNode prefetch) { + + if (prefetch == null) { + return this; + } + + if (prefetches == null) { + prefetches = new PrefetchTreeNode(); + } + + prefetches.merge(prefetch); + return this; + } + + /** + * Merges a prefetch path with specified semantics into the query prefetch + * tree. + * + * @return this object + */ + public ObjectSelect<T> addPrefetch(String path, int semantics) { + + if (path == null) { + return this; + } + + if (prefetches == null) { + prefetches = new PrefetchTreeNode(); + } + + prefetches.addPath(path).setSemantics(semantics); + return this; + } + + /** + * Resets query fetch limit - a parameter that defines max number of objects + * that should be ever be fetched from the database. + */ + public ObjectSelect<T> limit(int fetchLimit) { + if (this.limit != fetchLimit) { + this.limit = fetchLimit; + this.replacementQuery = null; + } + + return this; + } + + /** + * Resets query fetch offset - a parameter that defines how many objects + * should be skipped when reading data from the database. + */ + public ObjectSelect<T> offset(int fetchOffset) { + if (this.offset != fetchOffset) { + this.offset = fetchOffset; + this.replacementQuery = null; + } + + return this; + } + + /** + * Resets query page size. A non-negative page size enables query result + * pagination that saves memory and processing time for large lists if only + * parts of the result are ever going to be accessed. + */ + public ObjectSelect<T> pageSize(int pageSize) { + if (this.pageSize != pageSize) { + this.pageSize = pageSize; + this.replacementQuery = null; + } + + return this; + } + + /** + * Sets fetch size of the PreparedStatement generated for this query. Only + * non-negative values would change the default size. + * + * @see Statement#setFetchSize(int) + */ + public ObjectSelect<T> statementFetchSize(int size) { + if (this.statementFetchSize != size) { + this.statementFetchSize = size; + this.replacementQuery = null; + } + + return this; + } + + public ObjectSelect<T> cacheStrategy(QueryCacheStrategy strategy, String... cacheGroups) { + if (this.cacheStrategy != strategy) { + this.cacheStrategy = strategy; + this.replacementQuery = null; + } + + return cacheGroups(cacheGroups); + } + + public ObjectSelect<T> cacheGroups(String... cacheGroups) { + this.cacheGroups = cacheGroups != null && cacheGroups.length > 0 ? cacheGroups : null; + this.replacementQuery = null; + return this; + } + + public ObjectSelect<T> cacheGroups(Collection<String> cacheGroups) { + + if (cacheGroups == null) { + return cacheGroups((String) null); + } + + String[] array = new String[cacheGroups.size()]; + return cacheGroups(cacheGroups.toArray(array)); + } + + /** + * Instructs Cayenne to look for query results in the "local" cache when + * running the query. This is a short-hand notation for: + * + * <pre> + * query.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroups); + * </pre> + */ + public ObjectSelect<T> localCache(String... cacheGroups) { + return cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, cacheGroups); + } + + /** + * Instructs Cayenne to look for query results in the "shared" cache when + * running the query. This is a short-hand notation for: + * + * <pre> + * query.cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroups); + * </pre> + */ + public ObjectSelect<T> sharedCache(String... cacheGroups) { + return cacheStrategy(QueryCacheStrategy.SHARED_CACHE, cacheGroups); + } + + public String[] getCacheGroups() { + return cacheGroups; + } + + public QueryCacheStrategy getCacheStrategy() { + return cacheStrategy; + } + + public int getStatementFetchSize() { + return statementFetchSize; + } + + public int getPageSize() { + return pageSize; + } + + public int getLimit() { + return limit; + } + + public int getOffset() { + return offset; + } + + public boolean isFetchingDataRows() { + return fetchingDataRows; + } + + public Class<?> getEntityType() { + return entityType; + } + + public String getEntityName() { + return entityName; + } + + public String getDbEntityName() { + return dbEntityName; + } + + public Expression getExp() { + return exp; + } + + public Collection<Ordering> getOrderings() { + return orderings; + } + + public PrefetchTreeNode getPrefetches() { + return prefetches; + } + + /** + * Selects objects using provided context. Essentially the inversion of + * "ObjectContext.select(query)". + */ + public List<T> select(ObjectContext context) { + return context.select(this); + } + + /** + * Selects a single object using provided context. Essentially the inversion + * of "ObjectContext.selectOne(Select)". + */ + public T selectOne(ObjectContext context) { + return context.selectOne(this); + } + +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java b/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java index 08c8544..ccd7a58 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/query/Ordering.java @@ -37,339 +37,371 @@ import org.apache.cayenne.util.XMLSerializable; import org.apache.commons.collections.ComparatorUtils; /** - * Defines object sorting criteria, used either for in-memory sorting of object lists or - * as a specification for building <em>ORDER BY</em> clause of a SelectQuery query. Note - * that in case of in-memory sorting, Ordering can be used with any JavaBeans, not just - * DataObjects. + * Defines object sorting criteria, used either for in-memory sorting of object + * lists or as a specification for building <em>ORDER BY</em> clause of a + * SelectQuery query. Note that in case of in-memory sorting, Ordering can be + * used with any JavaBeans, not just DataObjects. */ public class Ordering implements Comparator<Object>, Serializable, XMLSerializable { - protected String sortSpecString; - protected transient Expression sortSpec; - protected SortOrder sortOrder; - protected boolean pathExceptionSuppressed = false; - protected boolean nullSortedFirst = true; - - /** - * Orders a given list of objects, using a List of Orderings applied according the - * default iteration order of the Orderings list. I.e. each Ordering with lower index - * is more significant than any other Ordering with higher index. List being ordered - * is modified in place. - */ - public static void orderList(List<?> objects, List<? extends Ordering> orderings) { - Collections.sort(objects, ComparatorUtils.chainedComparator(orderings)); - } - - public Ordering() { - } - - /** - * @since 3.0 - */ - public Ordering(String sortPathSpec, SortOrder sortOrder) { - setSortSpecString(sortPathSpec); - setSortOrder(sortOrder); - } - - /** - * Sets sortSpec to be an expression represented by string argument. - * - * @since 1.1 - */ - public void setSortSpecString(String sortSpecString) { - if (!Util.nullSafeEquals(this.sortSpecString, sortSpecString)) { - this.sortSpecString = sortSpecString; - this.sortSpec = null; - } - } - - /** - * Sets sort order for whether nulls are at the top or bottom of the resulting list. - * Default is true. - * - * @param nullSortedFirst true sorts nulls to the top of the list, false sorts nulls - * to the bottom - */ - public void setNullSortedFirst(boolean nullSortedFirst) { - this.nullSortedFirst = nullSortedFirst; - } - - /** - * Get sort order for nulls. - * - * @return true if nulls are sorted to the top of the list, false if sorted to the - * bottom - */ - public boolean isNullSortedFirst() { - return nullSortedFirst; - } - - /** - * Sets whether a path with a null in the middle is ignored. For example, a sort from - * <code>painting</code> on <code>artist.name</code> would by default throw an - * exception if the artist was null. If set to true, then this is treated just like a - * null value. Default is false. - * - * @param pathExceptionSuppressed true to suppress exceptions and sort as null - */ - public void setPathExceptionSupressed(boolean pathExceptionSuppressed) { - this.pathExceptionSuppressed = pathExceptionSuppressed; - } - - /** - * Is a path with a null in the middle is ignored. - * - * @return true is exception is suppressed and sorted as null - */ - public boolean isPathExceptionSuppressed() { - return pathExceptionSuppressed; - } - - /** - * Returns sortSpec string representation. - * - * @since 1.1 - */ - public String getSortSpecString() { - return sortSpecString; - } - - /** - * Sets the sort order for this ordering. - * - * @since 3.0 - */ - public void setSortOrder(SortOrder order) { - this.sortOrder = order; - } - - /** Returns true if sorting is done in ascending order. */ - public boolean isAscending() { - return sortOrder == null - || sortOrder == SortOrder.ASCENDING - || sortOrder == SortOrder.ASCENDING_INSENSITIVE; - } - - /** - * Returns true if the sorting is done in descending order. - * - * @since 3.0 - */ - public boolean isDescending() { - return !isAscending(); - } - - /** - * If the sort order is DESCENDING or DESCENDING_INSENSITIVE, sets the sort order to - * ASCENDING or ASCENDING_INSENSITIVE, respectively. - * - * @since 3.0 - */ - public void setAscending() { - if (sortOrder == null || sortOrder == SortOrder.DESCENDING) - setSortOrder(SortOrder.ASCENDING); - else if (sortOrder == SortOrder.DESCENDING_INSENSITIVE) - setSortOrder(SortOrder.ASCENDING_INSENSITIVE); - } - - /** - * If the sort order is ASCENDING or ASCENDING_INSENSITIVE, sets the sort order to - * DESCENDING or DESCENDING_INSENSITIVE, respectively. - * - * @since 3.0 - */ - public void setDescending() { - if (sortOrder == null || sortOrder == SortOrder.ASCENDING) - setSortOrder(SortOrder.DESCENDING); - else if (sortOrder == SortOrder.ASCENDING_INSENSITIVE) - setSortOrder(SortOrder.DESCENDING_INSENSITIVE); - } - - /** Returns true if the sorting is case insensitive */ - public boolean isCaseInsensitive() { - return !isCaseSensitive(); - } - - /** - * Returns true if the sorting is case sensitive. - * - * @since 3.0 - */ - public boolean isCaseSensitive() { - return sortOrder == null - || sortOrder == SortOrder.ASCENDING - || sortOrder == SortOrder.DESCENDING; - } - - /** - * If the sort order is ASCENDING or DESCENDING, sets the sort order to - * ASCENDING_INSENSITIVE or DESCENDING_INSENSITIVE, respectively. - * - * @since 3.0 - */ - public void setCaseInsensitive() { - if (sortOrder == null || sortOrder == SortOrder.ASCENDING) - setSortOrder(SortOrder.ASCENDING_INSENSITIVE); - else if (sortOrder == SortOrder.DESCENDING) - setSortOrder(SortOrder.DESCENDING_INSENSITIVE); - } - - /** - * If the sort order is ASCENDING_INSENSITIVE or DESCENDING_INSENSITIVE, sets the sort - * order to ASCENDING or DESCENDING, respectively. - * - * @since 3.0 - */ - public void setCaseSensitive() { - if (sortOrder == null || sortOrder == SortOrder.ASCENDING_INSENSITIVE) - setSortOrder(SortOrder.ASCENDING); - else if (sortOrder == SortOrder.DESCENDING_INSENSITIVE) - setSortOrder(SortOrder.DESCENDING); - } - - /** - * Returns the expression defining a ordering Java Bean property. - */ - public Expression getSortSpec() { - if (sortSpecString == null) { - return null; - } - - // compile on demand .. since orderings can only be paths, avoid the overhead of - // Expression.fromString, and parse them manually - if (sortSpec == null) { - - if (sortSpecString.startsWith(ASTDbPath.DB_PREFIX)) { - sortSpec = new ASTDbPath(sortSpecString.substring(ASTDbPath.DB_PREFIX - .length())); - } - else if (sortSpecString.startsWith(ASTObjPath.OBJ_PREFIX)) { - sortSpec = new ASTObjPath(sortSpecString.substring(ASTObjPath.OBJ_PREFIX - .length())); - } - else { - sortSpec = new ASTObjPath(sortSpecString); - } - } - - return sortSpec; - } - - /** - * Sets the expression defining a ordering Java Bean property. - */ - public void setSortSpec(Expression sortSpec) { - this.sortSpec = sortSpec; - this.sortSpecString = (sortSpec != null) ? sortSpec.toString() : null; - } - - /** - * Orders the given list of objects according to the ordering that this object - * specifies. List is modified in-place. - * - * @param objects a List of objects to be sorted - */ - public void orderList(List<?> objects) { - Collections.sort(objects, this); - } - - /** - * Comparable interface implementation. Can compare two Java Beans based on the stored - * expression. - */ - public int compare(Object o1, Object o2) { - Expression exp = getSortSpec(); - Object value1 = null; - Object value2 = null; - try { - value1 = exp.evaluate(o1); - } - catch (ExpressionException e) { - if (pathExceptionSuppressed - && e.getCause() instanceof org.apache.cayenne.reflect.UnresolvablePathException) { - // do nothing, we expect this - } - else { - // re-throw - throw e; - } - } - - try { - value2 = exp.evaluate(o2); - } - catch (ExpressionException e) { - if (pathExceptionSuppressed - && e.getCause() instanceof org.apache.cayenne.reflect.UnresolvablePathException) { - // do nothing, we expect this - } - else { - // rethrow - throw e; - } - } - - if (value1 == null && value2 == null) { - return 0; - } - else if (value1 == null) { - return nullSortedFirst ? -1 : 1; - } - else if (value2 == null) { - return nullSortedFirst ? 1 : -1; - } - - if (isCaseInsensitive()) { - // TODO: to upper case should probably be defined as a separate expression - // type - value1 = ConversionUtil.toUpperCase(value1); - value2 = ConversionUtil.toUpperCase(value2); - } - - int compareResult = ConversionUtil.toComparable(value1).compareTo( - ConversionUtil.toComparable(value2)); - return (isAscending()) ? compareResult : -compareResult; - } - - /** - * Encodes itself as a query ordering. - * - * @since 1.1 - */ - public void encodeAsXML(XMLEncoder encoder) { - encoder.print("<ordering"); - - if (isDescending()) { - encoder.print(" descending=\"true\""); - } - - if (isCaseInsensitive()) { - encoder.print(" ignore-case=\"true\""); - } - - encoder.print(">"); - if (getSortSpec() != null) { - getSortSpec().encodeAsXML(encoder); - } - encoder.println("</ordering>"); - } - - @Override - public String toString() { - StringWriter buffer = new StringWriter(); - PrintWriter pw = new PrintWriter(buffer); - XMLEncoder encoder = new XMLEncoder(pw); - encodeAsXML(encoder); - pw.close(); - buffer.flush(); - return buffer.toString(); - } - - /** - * Returns sort order for this ordering - * @since 3.1 - */ - public SortOrder getSortOrder() { - return sortOrder; - } + private static final long serialVersionUID = -9167074787055881422L; + + protected String sortSpecString; + protected transient Expression sortSpec; + protected SortOrder sortOrder; + protected boolean pathExceptionSuppressed = false; + protected boolean nullSortedFirst = true; + + /** + * Orders a given list of objects, using a List of Orderings applied + * according the default iteration order of the Orderings list. I.e. each + * Ordering with lower index is more significant than any other Ordering + * with higher index. List being ordered is modified in place. + */ + public static void orderList(List<?> objects, List<? extends Ordering> orderings) { + Collections.sort(objects, ComparatorUtils.chainedComparator(orderings)); + } + + public Ordering() { + } + + /** + * Create an ordering instance with a provided path and ascending sorting + * strategy. + * + * @since 4.0 + */ + public Ordering(String sortPathSpec) { + this(sortPathSpec, SortOrder.ASCENDING); + } + + /** + * @since 3.0 + */ + public Ordering(String sortPathSpec, SortOrder sortOrder) { + setSortSpecString(sortPathSpec); + setSortOrder(sortOrder); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + + if (!(object instanceof Ordering)) { + return false; + } + + Ordering o = (Ordering) object; + + if (!Util.nullSafeEquals(sortSpecString, o.sortSpecString)) { + return false; + } + + if (sortOrder != o.sortOrder) { + return false; + } + + if (pathExceptionSuppressed != o.pathExceptionSuppressed) { + return false; + } + + if (nullSortedFirst != o.nullSortedFirst) { + return false; + } + + return true; + } + + /** + * Sets sortSpec to be an expression represented by string argument. + * + * @since 1.1 + */ + public void setSortSpecString(String sortSpecString) { + if (!Util.nullSafeEquals(this.sortSpecString, sortSpecString)) { + this.sortSpecString = sortSpecString; + this.sortSpec = null; + } + } + + /** + * Sets sort order for whether nulls are at the top or bottom of the + * resulting list. Default is true. + * + * @param nullSortedFirst + * true sorts nulls to the top of the list, false sorts nulls to + * the bottom + */ + public void setNullSortedFirst(boolean nullSortedFirst) { + this.nullSortedFirst = nullSortedFirst; + } + + /** + * Get sort order for nulls. + * + * @return true if nulls are sorted to the top of the list, false if sorted + * to the bottom + */ + public boolean isNullSortedFirst() { + return nullSortedFirst; + } + + /** + * Sets whether a path with a null in the middle is ignored. For example, a + * sort from <code>painting</code> on <code>artist.name</code> would by + * default throw an exception if the artist was null. If set to true, then + * this is treated just like a null value. Default is false. + * + * @param pathExceptionSuppressed + * true to suppress exceptions and sort as null + */ + public void setPathExceptionSupressed(boolean pathExceptionSuppressed) { + this.pathExceptionSuppressed = pathExceptionSuppressed; + } + + /** + * Is a path with a null in the middle is ignored. + * + * @return true is exception is suppressed and sorted as null + */ + public boolean isPathExceptionSuppressed() { + return pathExceptionSuppressed; + } + + /** + * Returns sortSpec string representation. + * + * @since 1.1 + */ + public String getSortSpecString() { + return sortSpecString; + } + + /** + * Sets the sort order for this ordering. + * + * @since 3.0 + */ + public void setSortOrder(SortOrder order) { + this.sortOrder = order; + } + + /** Returns true if sorting is done in ascending order. */ + public boolean isAscending() { + return sortOrder == null || sortOrder == SortOrder.ASCENDING || sortOrder == SortOrder.ASCENDING_INSENSITIVE; + } + + /** + * Returns true if the sorting is done in descending order. + * + * @since 3.0 + */ + public boolean isDescending() { + return !isAscending(); + } + + /** + * If the sort order is DESCENDING or DESCENDING_INSENSITIVE, sets the sort + * order to ASCENDING or ASCENDING_INSENSITIVE, respectively. + * + * @since 3.0 + */ + public void setAscending() { + if (sortOrder == null || sortOrder == SortOrder.DESCENDING) + setSortOrder(SortOrder.ASCENDING); + else if (sortOrder == SortOrder.DESCENDING_INSENSITIVE) + setSortOrder(SortOrder.ASCENDING_INSENSITIVE); + } + + /** + * If the sort order is ASCENDING or ASCENDING_INSENSITIVE, sets the sort + * order to DESCENDING or DESCENDING_INSENSITIVE, respectively. + * + * @since 3.0 + */ + public void setDescending() { + if (sortOrder == null || sortOrder == SortOrder.ASCENDING) + setSortOrder(SortOrder.DESCENDING); + else if (sortOrder == SortOrder.ASCENDING_INSENSITIVE) + setSortOrder(SortOrder.DESCENDING_INSENSITIVE); + } + + /** Returns true if the sorting is case insensitive */ + public boolean isCaseInsensitive() { + return !isCaseSensitive(); + } + + /** + * Returns true if the sorting is case sensitive. + * + * @since 3.0 + */ + public boolean isCaseSensitive() { + return sortOrder == null || sortOrder == SortOrder.ASCENDING || sortOrder == SortOrder.DESCENDING; + } + + /** + * If the sort order is ASCENDING or DESCENDING, sets the sort order to + * ASCENDING_INSENSITIVE or DESCENDING_INSENSITIVE, respectively. + * + * @since 3.0 + */ + public void setCaseInsensitive() { + if (sortOrder == null || sortOrder == SortOrder.ASCENDING) + setSortOrder(SortOrder.ASCENDING_INSENSITIVE); + else if (sortOrder == SortOrder.DESCENDING) + setSortOrder(SortOrder.DESCENDING_INSENSITIVE); + } + + /** + * If the sort order is ASCENDING_INSENSITIVE or DESCENDING_INSENSITIVE, + * sets the sort order to ASCENDING or DESCENDING, respectively. + * + * @since 3.0 + */ + public void setCaseSensitive() { + if (sortOrder == null || sortOrder == SortOrder.ASCENDING_INSENSITIVE) + setSortOrder(SortOrder.ASCENDING); + else if (sortOrder == SortOrder.DESCENDING_INSENSITIVE) + setSortOrder(SortOrder.DESCENDING); + } + + /** + * Returns the expression defining a ordering Java Bean property. + */ + public Expression getSortSpec() { + if (sortSpecString == null) { + return null; + } + + // compile on demand .. since orderings can only be paths, avoid the + // overhead of + // Expression.fromString, and parse them manually + if (sortSpec == null) { + + if (sortSpecString.startsWith(ASTDbPath.DB_PREFIX)) { + sortSpec = new ASTDbPath(sortSpecString.substring(ASTDbPath.DB_PREFIX.length())); + } else if (sortSpecString.startsWith(ASTObjPath.OBJ_PREFIX)) { + sortSpec = new ASTObjPath(sortSpecString.substring(ASTObjPath.OBJ_PREFIX.length())); + } else { + sortSpec = new ASTObjPath(sortSpecString); + } + } + + return sortSpec; + } + + /** + * Sets the expression defining a ordering Java Bean property. + */ + public void setSortSpec(Expression sortSpec) { + this.sortSpec = sortSpec; + this.sortSpecString = (sortSpec != null) ? sortSpec.toString() : null; + } + + /** + * Orders the given list of objects according to the ordering that this + * object specifies. List is modified in-place. + * + * @param objects + * a List of objects to be sorted + */ + public void orderList(List<?> objects) { + Collections.sort(objects, this); + } + + /** + * Comparable interface implementation. Can compare two Java Beans based on + * the stored expression. + */ + public int compare(Object o1, Object o2) { + Expression exp = getSortSpec(); + Object value1 = null; + Object value2 = null; + try { + value1 = exp.evaluate(o1); + } catch (ExpressionException e) { + if (pathExceptionSuppressed && e.getCause() instanceof org.apache.cayenne.reflect.UnresolvablePathException) { + // do nothing, we expect this + } else { + // re-throw + throw e; + } + } + + try { + value2 = exp.evaluate(o2); + } catch (ExpressionException e) { + if (pathExceptionSuppressed && e.getCause() instanceof org.apache.cayenne.reflect.UnresolvablePathException) { + // do nothing, we expect this + } else { + // rethrow + throw e; + } + } + + if (value1 == null && value2 == null) { + return 0; + } else if (value1 == null) { + return nullSortedFirst ? -1 : 1; + } else if (value2 == null) { + return nullSortedFirst ? 1 : -1; + } + + if (isCaseInsensitive()) { + // TODO: to upper case should probably be defined as a separate + // expression + // type + value1 = ConversionUtil.toUpperCase(value1); + value2 = ConversionUtil.toUpperCase(value2); + } + + int compareResult = ConversionUtil.toComparable(value1).compareTo(ConversionUtil.toComparable(value2)); + return (isAscending()) ? compareResult : -compareResult; + } + + /** + * Encodes itself as a query ordering. + * + * @since 1.1 + */ + public void encodeAsXML(XMLEncoder encoder) { + encoder.print("<ordering"); + + if (isDescending()) { + encoder.print(" descending=\"true\""); + } + + if (isCaseInsensitive()) { + encoder.print(" ignore-case=\"true\""); + } + + encoder.print(">"); + if (getSortSpec() != null) { + getSortSpec().encodeAsXML(encoder); + } + encoder.println("</ordering>"); + } + + @Override + public String toString() { + StringWriter buffer = new StringWriter(); + PrintWriter pw = new PrintWriter(buffer); + XMLEncoder encoder = new XMLEncoder(pw); + encodeAsXML(encoder); + pw.close(); + buffer.flush(); + return buffer.toString(); + } + + /** + * Returns sort order for this ordering + * + * @since 3.1 + */ + public SortOrder getSortOrder() { + return sortOrder; + } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java index 11711fc..e44b08d 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java @@ -63,6 +63,21 @@ public class PrefetchTreeNode implements Serializable, XMLSerializable { protected Collection<PrefetchTreeNode> children; /** + * Creates and returns a prefetch tree spanning a single path. The tree is + * made of phantom nodes, up to the leaf node, which is non-phantom and has + * specified semantics. + * + * @since 4.0 + */ + public static PrefetchTreeNode withPath(String path, int semantics) { + PrefetchTreeNode root = new PrefetchTreeNode(); + PrefetchTreeNode node = root.addPath(path); + node.setPhantom(false); + node.setSemantics(semantics); + return root; + } + + /** * Creates a root node of the prefetch tree. Children can be added to the * parent by calling "addPath". */ http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java index 80dba3f..8618bed 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java @@ -20,6 +20,7 @@ package org.apache.cayenne.query; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -44,7 +45,7 @@ import org.apache.cayenne.util.XMLSerializable; public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery, XMLSerializable, Select<T> { private static final long serialVersionUID = 5486418811888197559L; - + public static final String DISTINCT_PROPERTY = "cayenne.SelectQuery.distinct"; public static final boolean DISTINCT_DEFAULT = false; @@ -500,11 +501,12 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery, /** * Adds a list of orderings. */ - public void addOrderings(List<? extends Ordering> orderings) { + public void addOrderings(Collection<? extends Ordering> orderings) { // If the supplied list of orderings is null, do not attempt to add // to the collection (addAll() will NPE otherwise). - if (orderings != null) + if (orderings != null) { nonNullOrderings().addAll(orderings); + } } /** @@ -589,7 +591,7 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery, * @since 4.0 */ public void addPrefetch(PrefetchTreeNode prefetchElement) { - metaData.mergePrefetch(prefetchElement); + metaData.mergePrefetch(prefetchElement); } /** http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java b/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java index 2e6150b..bf935df 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/remote/IncrementalSelectQuery.java @@ -18,6 +18,7 @@ ****************************************************************/ package org.apache.cayenne.remote; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -156,8 +157,8 @@ class IncrementalSelectQuery<T> extends SelectQuery<T> { } @Override - public void addOrderings(List orderings) { - query.addOrderings(orderings); + public void addOrderings(Collection<? extends Ordering> orderings) { + query.addOrderings(orderings); } @Override http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java new file mode 100644 index 0000000..cd69200 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelectTest.java @@ -0,0 +1,482 @@ +/***************************************************************** + * 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.query; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.apache.cayenne.DataRow; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.testdo.testmap.Artist; +import org.junit.Test; + +public class ObjectSelectTest { + + @Test + public void testDataRowQuery() { + ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class); + assertNotNull(q); + assertTrue(q.isFetchingDataRows()); + + assertEquals(Artist.class, q.getEntityType()); + assertNull(q.getEntityName()); + assertNull(q.getDbEntityName()); + } + + @Test + public void testQuery_RootType() { + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + assertNotNull(q); + assertNull(q.getExp()); + assertFalse(q.isFetchingDataRows()); + + assertEquals(Artist.class, q.getEntityType()); + assertNull(q.getEntityName()); + assertNull(q.getDbEntityName()); + } + + @Test + public void testQuery_RootType_WithQualifier() { + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class, ExpressionFactory.matchExp("a", "A")); + assertNotNull(q); + assertEquals("a = \"A\"", q.getExp().toString()); + assertFalse(q.isFetchingDataRows()); + + assertEquals(Artist.class, q.getEntityType()); + assertNull(q.getEntityName()); + assertNull(q.getDbEntityName()); + } + + @Test + public void testQuery_TypeAndEntity() { + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class, "Painting"); + assertNotNull(q); + assertFalse(q.isFetchingDataRows()); + + assertNull(q.getEntityType()); + assertEquals("Painting", q.getEntityName()); + assertNull(q.getDbEntityName()); + } + + @Test + public void testQuery_TypeAndDbEntity() { + ObjectSelect<DataRow> q = ObjectSelect.dbQuery("PAINTING"); + assertNotNull(q); + assertTrue(q.isFetchingDataRows()); + + assertNull(q.getEntityType()); + assertNull(q.getEntityName()); + assertEquals("PAINTING", q.getDbEntityName()); + } + + @Test + public void testExp() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.exp(ExpressionFactory.matchExp("a", 3)); + assertEquals("a = 3", q.getExp().toString()); + + q.exp(ExpressionFactory.matchExp("b", 4)); + assertEquals("b = 4", q.getExp().toString()); + } + + @Test + public void testAnd_Array() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.exp(ExpressionFactory.matchExp("a", 3)); + assertEquals("a = 3", q.getExp().toString()); + + q.and(ExpressionFactory.matchExp("b", 4), ExpressionFactory.greaterExp("c", 5)); + assertEquals("(a = 3) and (b = 4) and (c > 5)", q.getExp().toString()); + } + + @Test + public void testAnd_Collection() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.exp(ExpressionFactory.matchExp("a", 3)); + assertEquals("a = 3", q.getExp().toString()); + + Collection<Expression> exps = Arrays.asList(ExpressionFactory.matchExp("b", 4), + ExpressionFactory.greaterExp("c", 5)); + + q.and(exps); + assertEquals("(a = 3) and (b = 4) and (c > 5)", q.getExp().toString()); + } + + @Test + public void testAnd_ArrayNull() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.exp(ExpressionFactory.matchExp("a", 3)); + assertEquals("a = 3", q.getExp().toString()); + + q.and(); + assertEquals("a = 3", q.getExp().toString()); + } + + @Test + public void testAnd_ArrayEmpty() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.exp(ExpressionFactory.matchExp("a", 3)); + assertEquals("a = 3", q.getExp().toString()); + + q.and(new Expression[0]); + assertEquals("a = 3", q.getExp().toString()); + } + + @Test + public void testAnd_CollectionEmpty() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.exp(ExpressionFactory.matchExp("a", 3)); + assertEquals("a = 3", q.getExp().toString()); + + q.and(Collections.<Expression> emptyList()); + assertEquals("a = 3", q.getExp().toString()); + } + + @Test + public void testOr_Array() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.exp(ExpressionFactory.matchExp("a", 3)); + assertEquals("a = 3", q.getExp().toString()); + + q.or(ExpressionFactory.matchExp("b", 4), ExpressionFactory.greaterExp("c", 5)); + assertEquals("(a = 3) or (b = 4) or (c > 5)", q.getExp().toString()); + } + + @Test + public void testOr_Collection() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.exp(ExpressionFactory.matchExp("a", 3)); + assertEquals("a = 3", q.getExp().toString()); + + Collection<Expression> exps = Arrays.asList(ExpressionFactory.matchExp("b", 4), + ExpressionFactory.greaterExp("c", 5)); + + q.or(exps); + assertEquals("(a = 3) or (b = 4) or (c > 5)", q.getExp().toString()); + } + + @Test + public void testOrderBy_Array() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + Ordering o1 = new Ordering("x"); + q.orderBy(o1); + + Object[] result1 = q.getOrderings().toArray(); + assertEquals(1, result1.length); + assertSame(o1, result1[0]); + + Ordering o2 = new Ordering("y"); + q.orderBy(o2); + + Object[] result2 = q.getOrderings().toArray(); + assertEquals(1, result2.length); + assertSame(o2, result2[0]); + } + + @Test + public void testAddOrderBy_Array() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + Ordering o1 = new Ordering("x"); + q.orderBy(o1); + + Object[] result1 = q.getOrderings().toArray(); + assertEquals(1, result1.length); + assertSame(o1, result1[0]); + + Ordering o2 = new Ordering("y"); + q.addOrderBy(o2); + + Object[] result2 = q.getOrderings().toArray(); + assertEquals(2, result2.length); + assertSame(o1, result2[0]); + assertSame(o2, result2[1]); + } + + @Test + public void testOrderBy_Collection() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + Ordering o1 = new Ordering("x"); + q.orderBy(Collections.singletonList(o1)); + + Object[] result1 = q.getOrderings().toArray(); + assertEquals(1, result1.length); + assertSame(o1, result1[0]); + + Ordering o2 = new Ordering("y"); + q.orderBy(Collections.singletonList(o2)); + + Object[] result2 = q.getOrderings().toArray(); + assertEquals(1, result2.length); + assertSame(o2, result2[0]); + } + + @Test + public void testAddOrderBy_Collection() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + Ordering o1 = new Ordering("x"); + q.orderBy(Collections.singletonList(o1)); + + Object[] result1 = q.getOrderings().toArray(); + assertEquals(1, result1.length); + assertSame(o1, result1[0]); + + Ordering o2 = new Ordering("y"); + q.addOrderBy(Collections.singletonList(o2)); + + Object[] result2 = q.getOrderings().toArray(); + assertEquals(2, result2.length); + assertSame(o1, result2[0]); + assertSame(o2, result2[1]); + } + + @Test + public void testOrderBy_Property() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.orderBy("x"); + + Object[] result1 = q.getOrderings().toArray(); + assertEquals(1, result1.length); + assertEquals(new Ordering("x", SortOrder.ASCENDING), result1[0]); + + q.orderBy("y"); + + Object[] result2 = q.getOrderings().toArray(); + assertEquals(1, result2.length); + assertEquals(new Ordering("y", SortOrder.ASCENDING), result2[0]); + } + + @Test + public void testOrderBy_PropertyStrategy() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.orderBy("x", SortOrder.ASCENDING_INSENSITIVE); + + Object[] result1 = q.getOrderings().toArray(); + assertEquals(1, result1.length); + assertEquals(new Ordering("x", SortOrder.ASCENDING_INSENSITIVE), result1[0]); + + q.orderBy("y", SortOrder.DESCENDING); + + Object[] result2 = q.getOrderings().toArray(); + assertEquals(1, result2.length); + assertEquals(new Ordering("y", SortOrder.DESCENDING), result2[0]); + } + + @Test + public void testAddOrderBy_Property() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + q.addOrderBy("x"); + + Object[] result1 = q.getOrderings().toArray(); + assertEquals(1, result1.length); + assertEquals(new Ordering("x", SortOrder.ASCENDING), result1[0]); + + q.addOrderBy("y"); + + Object[] result2 = q.getOrderings().toArray(); + assertEquals(2, result2.length); + assertEquals(new Ordering("x", SortOrder.ASCENDING), result2[0]); + assertEquals(new Ordering("y", SortOrder.ASCENDING), result2[1]); + } + + @Test + public void testPrefetch() { + + PrefetchTreeNode root = PrefetchTreeNode.withPath("a.b", PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS); + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + q.prefetch(root); + + assertSame(root, q.getPrefetches()); + } + + @Test + public void testPrefetch_Path() { + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + q.prefetch("a.b", PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS); + PrefetchTreeNode root1 = q.getPrefetches(); + + assertNotNull(root1); + assertNotNull(root1.getNode("a.b")); + + q.prefetch("a.c", PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS); + PrefetchTreeNode root2 = q.getPrefetches(); + + assertNotNull(root2); + assertNotNull(root2.getNode("a.c")); + assertNull(root2.getNode("a.b")); + assertNotSame(root1, root2); + } + + @Test + public void testAddPrefetch() { + + PrefetchTreeNode root = PrefetchTreeNode.withPath("a.b", PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS); + + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + q.prefetch(root); + + assertSame(root, q.getPrefetches()); + + PrefetchTreeNode subRoot = PrefetchTreeNode.withPath("a.b.c", PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS); + q.addPrefetch(subRoot); + + assertSame(root, q.getPrefetches()); + + assertNotNull(root.getNode("a.b.c")); + } + + @Test + public void testLimit() { + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + assertEquals(0, q.getLimit()); + q.limit(2); + assertEquals(2, q.getLimit()); + + q.limit(3).limit(5); + assertEquals(5, q.getLimit()); + } + + @Test + public void testOffset() { + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + assertEquals(0, q.getOffset()); + q.offset(2); + assertEquals(2, q.getOffset()); + + q.offset(3).offset(5); + assertEquals(5, q.getOffset()); + } + + @Test + public void testStatementFetchSize() { + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + assertEquals(0, q.getStatementFetchSize()); + q.statementFetchSize(2); + assertEquals(2, q.getStatementFetchSize()); + + q.statementFetchSize(3).statementFetchSize(5); + assertEquals(5, q.getStatementFetchSize()); + } + + + @Test + public void testCacheGroups_Collection() { + ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class); + + assertNull(q.getCacheStrategy()); + assertNull(q.getCacheGroups()); + + q.cacheGroups(Arrays.asList("a", "b")); + assertNull(q.getCacheStrategy()); + assertArrayEquals(new String[] { "a", "b" }, q.getCacheGroups()); + } + + @Test + public void testCacheStrategy() { + ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class); + + assertNull(q.getCacheStrategy()); + assertNull(q.getCacheGroups()); + + q.cacheStrategy(QueryCacheStrategy.LOCAL_CACHE, "a", "b"); + assertSame(QueryCacheStrategy.LOCAL_CACHE, q.getCacheStrategy()); + assertArrayEquals(new String[] { "a", "b" }, q.getCacheGroups()); + + q.cacheStrategy(QueryCacheStrategy.SHARED_CACHE); + assertSame(QueryCacheStrategy.SHARED_CACHE, q.getCacheStrategy()); + assertNull(q.getCacheGroups()); + } + + @Test + public void testLocalCache() { + ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class); + + assertNull(q.getCacheStrategy()); + assertNull(q.getCacheGroups()); + + q.localCache("a", "b"); + assertSame(QueryCacheStrategy.LOCAL_CACHE, q.getCacheStrategy()); + assertArrayEquals(new String[] { "a", "b" }, q.getCacheGroups()); + + q.localCache(); + assertSame(QueryCacheStrategy.LOCAL_CACHE, q.getCacheStrategy()); + assertNull(q.getCacheGroups()); + } + + @Test + public void testSharedCache() { + ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class); + + assertNull(q.getCacheStrategy()); + assertNull(q.getCacheGroups()); + + q.sharedCache("a", "b"); + assertSame(QueryCacheStrategy.SHARED_CACHE, q.getCacheStrategy()); + assertArrayEquals(new String[] { "a", "b" }, q.getCacheGroups()); + + q.sharedCache(); + assertSame(QueryCacheStrategy.SHARED_CACHE, q.getCacheStrategy()); + assertNull(q.getCacheGroups()); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java new file mode 100644 index 0000000..9a7a590 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_CompileIT.java @@ -0,0 +1,168 @@ +/***************************************************************** + * 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.query; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import org.apache.cayenne.CayenneDataObject; +import org.apache.cayenne.CayenneRuntimeException; +import org.apache.cayenne.DataRow; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.map.EntityResolver; +import org.apache.cayenne.testdo.testmap.Artist; +import org.apache.cayenne.unit.di.server.ServerCase; +import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.junit.Test; + +@UseServerRuntime(ServerCase.TESTMAP_PROJECT) +public class ObjectSelect_CompileIT extends ServerCase { + + @Inject + private EntityResolver resolver; + + @Test + public void testCreateReplacementQuery_Bare() { + + // use only a minimal number of attributes, with null/defaults for + // everything else + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + Query replacement = q.createReplacementQuery(resolver); + assertThat(replacement, instanceOf(SelectQuery.class)); + + @SuppressWarnings("unchecked") + SelectQuery<Artist> selectQuery = (SelectQuery<Artist>) replacement; + assertNull(selectQuery.getQualifier()); + assertEquals(Artist.class, selectQuery.getRoot()); + assertEquals(0, selectQuery.getOrderings().size()); + assertNull(selectQuery.getPrefetchTree()); + + assertEquals(QueryCacheStrategy.NO_CACHE, selectQuery.getCacheStrategy()); + assertNull(selectQuery.getCacheGroups()); + assertEquals(0, selectQuery.getFetchLimit()); + assertEquals(0, selectQuery.getFetchOffset()); + assertEquals(0, selectQuery.getPageSize()); + assertEquals(0, selectQuery.getStatementFetchSize()); + } + + @Test + public void testCreateReplacementQuery_Full() { + + // add all possible attributes to the query and make sure they got + // propagated + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class).exp(Artist.ARTIST_NAME.eq("me")) + .orderBy(Artist.DATE_OF_BIRTH.asc(), Artist.ARTIST_NAME.desc()).prefetch(Artist.PAINTING_ARRAY.joint()) + .localCache("cg2", "cg1").limit(46).offset(9).pageSize(6).statementFetchSize(789); + + Query replacement = q.createReplacementQuery(resolver); + assertThat(replacement, instanceOf(SelectQuery.class)); + + @SuppressWarnings("unchecked") + SelectQuery<Artist> selectQuery = (SelectQuery<Artist>) replacement; + assertEquals("artistName = \"me\"", selectQuery.getQualifier().toString()); + + assertEquals(2, selectQuery.getOrderings().size()); + assertArrayEquals(new Object[] { Artist.DATE_OF_BIRTH.asc(), Artist.ARTIST_NAME.desc() }, selectQuery + .getOrderings().toArray()); + + PrefetchTreeNode prefetch = selectQuery.getPrefetchTree(); + assertNotNull(prefetch); + assertEquals(1, prefetch.getChildren().size()); + + PrefetchTreeNode childPrefetch = prefetch.getNode(Artist.PAINTING_ARRAY.getName()); + assertEquals(Artist.PAINTING_ARRAY.getName(), childPrefetch.getName()); + assertEquals(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS, childPrefetch.getSemantics()); + + assertEquals(QueryCacheStrategy.LOCAL_CACHE, selectQuery.getCacheStrategy()); + assertArrayEquals(new Object[] { "cg2", "cg1" }, selectQuery.getCacheGroups()); + + assertEquals(46, selectQuery.getFetchLimit()); + assertEquals(9, selectQuery.getFetchOffset()); + assertEquals(6, selectQuery.getPageSize()); + assertEquals(789, selectQuery.getStatementFetchSize()); + } + + @Test + public void testCreateReplacementQuery_RootClass() { + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + @SuppressWarnings("rawtypes") + SelectQuery qr = (SelectQuery) q.createReplacementQuery(resolver); + assertEquals(Artist.class, qr.getRoot()); + assertFalse(qr.isFetchingDataRows()); + } + + @Test + public void testCreateReplacementQuery_RootDataRow() { + ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class); + + @SuppressWarnings("rawtypes") + SelectQuery qr = (SelectQuery) q.createReplacementQuery(resolver); + assertEquals(Artist.class, qr.getRoot()); + assertTrue(qr.isFetchingDataRows()); + } + + @Test + public void testCreateReplacementQuery_RootDbEntity() { + ObjectSelect<DataRow> q = ObjectSelect.dbQuery("ARTIST"); + + @SuppressWarnings("rawtypes") + SelectQuery qr = (SelectQuery) q.createReplacementQuery(resolver); + assertEquals(resolver.getDbEntity("ARTIST"), qr.getRoot()); + assertTrue(qr.isFetchingDataRows()); + } + + @Test + public void testCreateReplacementQuery_RootObjEntity() { + ObjectSelect<CayenneDataObject> q = ObjectSelect.query(CayenneDataObject.class, "Artist"); + + @SuppressWarnings("rawtypes") + SelectQuery qr = (SelectQuery) q.createReplacementQuery(resolver); + assertEquals(resolver.getObjEntity(Artist.class), qr.getRoot()); + assertFalse(qr.isFetchingDataRows()); + } + + @Test(expected = CayenneRuntimeException.class) + public void testCreateReplacementQuery_RootAbscent() { + ObjectSelect<DataRow> q = ObjectSelect.dataRowQuery(Artist.class).entityName(null); + q.createReplacementQuery(resolver); + } + + @Test + public void testCreateReplacementQuery_DataRows() { + ObjectSelect<Artist> q = ObjectSelect.query(Artist.class); + + @SuppressWarnings("rawtypes") + SelectQuery selectQuery1 = (SelectQuery) q.createReplacementQuery(resolver); + assertFalse(selectQuery1.isFetchingDataRows()); + + q.fetchDataRows(); + + @SuppressWarnings("rawtypes") + SelectQuery selectQuery2 = (SelectQuery) q.createReplacementQuery(resolver); + assertTrue(selectQuery2.isFetchingDataRows()); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java new file mode 100644 index 0000000..1b921a4 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_RunIT.java @@ -0,0 +1,94 @@ +/***************************************************************** + * 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.query; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import java.util.List; + +import org.apache.cayenne.DataRow; +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.Artist; +import org.apache.cayenne.unit.di.server.ServerCase; +import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.junit.Test; + +@UseServerRuntime(ServerCase.TESTMAP_PROJECT) +public class ObjectSelect_RunIT extends ServerCase { + + @Inject + private DataContext context; + + @Inject + private DBHelper dbHelper; + + @Override + protected void setUpAfterInjection() throws Exception { + dbHelper.deleteAll("PAINTING_INFO"); + dbHelper.deleteAll("PAINTING"); + dbHelper.deleteAll("ARTIST_EXHIBIT"); + dbHelper.deleteAll("ARTIST_GROUP"); + dbHelper.deleteAll("ARTIST"); + } + + protected void createArtistsDataSet() throws Exception { + TableHelper tArtist = new TableHelper(dbHelper, "ARTIST"); + tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"); + + long dateBase = System.currentTimeMillis(); + + for (int i = 1; i <= 20; i++) { + tArtist.insert(i, "artist" + i, new java.sql.Date(dateBase + 10000 * i)); + } + } + + @Test + public void test_SelectObjects() throws Exception { + + createArtistsDataSet(); + + List<Artist> result = ObjectSelect.query(Artist.class).select(context); + assertEquals(20, result.size()); + assertThat(result.get(0), instanceOf(Artist.class)); + + Artist a = ObjectSelect.query(Artist.class).exp(Artist.ARTIST_NAME.eq("artist14")).selectOne(context); + assertNotNull(a); + assertEquals("artist14", a.getArtistName()); + } + + @Test + public void test_SelectDataRows() throws Exception { + + createArtistsDataSet(); + + List<DataRow> result = ObjectSelect.dataRowQuery(Artist.class).select(context); + assertEquals(20, result.size()); + assertThat(result.get(0), instanceOf(DataRow.class)); + + DataRow a = ObjectSelect.dataRowQuery(Artist.class).exp(Artist.ARTIST_NAME.eq("artist14")).selectOne(context); + assertNotNull(a); + assertEquals("artist14", a.get("ARTIST_NAME")); + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/a0f941a0/docs/doc/src/main/resources/RELEASE-NOTES.txt ---------------------------------------------------------------------- diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt index 86ec1c3..7711b36 100644 --- a/docs/doc/src/main/resources/RELEASE-NOTES.txt +++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt @@ -66,6 +66,7 @@ CAY-1952 Undeprecate (actually restore) ObjectContext.deleteObject(..) CAY-1953 Redo ResultIteratorCallback to handle single row callback instead of iterator CAY-1954 Make Cayenne class constructor protected CAY-1958 SelectById - a new full-featured select query to get objects by id +CAY-1959 Chainable API for SelectQuery CAY-1960 ExpressionFactory.exp(..) , and(..), or(..) CAY-1962 Implement CayenneTable column resize on double-click on the header separator CAY-1965 Change version from 3.2 to 4.0