From 08602afcff2c49adc65f2ee6c910ee3fedfe2d38 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 18 May 2014 21:17:19 +1200 Subject: [PATCH] Fix for #37 - disjunction expression should not produce inner join - left outer join instead --- .../ebeaninternal/api/LoadBeanRequest.java | 12 +- .../ebeaninternal/api/LoadManyRequest.java | 100 +++++------ .../avaje/ebeaninternal/api/LoadRequest.java | 22 ++- .../ebeaninternal/api/ManyWhereJoins.java | 55 +++++- .../avaje/ebeaninternal/api/PropertyJoin.java | 39 +++++ .../ebeaninternal/api/SpiExpression.java | 13 +- .../com/avaje/ebeaninternal/api/SpiQuery.java | 23 ++- .../server/core/DefaultBeanLoader.java | 6 + .../server/core/OrmQueryRequest.java | 9 +- .../server/deploy/BeanProperty.java | 7 +- .../server/deploy/BeanPropertyAssoc.java | 16 +- .../server/deploy/BeanPropertyAssocOne.java | 17 +- .../server/deploy/DbSqlContext.java | 4 +- .../server/deploy/TableJoin.java | 31 ++-- .../server/deploy/meta/DeployTableJoin.java | 19 +- .../server/expression/JunctionExpression.java | 25 ++- .../server/loadcontext/DLoadBaseContext.java | 7 + .../server/loadcontext/DLoadBeanContext.java | 6 +- .../server/loadcontext/DLoadManyContext.java | 4 +- .../server/query/DefaultDbSqlContext.java | 7 +- .../server/query/SqlJoinType.java | 70 ++++++++ .../server/query/SqlTreeBuilder.java | 24 +-- .../server/query/SqlTreeNode.java | 2 +- .../server/query/SqlTreeNodeBean.java | 50 +++--- .../server/query/SqlTreeNodeExtraJoin.java | 44 ++--- .../server/query/SqlTreeNodeManyRoot.java | 4 +- .../query/SqlTreeNodeManyWhereJoin.java | 39 +++-- .../server/query/SqlTreeNodeRoot.java | 6 +- .../server/querydefn/DefaultOrmQuery.java | 27 ++- .../basic/TestOrderTotalAmountFormula.java | 5 +- .../tests/batchload/TestSecondaryQueries.java | 85 ++++++++- .../com/avaje/tests/model/basic/Contact.java | 2 +- .../com/avaje/tests/query/TestIContains.java | 14 +- .../TestImplicitJoinOnParentRelatonship.java | 63 +++++++ .../com/avaje/tests/query/TestJoinQuery.java | 38 ---- .../avaje/tests/query/TestPagingListLoop.java | 48 ------ .../query/TestQueryFetchManyTwoDeep.java | 162 ++++++++++++++++++ .../tests/query/TestQueryFindIterate.java | 1 - .../com/avaje/tests/query/TestRowCount.java | 3 +- .../TestDisjunctWhereOuterJoin.java | 2 +- .../joins/TestDisjunctWhereOuterOnMany.java | 61 +++++++ .../{ => joins}/TestQueryJoinManyNonRoot.java | 12 +- .../TestQueryJoinQueryNonRoot.java | 7 +- .../TestQueryManyToOneWhereClauseJoin.java | 94 ++++++++++ src/test/resources/ebean.properties | 2 +- 45 files changed, 956 insertions(+), 331 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/api/PropertyJoin.java create mode 100644 src/main/java/com/avaje/ebeaninternal/server/query/SqlJoinType.java create mode 100644 src/test/java/com/avaje/tests/query/TestImplicitJoinOnParentRelatonship.java delete mode 100644 src/test/java/com/avaje/tests/query/TestJoinQuery.java delete mode 100644 src/test/java/com/avaje/tests/query/TestPagingListLoop.java create mode 100644 src/test/java/com/avaje/tests/query/TestQueryFetchManyTwoDeep.java rename src/test/java/com/avaje/tests/query/{ => joins}/TestDisjunctWhereOuterJoin.java (97%) create mode 100644 src/test/java/com/avaje/tests/query/joins/TestDisjunctWhereOuterOnMany.java rename src/test/java/com/avaje/tests/query/{ => joins}/TestQueryJoinManyNonRoot.java (77%) rename src/test/java/com/avaje/tests/query/{ => joins}/TestQueryJoinQueryNonRoot.java (93%) create mode 100644 src/test/java/com/avaje/tests/query/joins/TestQueryManyToOneWhereClauseJoin.java diff --git a/src/main/java/com/avaje/ebeaninternal/api/LoadBeanRequest.java b/src/main/java/com/avaje/ebeaninternal/api/LoadBeanRequest.java index f5a8006d7..4d4d2a443 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/LoadBeanRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/api/LoadBeanRequest.java @@ -2,8 +2,8 @@ package com.avaje.ebeaninternal.api; import java.util.List; -import com.avaje.ebean.Transaction; import com.avaje.ebean.bean.EntityBeanIntercept; +import com.avaje.ebeaninternal.server.core.OrmQueryRequest; /** * Request for loading ManyToOne and OneToOne relationships. @@ -18,10 +18,12 @@ public class LoadBeanRequest extends LoadRequest { private final boolean loadCache; - public LoadBeanRequest(LoadBeanBuffer LoadBuffer, Transaction transaction, boolean lazy, String lazyLoadProperty, - boolean loadCache) { - - super(transaction, lazy); + public LoadBeanRequest(LoadBeanBuffer LoadBuffer, boolean lazy, String lazyLoadProperty, boolean loadCache) { + this(LoadBuffer, null, lazy, lazyLoadProperty, loadCache); + } + + public LoadBeanRequest(LoadBeanBuffer LoadBuffer, OrmQueryRequest parentRequest, boolean lazy, String lazyLoadProperty, boolean loadCache) { + super(parentRequest, lazy); this.LoadBuffer = LoadBuffer; this.batch = LoadBuffer.getBatch(); this.lazyLoadProperty = lazyLoadProperty; diff --git a/src/main/java/com/avaje/ebeaninternal/api/LoadManyRequest.java b/src/main/java/com/avaje/ebeaninternal/api/LoadManyRequest.java index e053a1311..ff48c0d0d 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/LoadManyRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/api/LoadManyRequest.java @@ -2,69 +2,71 @@ package com.avaje.ebeaninternal.api; import java.util.List; -import com.avaje.ebean.Transaction; import com.avaje.ebean.bean.BeanCollection; +import com.avaje.ebeaninternal.server.core.OrmQueryRequest; /** - * Request for loading Associated One Beans. + * Request for loading Associated Many Beans. */ public class LoadManyRequest extends LoadRequest { + private final List> batch; - private final List> batch; + private final LoadManyBuffer loadContext; - private final LoadManyBuffer loadContext; + private final boolean onlyIds; - private final boolean onlyIds; - - private final boolean loadCache; - - public LoadManyRequest(LoadManyBuffer loadContext, Transaction transaction, int batchSize, boolean lazy, + private final boolean loadCache; + + public LoadManyRequest(LoadManyBuffer loadContext, int batchSize, boolean lazy,boolean onlyIds, boolean loadCache) { + this(loadContext, null, batchSize, lazy, onlyIds, loadCache); + } + + public LoadManyRequest(LoadManyBuffer loadContext, OrmQueryRequest parentRequest, int batchSize, boolean lazy, boolean onlyIds, boolean loadCache) { - super(transaction, lazy); - this.loadContext = loadContext; - this.batch = loadContext.getBatch(); - this.onlyIds = onlyIds; - this.loadCache = loadCache; - } + super(parentRequest, lazy); + this.loadContext = loadContext; + this.batch = loadContext.getBatch(); + this.onlyIds = onlyIds; + this.loadCache = loadCache; + } - public String getDescription() { - return "path:" + loadContext.getFullPath() + " size:"+ batch.size(); - } + public String getDescription() { + return "path:" + loadContext.getFullPath() + " size:" + batch.size(); + } - /** - * Return the batch of collections to actually load. - */ - public List> getBatch() { - return batch; - } + /** + * Return the batch of collections to actually load. + */ + public List> getBatch() { + return batch; + } - /** - * Return the load context. - */ - public LoadManyBuffer getLoadContext() { - return loadContext; - } + /** + * Return the load context. + */ + public LoadManyBuffer getLoadContext() { + return loadContext; + } - /** - * Return true if lazy loading should only load the id values. - *

- * This for use when lazy loading is invoked on methods such - * as clear() and removeAll() where it generally makes sense to - * only fetch the Id values as the other property information is - * not used. - *

- */ - public boolean isOnlyIds() { - return onlyIds; - } + /** + * Return true if lazy loading should only load the id values. + *

+ * This for use when lazy loading is invoked on methods such as clear() and removeAll() where it + * generally makes sense to only fetch the Id values as the other property information is not + * used. + *

+ */ + public boolean isOnlyIds() { + return onlyIds; + } + + /** + * Return true if we should load the Collection ids into the cache. + */ + public boolean isLoadCache() { + return loadCache; + } - /** - * Return true if we should load the Collection ids into the cache. - */ - public boolean isLoadCache() { - return loadCache; - } - } diff --git a/src/main/java/com/avaje/ebeaninternal/api/LoadRequest.java b/src/main/java/com/avaje/ebeaninternal/api/LoadRequest.java index 2364ce71e..0da6a26d9 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/LoadRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/api/LoadRequest.java @@ -1,22 +1,36 @@ package com.avaje.ebeaninternal.api; import com.avaje.ebean.Transaction; +import com.avaje.ebeaninternal.server.core.OrmQueryRequest; /** * Request for loading Associated One Beans. */ public abstract class LoadRequest { - protected final boolean lazy; + protected final OrmQueryRequest parentRequest; - protected final Transaction transaction; + protected final Transaction transaction; - public LoadRequest(Transaction transaction, boolean lazy) { + protected final boolean lazy; - this.transaction = transaction; + public LoadRequest(OrmQueryRequest parentRequest, boolean lazy) { + + this.parentRequest = parentRequest; + this.transaction = parentRequest == null ? null : parentRequest.getTransaction(); this.lazy = lazy; } + /** + * Log the just executed secondary query with the 'root' query if 'logSecondaryQuery' is set to + * true. This is for testing purposes to confirm the secondary query executes etc. + */ + public void logSecondaryQuery(SpiQuery query) { + if (parentRequest != null && parentRequest.isLogSecondaryQuery()) { + parentRequest.getQuery().logSecondaryQuery(query); + } + } + /** * Return true if this is a lazy load and false if it is a secondary query. */ diff --git a/src/main/java/com/avaje/ebeaninternal/api/ManyWhereJoins.java b/src/main/java/com/avaje/ebeaninternal/api/ManyWhereJoins.java index b00cdb123..8421c034b 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/ManyWhereJoins.java +++ b/src/main/java/com/avaje/ebeaninternal/api/ManyWhereJoins.java @@ -1,13 +1,15 @@ package com.avaje.ebeaninternal.api; import java.io.Serializable; -import java.util.Set; +import java.util.Collection; +import java.util.TreeMap; import java.util.TreeSet; import com.avaje.ebeaninternal.server.deploy.BeanProperty; import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; import com.avaje.ebeaninternal.server.el.ElPropertyDeploy; import com.avaje.ebeaninternal.server.query.SplitName; +import com.avaje.ebeaninternal.server.query.SqlJoinType; /** * Holds the joins needs to support the many where predicates. @@ -17,12 +19,32 @@ public class ManyWhereJoins implements Serializable { private static final long serialVersionUID = -6490181101871795417L; - private final TreeSet joins = new TreeSet(); + private final TreeMap joins = new TreeMap(); private StringBuilder formulaProperties = new StringBuilder(); private boolean formulaWithJoin; + /** + * 'Mode' indicating that joins added while this is true are required to be outer joins. + */ + private boolean requireOuterJoins; + + /** + * Return the current 'mode' indicating if outer joins are currently required or not. + */ + public boolean isRequireOuterJoins() { + return requireOuterJoins; + } + + /** + * Set the 'mode' to be that joins added are required to be outer joins. + * This is set during the evaluation of disjunction predicates. + */ + public void setRequireOuterJoins(boolean requireOuterJoins) { + this.requireOuterJoins = requireOuterJoins; + } + /** * Add a many where join. */ @@ -34,10 +56,10 @@ public class ManyWhereJoins implements Serializable { join = addManyToJoin(join, p.getName()); } if (join != null){ - joins.add(join); + addJoin(join); String secondaryTableJoinPrefix = p.getSecondaryTableJoinPrefix(); if (secondaryTableJoinPrefix != null) { - joins.add(join+"."+secondaryTableJoinPrefix); + addJoin(join+"."+secondaryTableJoinPrefix); } addParentJoins(join); } @@ -58,11 +80,16 @@ public class ManyWhereJoins implements Serializable { private void addParentJoins(String join) { String[] split = SplitName.split(join); if (split[0] != null){ - joins.add(split[0]); + addJoin(split[0]); addParentJoins(split[0]); } } + private void addJoin(String property) { + SqlJoinType joinType = (requireOuterJoins) ? SqlJoinType.OUTER: SqlJoinType.INNER; + joins.put(property, new PropertyJoin(property, joinType)); + } + /** * Return true if there are no extra many where joins. */ @@ -73,10 +100,22 @@ public class ManyWhereJoins implements Serializable { /** * Return the set of many where joins. */ - public Set getJoins() { - return joins; + public Collection getPropertyJoins() { + return joins.values(); } - + + /** + * Return the set of property names for the many where joins. + */ + public TreeSet getPropertyNames() { + + TreeSet propertyNames = new TreeSet(); + for (PropertyJoin join : joins.values()) { + propertyNames.add(join.getProperty()); + } + return propertyNames; + } + /** * In findRowCount query found a formula property with a join clause so building a select clause * specifically for the findRowCount query. diff --git a/src/main/java/com/avaje/ebeaninternal/api/PropertyJoin.java b/src/main/java/com/avaje/ebeaninternal/api/PropertyJoin.java new file mode 100644 index 000000000..2b183074c --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/api/PropertyJoin.java @@ -0,0 +1,39 @@ +package com.avaje.ebeaninternal.api; + +import com.avaje.ebeaninternal.server.query.SqlJoinType; + +/** + * Represents a join required for a given property and whether than needs to be an outer join. + */ +public class PropertyJoin { + + /** + * The property name. + */ + private final String property; + + /** + * Set to true if the property needs to be an outer join. + */ + private final SqlJoinType joinType; + + public PropertyJoin(String property, SqlJoinType joinType) { + this.property = property; + this.joinType = joinType; + } + + /** + * Return the property that should be joined. + */ + public String getProperty() { + return property; + } + + /** + * Return true if this join is required to be an outer join. + */ + public SqlJoinType getSqlJoinType() { + return joinType; + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiExpression.java b/src/main/java/com/avaje/ebeaninternal/api/SpiExpression.java index 09c3574f4..d7332f7d8 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiExpression.java @@ -10,13 +10,12 @@ import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; */ public interface SpiExpression extends Expression { - /** - * Process "Many" properties populating ManyWhereJoins. - *

- * Predicates on Many properties require an extra independent - * join clause. - *

- */ + /** + * Process "Many" properties populating ManyWhereJoins. + *

+ * Predicates on Many properties require an extra independent join clause. + *

+ */ public void containsMany(BeanDescriptor desc, ManyWhereJoins whereManyJoins); /** diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index 586818857..ded6c976f 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -260,10 +260,25 @@ public interface SpiQuery extends Query { */ public Boolean isAutofetch(); -// /** -// * Return explicit forUpdate setting or null. -// */ -// public boolean isForUpdate(); + /** + * Set to true if you want to capture executed secondary queries. + */ + public void setLogSecondaryQuery(boolean logSecondaryQuery); + + /** + * Return true if executed secondary queries should be captured. + */ + public boolean isLogSecondaryQuery(); + + /** + * Return the list of secondary queries that were executed. + */ + public List> getLoggedSecondaryQueries(); + + /** + * Log an executed secondary query. + */ + public void logSecondaryQuery(SpiQuery query); /** * If return null then no autoFetch profiling for this query. If a diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultBeanLoader.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultBeanLoader.java index 43f5bbc33..362c91aec 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultBeanLoader.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultBeanLoader.java @@ -140,6 +140,9 @@ public class DefaultBeanLoader { desc.cacheManyPropPut(many, bc, parentId); } } + + // log the query (for testing secondary queries) + loadRequest.logSecondaryQuery(query); } public void loadMany(BeanCollection bc, boolean onlyIds) { @@ -322,6 +325,9 @@ public class DefaultBeanLoader { // necessary but allow processing to continue until it is accessed by client code ebis[i].checkLazyLoadFailure(); } + + // log the query (for testing secondary queries) + loadRequest.logSecondaryQuery(query); } public void refresh(EntityBean bean) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java index d5eeb86df..c9ed30418 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/OrmQueryRequest.java @@ -54,7 +54,7 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe private HashQuery cacheKey; private HashQueryPlan queryPlanHash; - + /** * Create the InternalQueryRequest. */ @@ -363,4 +363,11 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe beanDescriptor.flushPersistenceContextOnIterate(persistenceContext); } + /** + * Return true if the request wants to log the secondary queries (test purpose). + */ + public boolean isLogSecondaryQuery() { + return query.isLogSecondaryQuery(); + } + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java index 97af4fcfa..b48bb7767 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -27,6 +27,7 @@ import com.avaje.ebeaninternal.server.el.ElPropertyChainBuilder; import com.avaje.ebeaninternal.server.el.ElPropertyValue; import com.avaje.ebeaninternal.server.lib.util.StringHelper; import com.avaje.ebeaninternal.server.query.SqlBeanLoad; +import com.avaje.ebeaninternal.server.query.SqlJoinType; import com.avaje.ebeaninternal.server.reflect.BeanReflectGetter; import com.avaje.ebeaninternal.server.reflect.BeanReflectSetter; import com.avaje.ebeaninternal.server.text.json.ReadJsonContext; @@ -509,14 +510,14 @@ public class BeanProperty implements ElPropertyValue { * Add any extra joins required to support this property. Generally a no * operation except for a OneToOne exported. */ - public void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { + public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { if (formula && sqlFormulaJoin != null) { - ctx.appendFormulaJoin(sqlFormulaJoin, forceOuterJoin); + ctx.appendFormulaJoin(sqlFormulaJoin, joinType); } else if (secondaryTableJoin != null) { String relativePrefix = ctx.getRelativePrefix(secondaryTableJoinPrefix); - secondaryTableJoin.addJoin(forceOuterJoin, relativePrefix, ctx); + secondaryTableJoin.addJoin(joinType, relativePrefix, ctx); } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssoc.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssoc.java index 54583bb77..a8dbae9e6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssoc.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssoc.java @@ -16,6 +16,7 @@ import com.avaje.ebeaninternal.server.deploy.id.ImportedIdSimple; import com.avaje.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssoc; import com.avaje.ebeaninternal.server.el.ElPropertyChainBuilder; import com.avaje.ebeaninternal.server.el.ElPropertyValue; +import com.avaje.ebeaninternal.server.query.SqlJoinType; /** * Abstract base for properties mapped to an associated bean, list, set or map. @@ -129,22 +130,15 @@ public abstract class BeanPropertyAssoc extends BeanProperty { /** * Add table join with table alias based on prefix. */ - public boolean addJoin(boolean forceOuterJoin, String prefix, DbSqlContext ctx) { - return tableJoin.addJoin(forceOuterJoin, prefix, ctx); + public SqlJoinType addJoin(SqlJoinType joinType, String prefix, DbSqlContext ctx) { + return tableJoin.addJoin(joinType, prefix, ctx); } /** * Add table join with explicit table alias. */ - public boolean addJoin(boolean forceOuterJoin, String a1, String a2, DbSqlContext ctx) { - return tableJoin.addJoin(forceOuterJoin, a1, a2, ctx); - } - - /** - * Add table join with explicit table alias. - */ - public void addInnerJoin(String a1, String a2, DbSqlContext ctx) { - tableJoin.addInnerJoin(a1, a2, ctx); + public SqlJoinType addJoin(SqlJoinType joinType, String a1, String a2, DbSqlContext ctx) { + return tableJoin.addJoin(joinType, a1, a2, ctx); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index 3ca52206c..7d9b620bc 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -23,6 +23,7 @@ import com.avaje.ebeaninternal.server.el.ElPropertyChainBuilder; import com.avaje.ebeaninternal.server.el.ElPropertyValue; import com.avaje.ebeaninternal.server.query.SplitName; import com.avaje.ebeaninternal.server.query.SqlBeanLoad; +import com.avaje.ebeaninternal.server.query.SqlJoinType; import com.avaje.ebeaninternal.server.text.json.ReadJsonContext; import com.avaje.ebeaninternal.server.text.json.WriteJsonContext; @@ -508,9 +509,9 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { } @Override - public void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { + public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { if (!isTransient) { - localHelp.appendFrom(ctx, forceOuterJoin); + localHelp.appendFrom(ctx, joinType); } } @@ -587,7 +588,7 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { abstract void appendSelect(DbSqlContext ctx, boolean subQuery); - abstract void appendFrom(DbSqlContext ctx, boolean forceOuterJoin); + abstract void appendFrom(DbSqlContext ctx, SqlJoinType joinType); } @@ -633,7 +634,7 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { } @Override - void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { + void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { } @Override @@ -729,11 +730,11 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { } @Override - void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { + void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { if (targetInheritInfo != null) { // add join to support the discriminator column String relativePrefix = ctx.getRelativePrefix(name); - tableJoin.addJoin(forceOuterJoin, relativePrefix, ctx); + tableJoin.addJoin(joinType, relativePrefix, ctx); } } @@ -822,10 +823,10 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { } @Override - void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { + void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { String relativePrefix = ctx.getRelativePrefix(getName()); - tableJoin.addJoin(forceOuterJoin, relativePrefix, ctx); + tableJoin.addJoin(joinType, relativePrefix, ctx); } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java index 48dcbc831..4ae93ea88 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/DbSqlContext.java @@ -1,5 +1,7 @@ package com.avaje.ebeaninternal.server.deploy; +import com.avaje.ebeaninternal.server.query.SqlJoinType; + /** * Used to provide context during sql construction. */ @@ -72,7 +74,7 @@ public interface DbSqlContext { * Append a Sql Formula join. This converts the "${ta}" keyword to the current * table alias. */ - public void appendFormulaJoin(String sqlFormulaJoin, boolean forceOuterJoin); + public void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType); /** * Return the current content length. diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/TableJoin.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/TableJoin.java index 8e42d50bd..e1a89bdfa 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/TableJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/TableJoin.java @@ -10,6 +10,7 @@ import com.avaje.ebeaninternal.server.deploy.meta.DeployTableJoin; import com.avaje.ebeaninternal.server.deploy.meta.DeployTableJoinColumn; import com.avaje.ebeaninternal.server.query.SplitName; import com.avaje.ebeaninternal.server.query.SqlBeanLoad; +import com.avaje.ebeaninternal.server.query.SqlJoinType; /** * Represents a join to another table. @@ -32,9 +33,9 @@ public final class TableJoin { private final String table; /** - * The type of join. LEFT OUTER etc. + * The type of join as per deployment (cardinality and optionality). */ - private final String type; + private final SqlJoinType type; /** * The persist cascade info. @@ -60,7 +61,7 @@ public final class TableJoin { this.importedPrimaryKey = deploy.isImportedPrimaryKey(); this.table = InternString.intern(deploy.getTable()); - this.type = InternString.intern(deploy.getType()); + this.type = deploy.getType(); this.cascadeInfo = deploy.getCascadeInfo(); this.inheritInfo = deploy.getInheritInfo(); @@ -149,7 +150,7 @@ public final class TableJoin { /** * Return the type of join. LEFT OUTER JOIN etc. */ - public String getType() { + public SqlJoinType getType() { return type; } @@ -157,32 +158,26 @@ public final class TableJoin { * Return true if this join is a left outer join. */ public boolean isOuterJoin() { - return type.equals(LEFT_OUTER); + return type == SqlJoinType.OUTER; } - public boolean addJoin(boolean forceOuterJoin, String prefix, DbSqlContext ctx) { + public SqlJoinType addJoin(SqlJoinType joinType, String prefix, DbSqlContext ctx) { String[] names = SplitName.split(prefix); String a1 = ctx.getTableAlias(names[0]); String a2 = ctx.getTableAlias(prefix); - return addJoin(forceOuterJoin, a1, a2, ctx); + return addJoin(joinType, a1, a2, ctx); } - public boolean addJoin(boolean forceOuterJoin, String a1, String a2, DbSqlContext ctx) { + public SqlJoinType addJoin(SqlJoinType joinType, String a1, String a2, DbSqlContext ctx) { String inheritance = inheritInfo != null ? inheritInfo.getWhere() : null; - ctx.addJoin(forceOuterJoin?LEFT_OUTER:type, table, columns(), a1, a2, inheritance); + String joinLiteral = joinType.getLiteral(type); + ctx.addJoin(joinLiteral, table, columns(), a1, a2, inheritance); - return forceOuterJoin || LEFT_OUTER.equals(type); - } - - /** - * Explicitly add a (non-outer) join. - */ - public void addInnerJoin(String a1, String a2, DbSqlContext ctx) { - String inheritance = inheritInfo != null ? inheritInfo.getWhere() : null; - ctx.addJoin(JOIN, table, columns(), a1, a2, inheritance); + return joinType.autoToOuter(type); } + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployTableJoin.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployTableJoin.java index d77c21d0c..02bb17492 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployTableJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployTableJoin.java @@ -9,6 +9,7 @@ import com.avaje.ebeaninternal.server.deploy.BeanCascadeInfo; import com.avaje.ebeaninternal.server.deploy.BeanTable; import com.avaje.ebeaninternal.server.deploy.InheritInfo; import com.avaje.ebeaninternal.server.deploy.TableJoin; +import com.avaje.ebeaninternal.server.query.SqlJoinType; /** * Represents a join to another table during deployment phase. @@ -32,7 +33,7 @@ public class DeployTableJoin { /** * The type of join. LEFT OUTER etc. */ - private String type = TableJoin.JOIN; + private SqlJoinType type = SqlJoinType.INNER; /** * The list of properties mapped to this joined table. @@ -162,7 +163,7 @@ public class DeployTableJoin { /** * Return the type of join. LEFT OUTER JOIN etc. */ - public String getType() { + public SqlJoinType getType() { return type; } @@ -170,7 +171,11 @@ public class DeployTableJoin { * Return true if this join is a left outer join. */ public boolean isOuterJoin() { - return type.equals(TableJoin.LEFT_OUTER); + return type == SqlJoinType.OUTER; + } + + private void setType(SqlJoinType type) { + this.type = type; } /** @@ -179,13 +184,13 @@ public class DeployTableJoin { public void setType(String joinType) { joinType = joinType.toUpperCase(); if (joinType.equalsIgnoreCase(TableJoin.JOIN)) { - type = TableJoin.JOIN; + type = SqlJoinType.INNER; } else if (joinType.indexOf("LEFT") > -1) { - type = TableJoin.LEFT_OUTER; + type = SqlJoinType.OUTER; } else if (joinType.indexOf("OUTER") > -1) { - type = TableJoin.LEFT_OUTER; + type = SqlJoinType.OUTER; } else if (joinType.indexOf("INNER") > -1) { - type = TableJoin.JOIN; + type = SqlJoinType.INNER; } else { throw new RuntimeException(Message.msg("join.type.unknown", joinType)); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java index c55eac335..87d3818d9 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java @@ -40,7 +40,7 @@ abstract class JunctionExpression implements Junction, SpiExpression, Expr private static final long serialVersionUID = -645619859900030678L; Conjunction(com.avaje.ebean.Query query, ExpressionList parent) { - super(AND, query, parent); + super(false, AND, query, parent); } } @@ -49,17 +49,21 @@ abstract class JunctionExpression implements Junction, SpiExpression, Expr private static final long serialVersionUID = -8464470066692221413L; Disjunction(com.avaje.ebean.Query query, ExpressionList parent) { - super(OR, query, parent); + super(true, OR, query, parent); } } - // private final ArrayList list = new - // ArrayList(); private final DefaultExpressionList exprList; private final String joinType; - JunctionExpression(String joinType, com.avaje.ebean.Query query, ExpressionList parent) { + /** + * If true then a disjunction which means outer joins are required. + */ + private final boolean disjunction; + + JunctionExpression(boolean disjunction, String joinType, com.avaje.ebean.Query query, ExpressionList parent) { + this.disjunction = disjunction; this.joinType = joinType; this.exprList = new DefaultExpressionList(query, parent); } @@ -68,9 +72,20 @@ abstract class JunctionExpression implements Junction, SpiExpression, Expr List list = exprList.internalList(); + // get the current state for 'require outer joins' + boolean parentOuterJoins = manyWhereJoin.isRequireOuterJoins(); + if (disjunction) { + // turn on outer joins required for disjunction expressions + manyWhereJoin.setRequireOuterJoins(true); + } + for (int i = 0; i < list.size(); i++) { list.get(i).containsMany(desc, manyWhereJoin); } + if (disjunction && !parentOuterJoins) { + // restore state to not forcing outer joins + manyWhereJoin.setRequireOuterJoins(false); + } } public Junction add(Expression item) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBaseContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBaseContext.java index 674339a27..834c53f08 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBaseContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBaseContext.java @@ -56,6 +56,13 @@ public abstract class DLoadBaseContext { if (queryProps == null) { return batchSize; } + + int queryFetchBatch = queryProps.getQueryFetchBatch(); + if (queryFetchBatch > 0) { + // property join was automatically set to a 'query join' + return queryFetchBatch; + } + FetchConfig fetchConfig = queryProps.getFetchConfig(); if (fetchConfig == null) { return batchSize; diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java index 10bd7d196..ba9f12c0e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadBeanContext.java @@ -29,8 +29,8 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex super(parent, desc, path, defaultBatchSize, queryProps); + this.bufferList = new ArrayList(); this.currentBuffer = createBuffer(firstBatchSize); - this.bufferList = queryFetch ? new ArrayList() : null; } protected void configureQuery(SpiQuery query, String lazyLoadProperty) { @@ -78,7 +78,7 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex for (LoadBuffer loadBuffer : bufferList) { if (!loadBuffer.list.isEmpty()) { boolean loadCache = false; - LoadBeanRequest req = new LoadBeanRequest(loadBuffer, parentRequest.getTransaction(), false, null, loadCache); + LoadBeanRequest req = new LoadBeanRequest(loadBuffer, parentRequest, false, null, loadCache); parent.getEbeanServer().loadBean(req); if (!queryProps.isQueryFetchAll()) { @@ -173,7 +173,7 @@ public class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContex } } - LoadBeanRequest req = new LoadBeanRequest(this, null, true, ebi.getLazyLoadProperty(), context.hitCache); + LoadBeanRequest req = new LoadBeanRequest(this, true, ebi.getLazyLoadProperty(), context.hitCache); context.desc.getEbeanServer().loadBean(req); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java index 394c4abdc..9ce7a6000 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/loadcontext/DLoadManyContext.java @@ -91,7 +91,7 @@ public class DLoadManyContext extends DLoadBaseContext implements LoadManyContex if (bufferList != null) { for (LoadBuffer loadBuffer : bufferList) { if (!loadBuffer.list.isEmpty()) { - LoadManyRequest req = new LoadManyRequest(loadBuffer, parentRequest.getTransaction(), requestedBatchSize, false, false, false); + LoadManyRequest req = new LoadManyRequest(loadBuffer, parentRequest, requestedBatchSize, false, false, false); parent.getEbeanServer().loadMany(req); if (!queryProps.isQueryFetchAll()) { // Stop - only fetch the first batch ... the rest will be lazy loaded @@ -187,7 +187,7 @@ public class DLoadManyContext extends DLoadBaseContext implements LoadManyContex // Should reduce the list by checking each beanCollection in the L2 first before executing the query - LoadManyRequest req = new LoadManyRequest(this, null, batchSize, true, onlyIds, useCache); + LoadManyRequest req = new LoadManyRequest(this, batchSize, true, onlyIds, useCache); context.parent.getEbeanServer().loadMany(req); } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java index 6adbf6e84..ecc1118a8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -175,12 +175,11 @@ public class DefaultDbSqlContext implements DbSqlContext { return this; } - public void appendFormulaJoin(String sqlFormulaJoin, boolean forceOuterJoin) { + public void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType) { // replace ${ta} place holder with the real table alias... String tableAlias = tableAliasStack.peek(); - String converted = StringHelper - .replaceString(sqlFormulaJoin, tableAliasPlaceHolder, tableAlias); + String converted = StringHelper.replaceString(sqlFormulaJoin, tableAliasPlaceHolder, tableAlias); if (formulaJoins == null) { formulaJoins = new HashSet(); @@ -195,7 +194,7 @@ public class DefaultDbSqlContext implements DbSqlContext { formulaJoins.add(converted); sb.append(" "); - if (forceOuterJoin) { + if (joinType == SqlJoinType.OUTER) { if ("join".equals(sqlFormulaJoin.substring(0, 4).toLowerCase())) { // prepend left outer as we are in the 'many' part append(" left outer "); diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlJoinType.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlJoinType.java new file mode 100644 index 000000000..49daa320f --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlJoinType.java @@ -0,0 +1,70 @@ +package com.avaje.ebeaninternal.server.query; + +/** + * Inner join, Outer join or automatic determination based on cardinality and optionality. + */ +public enum SqlJoinType { + + /** + * It is an inner join. + */ + INNER("join"), + + /** + * It is an outer join. + */ + OUTER("left outer join"), + + /** + * It is automatically determined based on cardinality and optionality. + */ + AUTO("JOIN-TYPE-AUTO-LITERAL-NOT-USED"); + + String literal; + + SqlJoinType(String literal) { + this.literal = literal; + } + + /** + * Return the SQL join literal. + */ + public String getLiteral() { + return literal; + } + + /** + * Return the actual SQL join literal taking into account the current join type and the 'default + * join type as per deployment cardinality and optionality'. + */ + public String getLiteral(SqlJoinType deploymentJoinType) { + if (this == SqlJoinType.AUTO) { + return deploymentJoinType.getLiteral(); + } + return this.getLiteral(); + } + + /** + * If this is an AUTO join set it to OUTER as we are joining to a Many. + */ + public SqlJoinType autoToOuter() { + if (this == AUTO) { + return OUTER; + } else { + return this; + } + } + + /** + * If join is AUTO but deploymentJoinType is OUTER then go into OUTER join mode. + */ + public SqlJoinType autoToOuter(SqlJoinType deploymentJoinType) { + if (this == AUTO && deploymentJoinType == OUTER) { + return OUTER; + } else { + return this; + } + } + +} + diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java index 10372060e..d1ae6cfc1 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeBuilder.java @@ -10,7 +10,11 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.avaje.ebeaninternal.api.ManyWhereJoins; +import com.avaje.ebeaninternal.api.PropertyJoin; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.api.SpiQuery.Type; import com.avaje.ebeaninternal.server.core.OrmQueryRequest; @@ -25,9 +29,6 @@ import com.avaje.ebeaninternal.server.el.ElPropertyValue; import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail; import com.avaje.ebeaninternal.server.querydefn.OrmQueryProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Factory for SqlTree. */ @@ -169,7 +170,7 @@ public class SqlTreeBuilder { if (rawSql) { return "Not Used"; } - rootNode.appendFrom(ctx, false); + rootNode.appendFrom(ctx, SqlJoinType.AUTO); return ctx.getContent(); } @@ -181,7 +182,7 @@ public class SqlTreeBuilder { if (!rawSql) { alias.addJoin(queryDetail.getIncludes(), desc); alias.addJoin(predicates.getPredicateIncludes(), desc); - alias.addManyWhereJoins(manyWhereJoins.getJoins()); + alias.addManyWhereJoins(manyWhereJoins.getPropertyNames()); // build set of table alias alias.buildAlias(); @@ -237,10 +238,10 @@ public class SqlTreeBuilder { */ private void addManyWhereJoins(List myJoinList) { - Set includes = manyWhereJoins.getJoins(); - for (String joinProp : includes) { - BeanPropertyAssoc beanProperty = (BeanPropertyAssoc) desc.getBeanPropertyFromPath(joinProp); - SqlTreeNodeManyWhereJoin nodeJoin = new SqlTreeNodeManyWhereJoin(joinProp, beanProperty); + Collection includes = manyWhereJoins.getPropertyJoins(); + for (PropertyJoin joinProp : includes) { + BeanPropertyAssoc beanProperty = (BeanPropertyAssoc) desc.getBeanPropertyFromPath(joinProp.getProperty()); + SqlTreeNodeManyWhereJoin nodeJoin = new SqlTreeNodeManyWhereJoin(joinProp.getProperty(), beanProperty, joinProp.getSqlJoinType()); myJoinList.add(nodeJoin); } } @@ -289,12 +290,11 @@ public class SqlTreeBuilder { // support the predicates or order by clauses. // remove ManyWhereJoins from the predicateIncludes - predicateIncludes.removeAll(manyWhereJoins.getJoins()); + predicateIncludes.removeAll(manyWhereJoins.getPropertyNames()); // look for predicateIncludes that are not in selectIncludes and add // them as extra joins to the query - IncludesDistiller extraJoinDistill = new IncludesDistiller(desc, selectIncludes, - predicateIncludes); + IncludesDistiller extraJoinDistill = new IncludesDistiller(desc, selectIncludes, predicateIncludes); Collection extraJoins = extraJoinDistill.getExtraJoinRootNodes(); if (extraJoins.isEmpty()) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java index 71b61f980..60ee2a566 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNode.java @@ -28,7 +28,7 @@ public interface SqlTreeNode { /** * Append to the FROM part of the sql. */ - public void appendFrom(DbSqlContext ctx, boolean forceOuterJoin); + public void appendFrom(DbSqlContext ctx, SqlJoinType joinType); /** * Append any where predicates for inheritance. diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java index 09bea9ed1..080cf2dc7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -28,50 +28,47 @@ public class SqlTreeNodeBean implements SqlTreeNode { private static final SqlTreeNode[] NO_CHILDREN = new SqlTreeNode[0]; - final BeanDescriptor desc; + protected final BeanDescriptor desc; - final IdBinder idBinder; + protected final IdBinder idBinder; /** * The children which will be other SelectBean or SelectProxyBean. */ - final SqlTreeNode[] children; + protected final SqlTreeNode[] children; - final boolean readOnlyLeaf; + protected final boolean readOnlyLeaf; /** * Set to true if this is a partial object fetch. */ - final boolean partialObject; + protected final boolean partialObject; - - final BeanProperty[] properties; + protected final BeanProperty[] properties; /** * Extra where clause added by Where annotation on associated many. */ - final String extraWhere; + protected final String extraWhere; - final BeanPropertyAssoc nodeBeanProp; + protected final BeanPropertyAssoc nodeBeanProp; - final TableJoin[] tableJoins; + protected final TableJoin[] tableJoins; /** * False if report bean and has no id property. */ - final boolean readId; + protected final boolean readId; - final boolean disableLazyLoad; + protected final boolean disableLazyLoad; - final InheritInfo inheritInfo; + protected final InheritInfo inheritInfo; - final String prefix; - - - final Map pathMap; + protected final String prefix; + protected final Map pathMap; - final BeanPropertyAssocMany lazyLoadParent; + protected final BeanPropertyAssocMany lazyLoadParent; public SqlTreeNodeBean(String prefix, BeanPropertyAssoc beanProp, SqlTreeProperties props, List myChildren, boolean withId) { @@ -452,20 +449,21 @@ public class SqlTreeNodeBean implements SqlTreeNode { /** * Append to the FROM clause for this node. */ - public void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { + public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { ctx.pushJoin(prefix); ctx.pushTableAlias(prefix); - forceOuterJoin = appendFromBaseTable(ctx, forceOuterJoin); + // join and return SqlJoinType to use for child joins + joinType = appendFromBaseTable(ctx, joinType); for (int i = 0; i < properties.length; i++) { // usually nothing... except for 1-1 Exported - properties[i].appendFrom(ctx, forceOuterJoin); + properties[i].appendFrom(ctx, joinType); } for (int i = 0; i < children.length; i++) { - children[i].appendFrom(ctx, forceOuterJoin); + children[i].appendFrom(ctx, joinType); } ctx.popTableAlias(); @@ -476,7 +474,7 @@ public class SqlTreeNodeBean implements SqlTreeNode { * Join to base table for this node. This includes a join to the intersection * table if this is a ManyToMany node. */ - public boolean appendFromBaseTable(DbSqlContext ctx, boolean forceOuterJoin) { + public SqlJoinType appendFromBaseTable(DbSqlContext ctx, SqlJoinType joinType) { if (nodeBeanProp instanceof BeanPropertyAssocMany) { BeanPropertyAssocMany manyProp = (BeanPropertyAssocMany) nodeBeanProp; @@ -488,14 +486,14 @@ public class SqlTreeNodeBean implements SqlTreeNode { String alias2 = alias + "z_"; TableJoin manyToManyJoin = manyProp.getIntersectionTableJoin(); - manyToManyJoin.addJoin(forceOuterJoin, parentAlias, alias2, ctx); + manyToManyJoin.addJoin(joinType, parentAlias, alias2, ctx); - return nodeBeanProp.addJoin(forceOuterJoin, alias2, alias, ctx); + return nodeBeanProp.addJoin(joinType, alias2, alias, ctx); } } - return nodeBeanProp.addJoin(forceOuterJoin, prefix, ctx); + return nodeBeanProp.addJoin(joinType, prefix, ctx); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java index 53841fbbe..573586329 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java @@ -22,13 +22,13 @@ import com.avaje.ebeaninternal.server.deploy.TableJoin; public class SqlTreeNodeExtraJoin implements SqlTreeNode { - final BeanPropertyAssoc assocBeanProperty; + private final BeanPropertyAssoc assocBeanProperty; - final String prefix; + private final String prefix; - final boolean manyJoin; + private final boolean manyJoin; - List children; + private List children; public SqlTreeNodeExtraJoin(String prefix, BeanPropertyAssoc assocBeanProperty) { this.prefix = prefix; @@ -66,7 +66,7 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode { children.add(child); } - public void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { + public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { boolean manyToMany = false; @@ -82,29 +82,29 @@ public class SqlTreeNodeExtraJoin implements SqlTreeNode { String alias2 = alias+"z_"; TableJoin manyToManyJoin = manyProp.getIntersectionTableJoin(); - manyToManyJoin.addJoin(forceOuterJoin, parentAlias, alias2, ctx); + manyToManyJoin.addJoin(joinType, parentAlias, alias2, ctx); - assocBeanProperty.addJoin(forceOuterJoin, alias2, alias, ctx); + assocBeanProperty.addJoin(joinType, alias2, alias, ctx); } } if (!manyToMany){ - assocBeanProperty.addJoin(forceOuterJoin, prefix, ctx); + assocBeanProperty.addJoin(joinType, prefix, ctx); } - - if (children != null){ - - if (manyJoin){ - // make sure all decendants use OUTER JOIN - forceOuterJoin = true; - } - - for (int i = 0; i < children.size(); i++) { - SqlTreeNodeExtraJoin child = children.get(i); - child.appendFrom(ctx, forceOuterJoin); - } - } - } + + if (children != null) { + + if (manyJoin) { + // if AUTO then make all decendants use OUTER JOIN + joinType = joinType.autoToOuter(); + } + + for (int i = 0; i < children.size(); i++) { + SqlTreeNodeExtraJoin child = children.get(i); + child.appendFrom(ctx, joinType); + } + } + } /** * Does nothing. diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java index 4189f1c5e..c875e80aa 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyRoot.java @@ -34,8 +34,8 @@ public final class SqlTreeNodeManyRoot extends SqlTreeNodeBean { * Force outer join for everything after the many property. */ @Override - public void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { - super.appendFrom(ctx, true); + public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { + super.appendFrom(ctx, joinType.autoToOuter()); } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java index c01ebae40..b739c2b9d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java @@ -14,20 +14,30 @@ import com.avaje.ebeaninternal.server.deploy.TableJoin; /** * Join to Many (or child of a many) to support where clause predicates on many properties. - * - * @author rbygrave */ public class SqlTreeNodeManyWhereJoin implements SqlTreeNode { private final String parentPrefix; + private final String prefix; - private final BeanPropertyAssoc nodeBeanProp; - private final SqlTreeNode[] children; - public SqlTreeNodeManyWhereJoin(String prefix, BeanPropertyAssoc prop) { + private final BeanPropertyAssoc nodeBeanProp; + + /** + * Child joins. + */ + private final SqlTreeNode[] children; + + /** + * The many where join which is either INNER or OUTER. + */ + private final SqlJoinType manyJoinType; + + public SqlTreeNodeManyWhereJoin(String prefix, BeanPropertyAssoc prop, SqlJoinType manyJoinType) { this.nodeBeanProp = prop; this.prefix = prefix; + this.manyJoinType = manyJoinType; String[] split = SplitName.split(prefix); this.parentPrefix = split[0]; @@ -39,12 +49,15 @@ public class SqlTreeNodeManyWhereJoin implements SqlTreeNode { /** * Append to the FROM clause for this node. */ - public void appendFrom(DbSqlContext ctx, boolean forceOuterJoin) { + @Override + public void appendFrom(DbSqlContext ctx, SqlJoinType currentJoinType) { - appendFromBaseTable(ctx, forceOuterJoin); + // always use the join type as per this many where join + // (OUTER for disjunction and otherwise INNER) + appendFromBaseTable(ctx, manyJoinType); for (int i = 0; i < children.length; i++) { - children[i].appendFrom(ctx, forceOuterJoin); + children[i].appendFrom(ctx, manyJoinType); } } @@ -52,25 +65,25 @@ public class SqlTreeNodeManyWhereJoin implements SqlTreeNode { * Join to base table for this node. This includes a join to the * intersection table if this is a ManyToMany node. */ - public void appendFromBaseTable(DbSqlContext ctx, boolean forceOuterJoin) { + public void appendFromBaseTable(DbSqlContext ctx, SqlJoinType joinType) { String alias = ctx.getTableAliasManyWhere(prefix); String parentAlias = ctx.getTableAliasManyWhere(parentPrefix); if (nodeBeanProp instanceof BeanPropertyAssocOne){ - nodeBeanProp.addInnerJoin(parentAlias, alias, ctx); + nodeBeanProp.addJoin(joinType, parentAlias, alias, ctx); } else { BeanPropertyAssocMany manyProp = (BeanPropertyAssocMany)nodeBeanProp; if (!manyProp.isManyToMany()) { - manyProp.addInnerJoin(parentAlias, alias, ctx); + manyProp.addJoin(joinType, parentAlias, alias, ctx); } else { String alias2 = alias + "z_"; TableJoin manyToManyJoin = manyProp.getIntersectionTableJoin(); - manyToManyJoin.addInnerJoin(parentAlias, alias2, ctx); - manyProp.addInnerJoin(alias2, alias, ctx); + manyToManyJoin.addJoin(joinType, parentAlias, alias2, ctx); + manyProp.addJoin(joinType, alias2, alias, ctx); } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java index 221a335a8..83344b06b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SqlTreeNodeRoot.java @@ -40,7 +40,7 @@ public final class SqlTreeNodeRoot extends SqlTreeNodeBean { * For the root node there is no join type or on clause etc. */ @Override - public boolean appendFromBaseTable(DbSqlContext ctx, boolean forceOuterJoin) { + public SqlJoinType appendFromBaseTable(DbSqlContext ctx, SqlJoinType joinType) { ctx.append(desc.getBaseTable()); ctx.append(" ").append(ctx.getTableAlias(null)); @@ -48,10 +48,10 @@ public final class SqlTreeNodeRoot extends SqlTreeNodeBean { if (includeJoin != null) { String a1 = ctx.getTableAlias(null); String a2 = "int_"; // unique alias for intersection join - includeJoin.addJoin(forceOuterJoin, a1, a2, ctx); + includeJoin.addJoin(joinType, a1, a2, ctx); } - return forceOuterJoin; + return joinType; } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index c4062befe..e914676df 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -191,6 +191,8 @@ public class DefaultOrmQuery implements SpiQuery { */ private boolean autoFetchTuned; + private boolean logSecondaryQuery; + /** * The node of the bean or collection that fired lazy loading. Not null if * profiling is on and this query is for lazy loading. Used to hook back a @@ -585,8 +587,31 @@ public class DefaultOrmQuery implements SpiQuery { public void setUsageProfiling(boolean usageProfiling) { this.usageProfiling = usageProfiling; } + + public void setLogSecondaryQuery(boolean logSecondaryQuery) { + this.logSecondaryQuery = logSecondaryQuery; + } + + public boolean isLogSecondaryQuery() { + return logSecondaryQuery; + } - public void setParentNode(ObjectGraphNode parentNode) { + private List> loggedSecondaryQueries; + + @Override + public List> getLoggedSecondaryQueries() { + return loggedSecondaryQueries; + } + + public void logSecondaryQuery(SpiQuery query) { + if (loggedSecondaryQueries == null) { + loggedSecondaryQueries = new ArrayList>(); + } + loggedSecondaryQueries.add(query); + } + + + public void setParentNode(ObjectGraphNode parentNode) { this.parentNode = parentNode; } diff --git a/src/test/java/com/avaje/tests/basic/TestOrderTotalAmountFormula.java b/src/test/java/com/avaje/tests/basic/TestOrderTotalAmountFormula.java index 7ef6340e0..bf985b22e 100644 --- a/src/test/java/com/avaje/tests/basic/TestOrderTotalAmountFormula.java +++ b/src/test/java/com/avaje/tests/basic/TestOrderTotalAmountFormula.java @@ -24,13 +24,14 @@ public class TestOrderTotalAmountFormula extends BaseTestCase { .findList(); for (Customer c0 : l0) { - System.out.println("customer: " + c0.getId()); + c0.getId(); List orders = c0.getOrders(); for (Order order : orders) { - System.out.println("... order:" + order); + order.getId(); } } } + } diff --git a/src/test/java/com/avaje/tests/batchload/TestSecondaryQueries.java b/src/test/java/com/avaje/tests/batchload/TestSecondaryQueries.java index 81e71073c..76f39f9bb 100644 --- a/src/test/java/com/avaje/tests/batchload/TestSecondaryQueries.java +++ b/src/test/java/com/avaje/tests/batchload/TestSecondaryQueries.java @@ -7,6 +7,9 @@ import org.junit.Test; import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; +import com.avaje.ebean.FetchConfig; +import com.avaje.ebean.Query; +import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.tests.model.basic.Customer; import com.avaje.tests.model.basic.Order; import com.avaje.tests.model.basic.ResetBasicData; @@ -14,23 +17,93 @@ import com.avaje.tests.model.basic.ResetBasicData; public class TestSecondaryQueries extends BaseTestCase { @Test - public void testQueries() { + public void testSecQueryOneToMany() { ResetBasicData.reset(); Order testOrder = ResetBasicData.createOrderCustAndOrder("testSecQry10"); Integer custId = testOrder.getCustomer().getId(); - Customer cust = Ebean.find(Customer.class).select("name").fetch("contacts", "+query") - .setId(custId).findUnique(); + Query query = Ebean.find(Customer.class) + .select("name") + .fetch("contacts", "+query") + .setId(custId); + SpiQuery spiQuery = (SpiQuery)query; + spiQuery.setLogSecondaryQuery(true); + + Customer cust = query.findUnique(); + Assert.assertNotNull(cust); + String generatedSql = query.getGeneratedSql(); + Assert.assertTrue(generatedSql.contains("from o_customer t0 where t0.id = ?")); + + List> loggedSecondaryQueries = spiQuery.getLoggedSecondaryQueries(); + Assert.assertEquals(1, loggedSecondaryQueries.size()); + + SpiQuery secondaryQuery = loggedSecondaryQueries.get(0); + String secondarySql = secondaryQuery.getGeneratedSql(); + + Assert.assertTrue(secondarySql.contains("from contact t0 where (t0.customer_id) in (?)")); + } + + + @Test + public void testManyToOneWithManyPlusOneToMany() { - List list = Ebean.find(Order.class).select("status").fetch("details", "+query(10)") - .fetch("customer", "+query name, status").fetch("customer.contacts").where() - .eq("status", Order.Status.NEW).findList(); + ResetBasicData.reset(); + Query query = Ebean.find(Order.class) + .select("status") + .fetch("customer", "name, status", new FetchConfig().query()) + .fetch("customer.contacts") + .fetch("details", new FetchConfig().query()) + .where().eq("status", Order.Status.NEW) + .query(); + +// .fetch("customer", "+query name, status") +// .fetch("details", "+query(10)") + + SpiQuery spiQuery = (SpiQuery)query; + spiQuery.setLogSecondaryQuery(true); + + List list = query.findList(); Assert.assertTrue(list.size() > 0); + for (Order order : list) { + order.getCustomer().getStatus(); + } + + + String generatedSql = spiQuery.getGeneratedSql(); + //select t0.id c0, t0.status c1, t0.kcustomer_id c2 from o_order t0 where t0.status = ? ; --bind(NEW) + Assert.assertEquals("select t0.id c0, t0.status c1, t0.kcustomer_id c2 from o_order t0 where t0.status = ? ", generatedSql); + + + List> secondaryQueries = spiQuery.getLoggedSecondaryQueries(); + Assert.assertEquals(2, secondaryQueries.size()); + + SpiQuery custSecondaryQuery = secondaryQueries.get(0); + String custSecondarySql = custSecondaryQuery.getGeneratedSql(); + + // select t0.id c0, t0.name c1, t0.status c2, + // t1.id c3, t1.first_name c4, t1.last_name c5, t1.phone c6, t1.mobile c7, t1.email c8, t1.cretime c9, t1.updtime c10, t1.customer_id c11, t1.group_id c12 + // from o_customer t0 + // left outer join contact t1 on t1.customer_id = t0.id + // where t0.id = ? order by t0.id; --bind(1) + + Assert.assertTrue(custSecondarySql.contains("from o_customer t0 ")); + Assert.assertTrue(custSecondarySql.contains("left outer join contact t1 on t1.customer_id = t0.id ")); + Assert.assertTrue(custSecondarySql.contains("where t0.id ")); + + + SpiQuery orderDetailsSecondaryQuery = secondaryQueries.get(1); + String ordSecondarySql = orderDetailsSecondaryQuery.getGeneratedSql(); + + // select ... + // from o_order_detail t0 + // where (t0.order_id) in (?,?,?,?,?) ; --bind(1,4,1,1,1) + + Assert.assertTrue(ordSecondarySql.contains(" from o_order_detail t0 where (t0.order_id) in (?")); } } diff --git a/src/test/java/com/avaje/tests/model/basic/Contact.java b/src/test/java/com/avaje/tests/model/basic/Contact.java index 5242b3a2d..01539010b 100644 --- a/src/test/java/com/avaje/tests/model/basic/Contact.java +++ b/src/test/java/com/avaje/tests/model/basic/Contact.java @@ -28,7 +28,7 @@ public class Contact { String mobile; String email; - @ManyToOne + @ManyToOne(optional=false) Customer customer; @ManyToOne(optional=true) diff --git a/src/test/java/com/avaje/tests/query/TestIContains.java b/src/test/java/com/avaje/tests/query/TestIContains.java index cbcd38870..ac79cb41c 100644 --- a/src/test/java/com/avaje/tests/query/TestIContains.java +++ b/src/test/java/com/avaje/tests/query/TestIContains.java @@ -1,7 +1,7 @@ package com.avaje.tests.query; -import junit.framework.Assert; +import org.junit.Assert; import org.junit.Test; import com.avaje.ebean.BaseTestCase; @@ -18,18 +18,18 @@ public class TestIContains extends BaseTestCase { ResetBasicData.reset(); // case insensitive - Query q0 = Ebean.find(Customer.class).where().icontains("name", "Rob").query(); + Query query = Ebean.find(Customer.class).where().icontains("name", "Rob").query(); - q0.findList(); - String generatedSql = q0.getGeneratedSql(); + query.findList(); + String generatedSql = query.getGeneratedSql(); Assert.assertTrue(generatedSql.contains("lower(t0.name)")); // not case insensitive - q0 = Ebean.find(Customer.class).where().contains("name", "Rob").query(); + query = Ebean.find(Customer.class).where().contains("name", "Rob").query(); - q0.findList(); - generatedSql = q0.getGeneratedSql(); + query.findList(); + generatedSql = query.getGeneratedSql(); Assert.assertTrue(generatedSql.contains(" t0.name ")); diff --git a/src/test/java/com/avaje/tests/query/TestImplicitJoinOnParentRelatonship.java b/src/test/java/com/avaje/tests/query/TestImplicitJoinOnParentRelatonship.java new file mode 100644 index 000000000..735cf8fec --- /dev/null +++ b/src/test/java/com/avaje/tests/query/TestImplicitJoinOnParentRelatonship.java @@ -0,0 +1,63 @@ +package com.avaje.tests.query; + +import org.junit.Assert; +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.Query; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.ResetBasicData; + +public class TestImplicitJoinOnParentRelatonship extends BaseTestCase { + + @Test + public void test() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .select("id, name") + .where().eq("orders.details.product.name", "Desk") + .query(); + + query.findList(); + + String expectedSql = "select distinct t0.id c0, t0.name c1 from o_customer t0 join o_order u1 on u1.kcustomer_id = t0.id join o_order_detail u2 on u2.order_id = u1.id join o_product u3 on u3.id = u2.product_id where u3.name = ? "; + Assert.assertEquals(expectedSql, query.getGeneratedSql()); + + // select distinct t0.id c0, t0.name c1 + // from o_customer t0 + // join o_order u1 on u1.kcustomer_id = t0.id + // join o_order_detail u2 on u2.order_id = u1.id + // join o_product u3 on u3.id = u2.product_id + // where u3.name = ? + + } + + + @Test + public void testWithDisjunction() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .select("id, name") + .where().disjunction().eq("orders.details.product.name", "Desk").eq("id", 4).endJunction() + .query(); + + query.findList(); + + String expectedSql = "select distinct t0.id c0, t0.name c1 from o_customer t0 left outer join o_order u1 on u1.kcustomer_id = t0.id left outer join o_order_detail u2 on u2.order_id = u1.id left outer join o_product u3 on u3.id = u2.product_id where (u3.name = ? or t0.id = ? ) "; + Assert.assertEquals(expectedSql, query.getGeneratedSql()); + + // select distinct t0.id c0, t0.name c1 + // from o_customer t0 select distinct t0.id c0, t0.name c1 from o_customer t0 join o_order u1 on u1.kcustomer_id = t0.id join o_order_detail u2 on u2.order_id = u1.id join o_product u3 on u3.id = u2.product_id where u3.name = ? + // left outer join o_order u1 on u1.kcustomer_id = t0.id + // left outer join o_order_detail u2 on u2.order_id = u1.id + // left outer join o_product u3 on u3.id = u2.product_id + // where (u3.name = ? or t0.id = ? ) ; --bind(Desk,4) + + } + +} diff --git a/src/test/java/com/avaje/tests/query/TestJoinQuery.java b/src/test/java/com/avaje/tests/query/TestJoinQuery.java deleted file mode 100644 index ce1de1ff1..000000000 --- a/src/test/java/com/avaje/tests/query/TestJoinQuery.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.avaje.tests.query; - -import java.util.List; - -import junit.framework.Assert; - -import org.junit.Test; - -import com.avaje.ebean.BaseTestCase; -import com.avaje.ebean.Ebean; -import com.avaje.ebean.Query; -import com.avaje.tests.model.basic.Customer; -import com.avaje.tests.model.basic.OrderShipment; -import com.avaje.tests.model.basic.ResetBasicData; - -public class TestJoinQuery extends BaseTestCase { - - @Test - public void test() { - - ResetBasicData.reset(); - - // test that join to order.details is not included - Query query = Ebean.find(Customer.class).setAutofetch(false).fetch("orders") - .fetch("orders.details"); - - List list = query.findList(); - Assert.assertTrue("has rows", list.size() > 0); - - // test that join to order.details is not included - Query shipQuery = Ebean.find(OrderShipment.class).setAutofetch(false) - .fetch("order").fetch("order.details"); - - List shipList = shipQuery.findList(); - Assert.assertTrue("has rows", shipList.size() > 0); - } - -} diff --git a/src/test/java/com/avaje/tests/query/TestPagingListLoop.java b/src/test/java/com/avaje/tests/query/TestPagingListLoop.java deleted file mode 100644 index e02e98b9d..000000000 --- a/src/test/java/com/avaje/tests/query/TestPagingListLoop.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.avaje.tests.query; - -import java.util.concurrent.ExecutionException; - -import org.junit.Test; - -import com.avaje.ebean.BaseTestCase; -import com.avaje.ebean.Ebean; -import com.avaje.ebean.PagingList; -import com.avaje.tests.model.basic.Customer; -import com.avaje.tests.model.basic.ResetBasicData; - -public class TestPagingListLoop extends BaseTestCase { - - @Test - public void test() throws InterruptedException, ExecutionException { - - // boolean autoRunTest = false; - // if (!autoRunTest){ - // // we only want to run this test manually. - // return; - // } - - try { - ResetBasicData.reset(); - - for (int i = 0; i < 50; i++) { - PagingList pagingList = Ebean.find(Customer.class).findPagingList(10); - pagingList.getFutureRowCount();// .get(); - - // createLeak(); - Thread.sleep(10); - } - - } catch (Exception e) { - e.printStackTrace(); - } - - } - - // private void createLeak() { - // - // // create a transaction we never close ... - // // ... a connection pool leak - // Ebean.getServer(null).createTransaction(); - // } - -} diff --git a/src/test/java/com/avaje/tests/query/TestQueryFetchManyTwoDeep.java b/src/test/java/com/avaje/tests/query/TestQueryFetchManyTwoDeep.java new file mode 100644 index 000000000..a3aad61be --- /dev/null +++ b/src/test/java/com/avaje/tests/query/TestQueryFetchManyTwoDeep.java @@ -0,0 +1,162 @@ +package com.avaje.tests.query; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.Query; +import com.avaje.ebeaninternal.api.SpiQuery; +import com.avaje.tests.model.basic.Contact; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.OrderShipment; +import com.avaje.tests.model.basic.ResetBasicData; + +public class TestQueryFetchManyTwoDeep extends BaseTestCase { + + @Test + public void testFetchOneToManyWithChildOneToMany() { + + ResetBasicData.reset(); + + // test that join to order.details is not included in the initial query (included in query join) + Query query = Ebean.find(Customer.class) + .setAutofetch(false) + .fetch("orders") + .fetch("orders.details"); + + SpiQuery spiQuery = (SpiQuery)query; + spiQuery.setLogSecondaryQuery(true); + + List list = query.findList(); + Assert.assertTrue("has rows", list.size() > 0); + Assert.assertTrue(query.getGeneratedSql().contains("from o_customer t0 ")); + Assert.assertTrue(query.getGeneratedSql().contains("left outer join o_order t1 on t1.kcustomer_id = t0.id")); + Assert.assertTrue(query.getGeneratedSql().contains("left outer join o_customer t2 on t2.id = t1.kcustomer_id")); + Assert.assertFalse(query.getGeneratedSql().contains("join or_order_ship")); + + //select t0.id c0, t0.status c1, t0.name c2, t0.smallnote c3, t0.anniversary c4, t0.cretime c5, t0.updtime c6, t0.billing_address_id c7, t0.shipping_address_id c8, t1.id c9, t1.status c10, t1.order_date c11, t1.ship_date c12, + // t2.name c13, t1.cretime c14, t1.updtime c15, t1.kcustomer_id c16 + // from o_customer t0 + // left outer join o_order t1 on t1.kcustomer_id = t0.id + // left outer join o_customer t2 on t2.id = t1.kcustomer_id + // where t1.order_date is not null order by t0.id; --bind() + + + List> secondaryQueries = spiQuery.getLoggedSecondaryQueries(); + Assert.assertNotNull(secondaryQueries); + Assert.assertEquals(1, secondaryQueries.size()); + + SpiQuery secondaryQuery = secondaryQueries.get(0); + String secondarySql = secondaryQuery.getGeneratedSql(); + Assert.assertTrue(secondarySql.contains("from o_order_detail t0 where (t0.order_id) in")); + + // select t0.order_id c0, t0.id c1, t0.order_qty c2, t0.ship_qty c3, t0.unit_price c4, t0.cretime c5, t0.updtime c6, t0.order_id c7, t0.product_id c8 + // from o_order_detail t0 + // where (t0.order_id) in (?,?,?,?,?) + + } + + @Test + public void testFetchOptionalManyToOneThenDownToMany() { + + ResetBasicData.reset(); + + // test that join to order.details is not included + Query shipQuery = Ebean.find(OrderShipment.class) + .setAutofetch(false) + .fetch("order") + .fetch("order.details"); + + List shipList = shipQuery.findList(); + Assert.assertTrue("has rows", shipList.size() > 0); + + String generatedSql = shipQuery.getGeneratedSql(); + + // select ... + // from or_order_ship t0 + // left outer join o_order t1 on t1.id = t0.order_id + // left outer join o_customer t3 on t3.id = t1.kcustomer_id + // left outer join o_order_detail t2 on t2.order_id = t1.id + // where t2.id > 0 ; --bind() + + Assert.assertTrue(generatedSql.contains("from or_order_ship t0")); + // Relationship from OrderShipment to Order is optional so outer join here + Assert.assertTrue(generatedSql.contains("left outer join o_order t1 on t1.id = t0.order_id")); + Assert.assertTrue(generatedSql.contains("left outer join o_customer t3 on t3.id = t1.kcustomer_id")); + Assert.assertTrue(generatedSql.contains("left outer join o_order_detail t2 on t2.order_id = t1.id")); + + + // If OrderShipment to Order is not optional you get inner joins up to o_order_detail (which is a many) + + // select ... + // from or_order_ship t0 + // join o_order t1 on t1.id = t0.order_id + // join o_customer t3 on t3.id = t1.kcustomer_id + // left outer join o_order_detail t2 on t2.order_id = t1.id + // where t2.id > 0 ; --bind() + } + + + @Test + public void testFetchMandatoryManyToOneThenDownToMany() { + + ResetBasicData.reset(); + + // test that join to order.details is not included + Query query = Ebean.find(Contact.class) + .setAutofetch(false) + .fetch("customer") + .fetch("customer.orders"); + + List shipList = query.findList(); + Assert.assertTrue("has rows", shipList.size() > 0); + + String generatedSql = query.getGeneratedSql(); + + // select ... + // from contact t0 + // join o_customer t1 on t1.id = t0.customer_id + // left outer join o_order t2 on t2.kcustomer_id = t1.id + // left outer join o_customer t3 on t3.id = t2.kcustomer_id + // where t2.order_date is not null ; --bind() + + Assert.assertTrue(generatedSql.contains("from contact t0 ")); + // Relationship from Contact to Customer is mandatory so inner join here + Assert.assertTrue(generatedSql.contains("join o_customer t1 on t1.id = t0.customer_id")); + // outer join on many relationship 'orders' + Assert.assertTrue(generatedSql.contains("left outer join o_order t2 on t2.kcustomer_id = t1.id")); + + } + + @Test + public void testFetchMandatoryManyToOneWithPredicate() { + + ResetBasicData.reset(); + + // test that join to order.details is not included + Query query = Ebean.find(Contact.class) + .setAutofetch(false) + .fetch("customer") + .where().ilike("customer.name", "Rob%") + .query(); + + List list = query.findList(); + Assert.assertTrue("has rows", list.size() > 0); + + String generatedSql = query.getGeneratedSql(); + + // select ... + // from contact t0 + // join o_customer t1 on t1.id = t0.customer_id + // where lower(t1.name) like ? ; --bind(rob%) + + Assert.assertTrue(generatedSql.contains("from contact t0 ")); + Assert.assertTrue(generatedSql.contains("join o_customer t1 on t1.id = t0.customer_id")); + Assert.assertTrue(generatedSql.contains("where lower(t1.name) like ?")); + + } + +} diff --git a/src/test/java/com/avaje/tests/query/TestQueryFindIterate.java b/src/test/java/com/avaje/tests/query/TestQueryFindIterate.java index 6f0a27e0c..aea057179 100644 --- a/src/test/java/com/avaje/tests/query/TestQueryFindIterate.java +++ b/src/test/java/com/avaje/tests/query/TestQueryFindIterate.java @@ -6,7 +6,6 @@ import org.junit.Test; import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; import com.avaje.ebean.EbeanServer; -import com.avaje.ebean.FetchConfig; import com.avaje.ebean.Query; import com.avaje.ebean.QueryIterator; import com.avaje.tests.model.basic.Customer; diff --git a/src/test/java/com/avaje/tests/query/TestRowCount.java b/src/test/java/com/avaje/tests/query/TestRowCount.java index 852a097b3..3f6ddb969 100644 --- a/src/test/java/com/avaje/tests/query/TestRowCount.java +++ b/src/test/java/com/avaje/tests/query/TestRowCount.java @@ -2,8 +2,7 @@ package com.avaje.tests.query; import java.util.List; -import junit.framework.Assert; - +import org.junit.Assert; import org.junit.Test; import com.avaje.ebean.BaseTestCase; diff --git a/src/test/java/com/avaje/tests/query/TestDisjunctWhereOuterJoin.java b/src/test/java/com/avaje/tests/query/joins/TestDisjunctWhereOuterJoin.java similarity index 97% rename from src/test/java/com/avaje/tests/query/TestDisjunctWhereOuterJoin.java rename to src/test/java/com/avaje/tests/query/joins/TestDisjunctWhereOuterJoin.java index 824b7b97a..d46361f2d 100644 --- a/src/test/java/com/avaje/tests/query/TestDisjunctWhereOuterJoin.java +++ b/src/test/java/com/avaje/tests/query/joins/TestDisjunctWhereOuterJoin.java @@ -1,4 +1,4 @@ -package com.avaje.tests.query; +package com.avaje.tests.query.joins; import java.util.List; diff --git a/src/test/java/com/avaje/tests/query/joins/TestDisjunctWhereOuterOnMany.java b/src/test/java/com/avaje/tests/query/joins/TestDisjunctWhereOuterOnMany.java new file mode 100644 index 000000000..d0ddb8487 --- /dev/null +++ b/src/test/java/com/avaje/tests/query/joins/TestDisjunctWhereOuterOnMany.java @@ -0,0 +1,61 @@ +package com.avaje.tests.query.joins; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.Query; +import com.avaje.tests.model.basic.UUOne; +import com.avaje.tests.model.basic.UUTwo; + +public class TestDisjunctWhereOuterOnMany extends BaseTestCase { + + @Test + public void test() { + + // setup + UUOne master1 = new UUOne(); + master1.setName("testDisjOuter_1_name"); + + UUTwo detail1 = new UUTwo(); + detail1.setMaster(master1); + detail1.setName("testDisjOuter_CHILD_1"); + + UUOne master2 = new UUOne(); + master2.setName("testDisjOuter_2_name"); + + Ebean.save(master1); + Ebean.save(detail1); + Ebean.save(master2); + + + // Have outer join so that "testDisjOuter_2_name" is found + Query query = Ebean.find(UUOne.class) + .where().disjunction() + .eq("name", "testDisjOuter_2_name") + .eq("comments.name", "testDisjOuter_CHILD_1") + .endJunction() + .query(); + + List list = query.findList(); + int rowCount = query.findRowCount(); + + // select distinct t0.id c0, t0.name c1 + // from uuone t0 + // join uutwo u1 on u1.master_id = t0.id + // left outer join uutwo t1 on t1.master_id = t0.id + // where (t0.name = ? or u1.name = ? ) ; + // --bind(testDisjOuter_2_name,testDisjOuter_CHILD_1) + + Assert.assertEquals(2, list.size()); + Assert.assertEquals(2, rowCount); + + String expectedSql = "select distinct t0.id c0, t0.name c1 from uuone t0 left outer join uutwo u1 on u1.master_id = t0.id where (t0.name = ? or u1.name = ? ) "; + Assert.assertEquals(expectedSql, query.getGeneratedSql()); + + } + +} diff --git a/src/test/java/com/avaje/tests/query/TestQueryJoinManyNonRoot.java b/src/test/java/com/avaje/tests/query/joins/TestQueryJoinManyNonRoot.java similarity index 77% rename from src/test/java/com/avaje/tests/query/TestQueryJoinManyNonRoot.java rename to src/test/java/com/avaje/tests/query/joins/TestQueryJoinManyNonRoot.java index acc1fe662..c73660483 100644 --- a/src/test/java/com/avaje/tests/query/TestQueryJoinManyNonRoot.java +++ b/src/test/java/com/avaje/tests/query/joins/TestQueryJoinManyNonRoot.java @@ -1,4 +1,4 @@ -package com.avaje.tests.query; +package com.avaje.tests.query.joins; import java.util.List; @@ -30,7 +30,15 @@ public class TestQueryJoinManyNonRoot extends BaseTestCase { Assert.assertTrue(list.size() > 0); Assert.assertTrue(sql.contains("join o_customer t1 on t1.id ")); Assert.assertTrue(sql.contains("left outer join contact t2 on")); - + + // select t0.id c0, t0.status c1, t0.order_date c2, t0.ship_date c3, t1.name c4, t0.cretime c5, t0.updtime c6, + // t1.id c7, t1.status c8, t1.name c9, t1.smallnote c10, t1.anniversary c11, t1.cretime c12, t1.updtime c13, t1.billing_address_id c14, t1.shipping_address_id c15, + // t2.id c16, t2.first_name c17, t2.last_name c18, t2.phone c19, t2.mobile c20, t2.email c21, t2.cretime c22, t2.updtime c23, t2.customer_id c24, t2.group_id c25 + // from o_order t0 + // join o_customer t1 on t1.id = t0.kcustomer_id + // left outer join contact t2 on t2.customer_id = t1.id + // where t0.id > ? ; --bind(0) + } @Test diff --git a/src/test/java/com/avaje/tests/query/TestQueryJoinQueryNonRoot.java b/src/test/java/com/avaje/tests/query/joins/TestQueryJoinQueryNonRoot.java similarity index 93% rename from src/test/java/com/avaje/tests/query/TestQueryJoinQueryNonRoot.java rename to src/test/java/com/avaje/tests/query/joins/TestQueryJoinQueryNonRoot.java index 5aa66e77d..3783e9050 100644 --- a/src/test/java/com/avaje/tests/query/TestQueryJoinQueryNonRoot.java +++ b/src/test/java/com/avaje/tests/query/joins/TestQueryJoinQueryNonRoot.java @@ -1,4 +1,4 @@ -package com.avaje.tests.query; +package com.avaje.tests.query.joins; import java.util.List; @@ -28,7 +28,8 @@ public class TestQueryJoinQueryNonRoot extends BaseTestCase { ResetBasicData.reset(); - List list = Ebean.find(Order.class).fetch("customer") + List list = Ebean.find(Order.class) + .fetch("customer") .fetch("customer.contacts", "firstName", new FetchConfig().query().lazy(10)) .fetch("customer.contacts.group") .where().lt("id", 3).findList(); @@ -52,4 +53,6 @@ public class TestQueryJoinQueryNonRoot extends BaseTestCase { // Assert.assertTrue(list2.size() > 0); } + + } diff --git a/src/test/java/com/avaje/tests/query/joins/TestQueryManyToOneWhereClauseJoin.java b/src/test/java/com/avaje/tests/query/joins/TestQueryManyToOneWhereClauseJoin.java new file mode 100644 index 000000000..237ada6ae --- /dev/null +++ b/src/test/java/com/avaje/tests/query/joins/TestQueryManyToOneWhereClauseJoin.java @@ -0,0 +1,94 @@ +package com.avaje.tests.query.joins; + +import org.junit.Assert; +import org.junit.Test; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.Query; +import com.avaje.tests.model.basic.Order; +import com.avaje.tests.model.basic.ResetBasicData; + +public class TestQueryManyToOneWhereClauseJoin extends BaseTestCase { + + + /** + * Testing 'where join' created for a ManyToOne relationship. + */ + @Test + public void testJoin() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Order.class) + .where().ilike("customer.name", "rob%") + .query(); + + query.findList(); + + //select t0.id c0, t0.status c1, t0.order_date c2, t0.ship_date c3, t1.name c4, t0.cretime c5, t0.updtime c6, t0.kcustomer_id c7 + String expectedSql = "from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where lower(t1.name) like ? "; + Assert.assertTrue(query.getGeneratedSql().contains(expectedSql)); + + // select t0.id c0, t0.status c1, t0.order_date c2, t0.ship_date c3, t1.name c4, t0.cretime c5, t0.updtime c6, t0.kcustomer_id c7 + // from o_order t0 + // join o_customer t1 on t1.id = t0.kcustomer_id + // where lower(t1.name) like ? ; --bind(rob%) + } + + /** + * Although this is a disjunction it is on a ManyToOne so the 'default' join type is still fine. + */ + @Test + public void testDisjunctionOnManyToOneJoin() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Order.class) + .where().disjunction().ilike("customer.name", "rob%").gt("id", 1).endJunction() + .query(); + + query.findList(); + + //select t0.id c0, t0.status c1, t0.order_date c2, t0.ship_date c3, t1.name c4, t0.cretime c5, t0.updtime c6, t0.kcustomer_id c7 + String expectedSql = "from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where (lower(t1.name) like ? or t0.id > ? ) "; + Assert.assertTrue(query.getGeneratedSql().contains(expectedSql)); + + // select t0.id c0, t0.status c1, t0.order_date c2, t0.ship_date c3, t1.name c4, t0.cretime c5, t0.updtime c6, t0.kcustomer_id c7 + // from o_order t0 + // join o_customer t1 on t1.id = t0.kcustomer_id + // where (lower(t1.name) like ? or t0.id > ? ) ; --bind(rob%,1) + + } + + + /** + * Testing ManyToOne relationship with predicate and fetch. + */ + @Test + public void testWhereAndFetch() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Order.class) + .fetch("customer") + .fetch("customer.contacts") + .where().ilike("customer.name", "rob%") + .query(); + + query.findList(); + + String generatedSql = query.getGeneratedSql(); + Assert.assertTrue(generatedSql.contains("from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id")); + Assert.assertTrue(generatedSql.contains("left outer join contact t2 on t2.customer_id = t1.id")); + Assert.assertTrue(generatedSql.contains("where lower(t1.name) like ?")); + + // select t0.id c0, t0.status c1, t0.order_date c2, t0.ship_date c3, t1.name c4, t0.cretime c5, t0.updtime c6, + // t1.id c7, t1.status c8, t1.name c9, t1.smallnote c10, t1.anniversary c11, t1.cretime c12, t1.updtime c13, t1.billing_address_id c14, t1.shipping_address_id c15, + // t2.id c16, t2.first_name c17, t2.last_name c18, t2.phone c19, t2.mobile c20, t2.email c21, t2.cretime c22, t2.updtime c23, t2.customer_id c24, t2.group_id c25 + // from o_order t0 + // join o_customer t1 on t1.id = t0.kcustomer_id + // left outer join contact t2 on t2.customer_id = t1.id + // where lower(t1.name) like ? ; --bind(rob%) + } +} diff --git a/src/test/resources/ebean.properties b/src/test/resources/ebean.properties index 349865a93..16fda2bb2 100644 --- a/src/test/resources/ebean.properties +++ b/src/test/resources/ebean.properties @@ -118,7 +118,7 @@ datasource.ora.maxConnections=10 datasource.ora.heartbeatsql=select count(*) from dual datasource.ora.isolationlevel=read_committed datasource.ora.capturestacktrace=true -datasource.ora.customProperties=oracle.net.CONNECT_TIMEOUT=3000;oracle.jdbc.ReadTimeout=10000 +#datasource.ora.customProperties=oracle.net.CONNECT_TIMEOUT=3000;oracle.jdbc.ReadTimeout=10000 datasource.pg.username=test datasource.pg.password=test