From 4059034891b0d21851c1c5fe50103eba8d65c2d2 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Fri, 8 Apr 2016 13:41:43 +1200 Subject: [PATCH] #642 - ENH: Add support for OneToMany expression isEmpty() / isNotEmpty() ... which is effectively an EXISTS and NOT EXISTS subquery --- src/main/java/com/avaje/ebean/Expr.java | 14 + .../com/avaje/ebean/ExpressionFactory.java | 10 + .../java/com/avaje/ebean/ExpressionList.java | 10 + .../server/deploy/BeanFkeyProperty.java | 6 + .../server/deploy/BeanProperty.java | 7 + .../server/deploy/BeanPropertyAssocMany.java | 23 + .../server/deploy/ExportedProperty.java | 84 +-- .../server/el/ElPropertyChain.java | 6 + .../server/el/ElPropertyValue.java | 6 + .../server/expression/AbstractExpression.java | 13 +- .../expression/DefaultExpressionFactory.java | 9 + .../expression/DefaultExpressionList.java | 12 + .../server/expression/IsEmptyExpression.java | 91 ++++ .../server/expression/JunctionExpression.java | 12 +- .../type/CtCompoundPropertyElAdapter.java | 6 + .../IsEmptyExpressionQueryTest.java | 113 ++++ .../com/avaje/tests/model/basic/Contact.java | 3 +- .../avaje/tests/model/basic/ContactNote.java | 5 + .../tests/model/basic/ResetBasicData.java | 506 +++++++++--------- .../tests/query/TestManyWhereJoinM2M.java | 33 ++ 20 files changed, 675 insertions(+), 294 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpression.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpressionQueryTest.java diff --git a/src/main/java/com/avaje/ebean/Expr.java b/src/main/java/com/avaje/ebean/Expr.java index 473c2f9f8..a6a9e8b7c 100644 --- a/src/main/java/com/avaje/ebean/Expr.java +++ b/src/main/java/com/avaje/ebean/Expr.java @@ -209,6 +209,20 @@ public class Expr { return Ebean.getExpressionFactory().icontains(propertyName, value); } + /** + * For collection properties that are empty (have not existing elements). + */ + public static Expression isEmpty(String propertyName) { + return Ebean.getExpressionFactory().isEmpty(propertyName); + } + + /** + * For collection properties that are not empty (have existing elements). + */ + public static Expression isNotEmpty(String propertyName) { + return Ebean.getExpressionFactory().isNotEmpty(propertyName); + } + /** * In - property has a value in the array of values. */ diff --git a/src/main/java/com/avaje/ebean/ExpressionFactory.java b/src/main/java/com/avaje/ebean/ExpressionFactory.java index dc1529c04..89ab15535 100644 --- a/src/main/java/com/avaje/ebean/ExpressionFactory.java +++ b/src/main/java/com/avaje/ebean/ExpressionFactory.java @@ -246,6 +246,16 @@ public interface ExpressionFactory { */ Expression notExists(Query subQuery); + /** + * Is empty expression for collection properties. + */ + Expression isEmpty(String propertyName); + + /** + * Is not empty expression for collection properties. + */ + Expression isNotEmpty(String propertyName); + /** * Id Equal to - ID property is equal to the value. */ diff --git a/src/main/java/com/avaje/ebean/ExpressionList.java b/src/main/java/com/avaje/ebean/ExpressionList.java index 10d805c88..0d9c85c16 100644 --- a/src/main/java/com/avaje/ebean/ExpressionList.java +++ b/src/main/java/com/avaje/ebean/ExpressionList.java @@ -754,6 +754,16 @@ public interface ExpressionList { */ ExpressionList notIn(String propertyName, Query subQuery); + /** + * Is empty expression for collection properties. + */ + ExpressionList isEmpty(String propertyName); + + /** + * Is not empty expression for collection properties. + */ + ExpressionList isNotEmpty(String propertyName); + /** * Exists expression */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanFkeyProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanFkeyProperty.java index 77ce914a7..b60b060b9 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanFkeyProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanFkeyProperty.java @@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.deploy; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.text.StringParser; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; import com.avaje.ebeaninternal.server.el.ElPropertyValue; /** @@ -123,6 +124,11 @@ public final class BeanFkeyProperty implements ElPropertyValue { return null; } + @Override + public String getAssocIsEmpty(SpiExpressionRequest request, String path) { + throw new RuntimeException("Not Supported or Expected"); + } + /** * Returns false as not an AssocOne. */ 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 25a9fbf83..777de4de3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -5,6 +5,7 @@ import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.config.EncryptKey; import com.avaje.ebean.config.dbplatform.DbEncryptFunction; import com.avaje.ebean.config.dbplatform.DbType; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; import com.avaje.ebeanservice.docstore.api.mapping.DocMappingBuilder; import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyMapping; import com.avaje.ebean.plugin.Property; @@ -834,6 +835,12 @@ public class BeanProperty implements ElPropertyValue, Property { return false; } + @Override + public String getAssocIsEmpty(SpiExpressionRequest request, String path) { + // overridden in BanePropertyAssocMany + throw new RuntimeException("Not Supported or Expected"); + } + public Object[] getAssocIdValues(EntityBean bean) { // Returns null as not an AssocOne. return null; diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index b79bf0dcf..01f42f5ea 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -11,6 +11,7 @@ import com.avaje.ebean.bean.BeanCollectionAdd; import com.avaje.ebean.bean.BeanCollectionLoader; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.text.PathProperties; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.core.DefaultSqlUpdate; import com.avaje.ebeaninternal.server.deploy.id.ImportedId; @@ -481,6 +482,28 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { help.add(collection, bean, false); } + @Override + public String getAssocIsEmpty(SpiExpressionRequest request, String path) { + + StringBuilder sb = new StringBuilder(); + + SpiQuery query = request.getQueryRequest().getQuery(); + if (manyToMany) { + sb.append(query.isAsDraft() ? intersectionDraftTable : intersectionPublishTable); + } else { + sb.append(targetDescriptor.getBaseTable(query.getTemporalMode())); + } + sb.append(" where "); + for (int i = 0; i < exportedProperties.length; i++) { + if (i > 0) { + sb.append(" and "); + } + exportedProperties[i].appendWhere(sb, path); + } + + return sb.toString(); + } + /** * Return the Id values from the given bean. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/ExportedProperty.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/ExportedProperty.java index 9cf7fdf1f..438eb99d2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/ExportedProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/ExportedProperty.java @@ -11,43 +11,53 @@ import com.avaje.ebeaninternal.server.core.InternString; */ public class ExportedProperty { - private final String foreignDbColumn; - - private final BeanProperty property; + private final String foreignDbColumn; - private final boolean embedded; - - public ExportedProperty(boolean embedded, String foreignDbColumn, BeanProperty property) { - this.embedded = embedded; - this.foreignDbColumn = InternString.intern(foreignDbColumn); - this.property = property; - } - - /** - * Return true if this is part of an embedded concatinated key. - */ - public boolean isEmbedded() { - return embedded; - } - - /** - * Return the property value from the bean. - */ - public Object getValue(EntityBean bean){ - return property.getValue(bean); - } - - /** - * Return the foreign database column matching this property. - *

- * We use this foreign database column in the query predicates - * in preference to a parentProperty.idProperty = value. - * Just using the foreign database column avoids triggering - * a join to the 'parent' table. - *

- */ - public String getForeignDbColumn() { - return foreignDbColumn; - } + private final BeanProperty property; + private final boolean embedded; + + public ExportedProperty(boolean embedded, String foreignDbColumn, BeanProperty property) { + this.embedded = embedded; + this.foreignDbColumn = InternString.intern(foreignDbColumn); + this.property = property; + } + + /** + * Return true if this is part of an embedded concatinated key. + */ + public boolean isEmbedded() { + return embedded; + } + + /** + * Return the property value from the bean. + */ + public Object getValue(EntityBean bean) { + return property.getValue(bean); + } + + /** + * Return the foreign database column matching this property. + *

+ * We use this foreign database column in the query predicates + * in preference to a parentProperty.idProperty = value. + * Just using the foreign database column avoids triggering + * a join to the 'parent' table. + *

+ */ + public String getForeignDbColumn() { + return foreignDbColumn; + } + + /** + * Append a logical where for the foreign db column to logical property name, + */ + public void appendWhere(StringBuilder sb, String path) { + sb.append(foreignDbColumn).append(" = "); + if (path != null) { + sb.append(path).append("."); + } + sb.append(property.getName()); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyChain.java b/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyChain.java index 7631eab47..38cad3d34 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyChain.java +++ b/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyChain.java @@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.el; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.text.StringParser; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; import com.avaje.ebeaninternal.server.deploy.BeanProperty; import com.avaje.ebeaninternal.server.lib.util.StringHelper; import com.avaje.ebeaninternal.server.query.SplitName; @@ -154,6 +155,11 @@ public class ElPropertyChain implements ElPropertyValue { return lastElPropertyValue.isLocalEncrypted(); } + @Override + public String getAssocIsEmpty(SpiExpressionRequest request, String path) { + return lastElPropertyValue.getAssocIsEmpty(request, path); + } + public Object[] getAssocIdValues(EntityBean bean) { // Don't navigate the object graph as bean // is assumed to be the appropriate type diff --git a/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyValue.java b/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyValue.java index dc180d9bb..9d2a7a1c2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyValue.java +++ b/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyValue.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.el; import com.avaje.ebean.plugin.ExpressionPath; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; /** * The expression language object that can get values. @@ -20,6 +21,11 @@ public interface ElPropertyValue extends ElPropertyDeploy, ExpressionPath { */ String getAssocIdInExpr(String prefix); + /** + * Return the logical where clause to support "Is empty". + */ + String getAssocIsEmpty(SpiExpressionRequest request, String path); + /** * Return true if this is an ManyToOne or OneToOne associated bean property. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java index f2fac018e..53fd10b96 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java @@ -28,12 +28,19 @@ public abstract class AbstractExpression implements SpiExpression { @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { - if (propName != null) { - ElPropertyDeploy elProp = desc.getElPropertyDeploy(propName); + propertyContainsMany(propName, desc, manyWhereJoin); + } + + /** + * Check the logical property path for containing a 'many' property. + */ + protected void propertyContainsMany(String propertyName, BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { + if (propertyName != null) { + ElPropertyDeploy elProp = desc.getElPropertyDeploy(propertyName); if (elProp != null) { if (elProp.containsFormulaWithJoin()) { // for findRowCount query select clause - manyWhereJoin.addFormulaWithJoin(propName); + manyWhereJoin.addFormulaWithJoin(propertyName); } if (elProp.containsMany()) { // for findRowCount we join to a many property diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java index 60eb6397d..84db696b3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -348,6 +348,15 @@ public class DefaultExpressionFactory implements SpiExpressionFactory { return new ExistsQueryExpression((SpiQuery) subQuery, true); } + @Override + public Expression isEmpty(String propertyName) { + return new IsEmptyExpression(propertyName, true); + } + + @Override + public Expression isNotEmpty(String propertyName) { + return new IsEmptyExpression(propertyName, false); + } /** * Id Equal to - ID property is equal to the value. diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java index 1f997f1f2..413e420ef 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java @@ -742,6 +742,18 @@ public class DefaultExpressionList implements SpiExpressionList { return this; } + @Override + public ExpressionList isEmpty(String propertyName) { + add(expr.isEmpty(propertyName)); + return this; + } + + @Override + public ExpressionList isNotEmpty(String propertyName) { + add(expr.isNotEmpty(propertyName)); + return this; + } + @Override public ExpressionList exists(Query subQuery) { add(expr.exists(subQuery)); diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpression.java new file mode 100644 index 000000000..4e806d2d3 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpression.java @@ -0,0 +1,91 @@ +package com.avaje.ebeaninternal.server.expression; + +import com.avaje.ebeaninternal.api.HashQueryPlanBuilder; +import com.avaje.ebeaninternal.api.ManyWhereJoins; +import com.avaje.ebeaninternal.api.SpiExpression; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; +import com.avaje.ebeaninternal.server.el.ElPropertyValue; +import com.avaje.ebeaninternal.server.query.SplitName; + +import java.io.IOException; + +public class IsEmptyExpression extends AbstractExpression { + + private final boolean empty; + + private final String propertyPath; + + public IsEmptyExpression(String propertyName, boolean empty) { + super(propertyName); + this.empty = empty; + this.propertyPath = SplitName.split(propertyName)[0]; + } + + @Override + public void writeDocQuery(DocQueryContext context) throws IOException { + + } + + public final String getPropName() { + return propName; + } + + @Override + public void addBindValues(SpiExpressionRequest request) { + // no bind values + } + + @Override + public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { + // we don't want the extra join + propertyContainsMany(propertyPath, desc, manyWhereJoin); + } + + @Override + public void addSql(SpiExpressionRequest request) { + + ElPropertyValue prop = getElProp(request); + if (prop == null) { + throw new IllegalStateException("Property [" + propName + "] not found"); + } + + if (empty) { + request.append("not "); + } + + request + .append("exists (select 1 from ") + .append(prop.getAssocIsEmpty(request, propertyPath)) + .append(")"); + } + + /** + * Based on the type and propertyName. + */ + @Override + public void queryPlanHash(HashQueryPlanBuilder builder) { + builder.add(IsEmptyExpression.class).add(propName); + } + + @Override + public int queryBindHash() { + return 1; + } + + @Override + public boolean isSameByPlan(SpiExpression other) { + if (!(other instanceof IsEmptyExpression)) { + return false; + } + + IsEmptyExpression that = (IsEmptyExpression) other; + return this.propName.equals(that.propName) + && this.empty == that.empty; + } + + @Override + public boolean isSameByBind(SpiExpression other) { + return (other instanceof IsEmptyExpression); + } +} 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 4170034e2..9b5e61a0a 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 @@ import java.util.Set; */ class JunctionExpression implements SpiJunction, SpiExpression, ExpressionList { - protected final DefaultExpressionList exprList; + private final DefaultExpressionList exprList; protected final Junction.Type type; @@ -547,6 +547,16 @@ class JunctionExpression implements SpiJunction, SpiExpression, Expression return exprList.notIn(propertyName, subQuery); } + @Override + public ExpressionList isEmpty(String propertyName) { + return exprList.isEmpty(propertyName); + } + + @Override + public ExpressionList isNotEmpty(String propertyName) { + return exprList.isNotEmpty(propertyName); + } + @Override public ExpressionList exists(Query subQuery) { return exprList.exists(subQuery); diff --git a/src/main/java/com/avaje/ebeaninternal/server/type/CtCompoundPropertyElAdapter.java b/src/main/java/com/avaje/ebeaninternal/server/type/CtCompoundPropertyElAdapter.java index 4f12100ba..19d26c993 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/CtCompoundPropertyElAdapter.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/CtCompoundPropertyElAdapter.java @@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.type; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.text.StringParser; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; import com.avaje.ebeaninternal.server.deploy.BeanProperty; import com.avaje.ebeaninternal.server.el.ElPropertyValue; @@ -52,6 +53,11 @@ public class CtCompoundPropertyElAdapter implements ElPropertyValue { throw new RuntimeException("Not Supported or Expected"); } + @Override + public String getAssocIsEmpty(SpiExpressionRequest request, String path) { + throw new RuntimeException("Not Supported or Expected"); + } + public Object[] getAssocIdValues(EntityBean bean) { throw new RuntimeException("Not Supported or Expected"); } diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpressionQueryTest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpressionQueryTest.java new file mode 100644 index 000000000..7f9695f25 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpressionQueryTest.java @@ -0,0 +1,113 @@ +package com.avaje.ebeaninternal.server.expression; + +import com.avaje.ebean.Ebean; +import com.avaje.ebean.Query; +import com.avaje.tests.model.basic.Contact; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.ResetBasicData; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IsEmptyExpressionQueryTest { + + @Test + public void isEmpty() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .select("id") + .where().isEmpty("contacts") + .query(); + + query.findList(); + assertThat(query.getGeneratedSql()).contains("select t0.id c0 from o_customer t0 where not exists (select 1 from contact where customer_id = t0.id"); + } + + @Test + public void isNotEmpty() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .select("id") + .where().isNotEmpty("contacts") + .query(); + + query.findList(); + assertThat(query.getGeneratedSql()).contains("select t0.id c0 from o_customer t0 where exists (select 1 from contact where customer_id = t0.id"); + } + + @Test + public void isEmpty_contacts() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Contact.class) + .select("id") + .where().isEmpty("notes") + .query(); + + query.findList(); + assertThat(query.getGeneratedSql()).contains("select t0.id c0 from contact t0 where not exists (select 1 from contact_note where contact_id = t0.id"); + } + + @Test + public void isNotEmpty_contacts() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Contact.class) + .select("id") + .where().isNotEmpty("notes") + .query(); + + query.findList(); + assertThat(query.getGeneratedSql()).contains("select t0.id c0 from contact t0 where exists (select 1 from contact_note where contact_id = t0.id"); + } + + + @Test + public void isEmpty_manyToMany() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Contact.class) + .select("id") + .where().isEmpty("notes") + .query(); + + query.findList(); + } + + + @Test + public void isEmpty_nested() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .select("id") + .where().isEmpty("contacts.notes") + .query(); + + query.findList(); + assertThat(query.getGeneratedSql()).contains("select distinct t0.id c0 from o_customer t0 join contact u1 on u1.customer_id = t0.id where not exists (select 1 from contact_note where contact_id = u1.id)"); + } + + @Test + public void isNotEmpty_nested() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .select("id") + .where().isNotEmpty("contacts.notes") + .query(); + + query.findList(); + assertThat(query.getGeneratedSql()).contains("select distinct t0.id c0 from o_customer t0 join contact u1 on u1.customer_id = t0.id where exists (select 1 from contact_note where contact_id = u1.id)"); + } + +} \ No newline at end of file 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 de11eab88..69709ffda 100644 --- a/src/test/java/com/avaje/tests/model/basic/Contact.java +++ b/src/test/java/com/avaje/tests/model/basic/Contact.java @@ -7,6 +7,7 @@ import com.avaje.ebean.annotation.DocEmbedded; import com.avaje.ebean.annotation.DocStore; import com.avaje.ebean.annotation.Index; +import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -41,7 +42,7 @@ public class Contact { @ManyToOne(optional=true) ContactGroup group; - @OneToMany + @OneToMany(cascade = CascadeType.ALL) List notes; @CreatedTimestamp diff --git a/src/test/java/com/avaje/tests/model/basic/ContactNote.java b/src/test/java/com/avaje/tests/model/basic/ContactNote.java index 9be2a876d..561b83aec 100644 --- a/src/test/java/com/avaje/tests/model/basic/ContactNote.java +++ b/src/test/java/com/avaje/tests/model/basic/ContactNote.java @@ -17,6 +17,11 @@ public class ContactNote extends BasicDomain { @Lob String note; + public ContactNote(String title, String note) { + this.title = title; + this.note = note; + } + public String getTitle() { return title; } diff --git a/src/test/java/com/avaje/tests/model/basic/ResetBasicData.java b/src/test/java/com/avaje/tests/model/basic/ResetBasicData.java index dab2fd605..db13c5fee 100644 --- a/src/test/java/com/avaje/tests/model/basic/ResetBasicData.java +++ b/src/test/java/com/avaje/tests/model/basic/ResetBasicData.java @@ -11,299 +11,301 @@ import java.util.List; public class ResetBasicData { - private static boolean runOnce; - - private static EbeanServer server = Ebean.getServer(null); - - public static synchronized void reset() { - - if (runOnce){ - return; - } + private static boolean runOnce; - final ResetBasicData me = new ResetBasicData(); - - server.execute(new TxRunnable() { - public void run() { + private static EbeanServer server = Ebean.getServer(null); + + public static synchronized void reset() { + + if (runOnce) { + return; + } + + final ResetBasicData me = new ResetBasicData(); + + server.execute(new TxRunnable() { + public void run() { if (server.find(Product.class).findRowCount() > 0) { // we can't really delete this base data as // the test rely on the products being in there return; } - //me.deleteAll(); - me.insertCountries(); - me.insertProducts(); - me.insertTestCustAndOrders(); - } - }); - runOnce = true; - } + //me.deleteAll(); + me.insertCountries(); + me.insertProducts(); + me.insertTestCustAndOrders(); + } + }); + runOnce = true; + } - public void deleteAll() { - Ebean.execute(new TxRunnable() { - public void run() { + public void deleteAll() { + Ebean.execute(new TxRunnable() { + public void run() { - // orm update use bean name and bean properties - server.createSqlUpdate("delete from o_cached_bean_child").execute(); - server.createSqlUpdate("delete from o_cached_bean_country").execute(); - server.createSqlUpdate("delete from o_cached_bean").execute(); + // orm update use bean name and bean properties + server.createSqlUpdate("delete from o_cached_bean_child").execute(); + server.createSqlUpdate("delete from o_cached_bean_country").execute(); + server.createSqlUpdate("delete from o_cached_bean").execute(); - server.createUpdate(OrderShipment.class, "delete from orderShipment").execute(); + server.createUpdate(OrderShipment.class, "delete from orderShipment").execute(); - server.createUpdate(OrderDetail.class, "delete from orderDetail").execute(); + server.createUpdate(OrderDetail.class, "delete from orderDetail").execute(); - server.createUpdate(Order.class, "delete from order").execute(); + server.createUpdate(Order.class, "delete from order").execute(); - server.createUpdate(Contact.class, "delete from contact").execute(); + server.createUpdate(Contact.class, "delete from contact").execute(); - server.createUpdate(Customer.class, "delete from Customer").execute(); + server.createUpdate(Customer.class, "delete from Customer").execute(); - server.createUpdate(Address.class, "delete from address").execute(); + server.createUpdate(Address.class, "delete from address").execute(); - // sql update uses table and column names - server.createSqlUpdate("delete from o_country").execute(); - server.createSqlUpdate("delete from o_product").execute(); + // sql update uses table and column names + server.createSqlUpdate("delete from o_country").execute(); + server.createSqlUpdate("delete from o_product").execute(); - } - }); - } - - - public void insertCountries() { + } + }); + } + + + public void insertCountries() { if (server.find(Country.class).findRowCount() > 0) { return; } server.execute(new TxRunnable() { - public void run() { - Country c = new Country(); - c.setCode("NZ"); - c.setName("New Zealand"); - server.save(c); - - Country au = new Country(); - au.setCode("AU"); - au.setName("Australia"); - server.save(au); - } - }); - } - + public void run() { + Country c = new Country(); + c.setCode("NZ"); + c.setName("New Zealand"); + server.save(c); - public void insertProducts() { + Country au = new Country(); + au.setCode("AU"); + au.setName("Australia"); + server.save(au); + } + }); + } + + + public void insertProducts() { if (server.find(Product.class).findRowCount() > 0) { return; } - server.execute(new TxRunnable() { - public void run() { - Product p = new Product(); - p.setName("Chair"); - p.setSku("C001"); - server.save(p); - - p = new Product(); - p.setName("Desk"); - p.setSku("DSK1"); - server.save(p); - - p = new Product(); - p.setName("Computer"); - p.setSku("C002"); - server.save(p); - - p = new Product(); - p.setName("Printer"); - p.setSku("C003"); - server.save(p); - } - }); - } - - public void insertTestCustAndOrders() { + server.execute(new TxRunnable() { + public void run() { + Product p = new Product(); + p.setName("Chair"); + p.setSku("C001"); + server.save(p); - Ebean.execute(new TxRunnable() { - public void run() { - Customer cust1 = insertCustomer("Rob"); - Customer cust2 = insertCustomerNoAddress(); - insertCustomerFiona(); - insertCustomerNoContacts("NocCust"); - - createOrder1(cust1); - createOrder2(cust2); - createOrder3(cust1); - createOrder4(cust1); + p = new Product(); + p.setName("Desk"); + p.setSku("DSK1"); + server.save(p); + + p = new Product(); + p.setName("Computer"); + p.setSku("C002"); + server.save(p); + + p = new Product(); + p.setName("Printer"); + p.setSku("C003"); + server.save(p); + } + }); + } + + public void insertTestCustAndOrders() { + + Ebean.execute(new TxRunnable() { + public void run() { + Customer cust1 = insertCustomer("Rob"); + Customer cust2 = insertCustomerNoAddress(); + insertCustomerFiona(); + insertCustomerNoContacts("NocCust"); + + createOrder1(cust1); + createOrder2(cust2); + createOrder3(cust1); + createOrder4(cust1); createOrder5(cust2); - } - }); - } - - public static Customer createCustAndOrder(String custName) { - - ResetBasicData me = new ResetBasicData(); - Customer cust1 = insertCustomer(custName); - me.createOrder1(cust1); - return cust1; - } - - public static Order createOrderCustAndOrder(String custName) { + } + }); + } - ResetBasicData me = new ResetBasicData(); - Customer cust1 = insertCustomer(custName); - return me.createOrder1(cust1); + public static Customer createCustAndOrder(String custName) { + + ResetBasicData me = new ResetBasicData(); + Customer cust1 = insertCustomer(custName); + me.createOrder1(cust1); + return cust1; + } + + public static Order createOrderCustAndOrder(String custName) { + + ResetBasicData me = new ResetBasicData(); + Customer cust1 = insertCustomer(custName); + return me.createOrder1(cust1); + } + + private static int contactEmailNum = 1; + + private Customer insertCustomerFiona() { + + Customer c = createCustomer("Fiona", "12 Apple St", "West Coast Rd", 1, "2009-08-31"); + c.setStatus(Customer.Status.ACTIVE); + + c.addContact(createContact("Fiona", "Black")); + c.addContact(createContact("Tracy", "Red")); + + Ebean.save(c); + return c; + } + + public static Contact createContact(String firstName, String lastName) { + Contact contact = new Contact(firstName, lastName); + String email = contact.getLastName() + (contactEmailNum++) + "@test.com"; + contact.setEmail(email.toLowerCase()); + return contact; + } + + private Customer insertCustomerNoContacts(String name) { + + Customer c = createCustomer("Roger", "15 Kumera Way", "Bos town", 1, "2010-04-10"); + c.setName(name); + c.setStatus(Customer.Status.ACTIVE); + + Ebean.save(c); + return c; + } + + private Customer insertCustomerNoAddress() { + + Customer c = new Customer(); + c.setName("Cust NoAddress"); + c.setStatus(Customer.Status.NEW); + c.addContact(createContact("Jack", "Black")); + + Ebean.save(c); + return c; + } + + private static Customer insertCustomer(String name) { + Customer c = createCustomer(name, "1 Banana St", "P.O.Box 1234", 1, null); + Ebean.save(c); + return c; + } + + public static Customer createCustomer(String name, String shippingStreet, String billingStreet, int contactSuffix) { + return createCustomer(name, shippingStreet, billingStreet, contactSuffix, null); + } + + public static Customer createCustomer(String name, String shippingStreet, String billingStreet, int contactSuffix, String annDate) { + + Customer c = new Customer(); + c.setName(name); + c.setStatus(Customer.Status.NEW); + if (annDate == null) { + annDate = "2010-04-14"; + } + c.setAnniversary(Date.valueOf(annDate)); + if (contactSuffix > 0) { + Contact jim = new Contact("Jim" + contactSuffix, "Cricket"); + jim.getNotes().add(new ContactNote("ORM Lives", "And it is cool!")); + c.addContact(jim); + c.addContact(new Contact("Fred" + contactSuffix, "Blue")); + c.addContact(new Contact("Bugs" + contactSuffix, "Bunny")); } - private static int contactEmailNum = 1; - - private Customer insertCustomerFiona() { - - Customer c = createCustomer("Fiona", "12 Apple St", "West Coast Rd", 1, "2009-08-31"); - c.setStatus(Customer.Status.ACTIVE); - - c.addContact(createContact("Fiona","Black")); - c.addContact(createContact("Tracy","Red")); + if (shippingStreet != null) { + Address shippingAddr = new Address(); + shippingAddr.setLine1(shippingStreet); + shippingAddr.setLine2("Sandringham"); + shippingAddr.setCity("Auckland"); + shippingAddr.setCountry(Ebean.getReference(Country.class, "NZ")); - Ebean.save(c); - return c; - } - - public static Contact createContact(String firstName, String lastName) { - Contact contact = new Contact(firstName,lastName); - String email = contact.getLastName()+(contactEmailNum++)+"@test.com"; - contact.setEmail(email.toLowerCase()); - return contact; - } - - private Customer insertCustomerNoContacts(String name) { - - Customer c = createCustomer("Roger", "15 Kumera Way", "Bos town", 1, "2010-04-10"); - c.setName(name); - c.setStatus(Customer.Status.ACTIVE); - - Ebean.save(c); - return c; - } - - private Customer insertCustomerNoAddress() { - - Customer c = new Customer(); - c.setName("Cust NoAddress"); - c.setStatus(Customer.Status.NEW); - c.addContact(createContact("Jack","Black")); - - Ebean.save(c); - return c; - } - - private static Customer insertCustomer(String name) { - Customer c = createCustomer(name, "1 Banana St", "P.O.Box 1234", 1, null); - Ebean.save(c); - return c; + c.setShippingAddress(shippingAddr); } - public static Customer createCustomer(String name, String shippingStreet, String billingStreet, int contactSuffix) { - return createCustomer(name, shippingStreet, billingStreet, contactSuffix, null); - } - - public static Customer createCustomer(String name, String shippingStreet, String billingStreet, int contactSuffix, String annDate) { - - Customer c = new Customer(); - c.setName(name); - c.setStatus(Customer.Status.NEW); - if (annDate == null){ - annDate = "2010-04-14"; - } - c.setAnniversary(Date.valueOf(annDate)); - if (contactSuffix > 0){ - c.addContact(new Contact("Jim"+contactSuffix,"Cricket")); - c.addContact(new Contact("Fred"+contactSuffix,"Blue")); - c.addContact(new Contact("Bugs"+contactSuffix,"Bunny")); - } - - if (shippingStreet != null){ - Address shippingAddr = new Address(); - shippingAddr.setLine1(shippingStreet); - shippingAddr.setLine2("Sandringham"); - shippingAddr.setCity("Auckland"); - shippingAddr.setCountry(Ebean.getReference(Country.class, "NZ")); - - c.setShippingAddress(shippingAddr); - } - - if (billingStreet != null){ - Address billingAddr = new Address(); - billingAddr.setLine1(billingStreet); - billingAddr.setLine2("St Lukes"); - billingAddr.setCity("Auckland"); - billingAddr.setCountry(Ebean.getReference(Country.class, "NZ")); - - c.setBillingAddress(billingAddr); - } - - return c; - } - - private Order createOrder1(Customer customer) { - - Product product1 = Ebean.getReference(Product.class, 1); - Product product2 = Ebean.getReference(Product.class, 2); - Product product3 = Ebean.getReference(Product.class, 3); - - - Order order = new Order(); - order.setCustomer(customer); - - List details = new ArrayList(); - details.add(new OrderDetail(product1, 5, 10.50)); - details.add(new OrderDetail(product2, 3, 1.10)); - details.add(new OrderDetail(product3, 1, 2.00)); - order.setDetails(details); - - - order.addShipment(new OrderShipment()); - - Ebean.save(order); - return order; - } + if (billingStreet != null) { + Address billingAddr = new Address(); + billingAddr.setLine1(billingStreet); + billingAddr.setLine2("St Lukes"); + billingAddr.setCity("Auckland"); + billingAddr.setCountry(Ebean.getReference(Country.class, "NZ")); - private void createOrder2(Customer customer) { - - Product product1 = Ebean.getReference(Product.class, 1); - - Order order = new Order(); + c.setBillingAddress(billingAddr); + } + + return c; + } + + private Order createOrder1(Customer customer) { + + Product product1 = Ebean.getReference(Product.class, 1); + Product product2 = Ebean.getReference(Product.class, 2); + Product product3 = Ebean.getReference(Product.class, 3); + + + Order order = new Order(); + order.setCustomer(customer); + + List details = new ArrayList(); + details.add(new OrderDetail(product1, 5, 10.50)); + details.add(new OrderDetail(product2, 3, 1.10)); + details.add(new OrderDetail(product3, 1, 2.00)); + order.setDetails(details); + + + order.addShipment(new OrderShipment()); + + Ebean.save(order); + return order; + } + + private void createOrder2(Customer customer) { + + Product product1 = Ebean.getReference(Product.class, 1); + + Order order = new Order(); order.setStatus(Status.SHIPPED); - order.setCustomer(customer); - - List details = new ArrayList(); - details.add(new OrderDetail(product1, 4, 10.50)); - order.setDetails(details); - - order.addShipment(new OrderShipment()); + order.setCustomer(customer); - Ebean.save(order); - } + List details = new ArrayList(); + details.add(new OrderDetail(product1, 4, 10.50)); + order.setDetails(details); - private void createOrder3(Customer customer) { - - Product product1 = Ebean.getReference(Product.class, 1); - Product product3 = Ebean.getReference(Product.class, 3); - - Order order = new Order(); - order.setStatus(Status.COMPLETE); - order.setCustomer(customer); - - List details = new ArrayList(); - details.add(new OrderDetail(product1, 3, 10.50)); - details.add(new OrderDetail(product3, 40, 2.10)); - details.add(new OrderDetail(product1, 5, 10.00)); - order.setDetails(details); - - order.addShipment(new OrderShipment()); + order.addShipment(new OrderShipment()); - Ebean.save(order); - } + Ebean.save(order); + } + + private void createOrder3(Customer customer) { + + Product product1 = Ebean.getReference(Product.class, 1); + Product product3 = Ebean.getReference(Product.class, 3); + + Order order = new Order(); + order.setStatus(Status.COMPLETE); + order.setCustomer(customer); + + List details = new ArrayList(); + details.add(new OrderDetail(product1, 3, 10.50)); + details.add(new OrderDetail(product3, 40, 2.10)); + details.add(new OrderDetail(product1, 5, 10.00)); + order.setDetails(details); + + order.addShipment(new OrderShipment()); + + Ebean.save(order); + } private void createOrder4(Customer customer) { diff --git a/src/test/java/com/avaje/tests/query/TestManyWhereJoinM2M.java b/src/test/java/com/avaje/tests/query/TestManyWhereJoinM2M.java index 1948f8874..5b9f90a2d 100644 --- a/src/test/java/com/avaje/tests/query/TestManyWhereJoinM2M.java +++ b/src/test/java/com/avaje/tests/query/TestManyWhereJoinM2M.java @@ -11,6 +11,8 @@ import com.avaje.ebean.Query; import com.avaje.tests.model.basic.MRole; import com.avaje.tests.model.basic.MUser; +import static org.assertj.core.api.Assertions.assertThat; + public class TestManyWhereJoinM2M extends BaseTestCase { @Test @@ -43,6 +45,10 @@ public class TestManyWhereJoinM2M extends BaseTestCase { Ebean.save(u1); + MUser u2 = new MUser(); + u2.setUserName("user2"); + Ebean.save(u2); + Ebean.commitTransaction(); Query query = Ebean.find(MUser.class).fetch("roles") @@ -59,5 +65,32 @@ public class TestManyWhereJoinM2M extends BaseTestCase { Assert.assertTrue(sql.contains("join mrole ")); Assert.assertTrue(sql.contains(".role_name = ?")); + isEmpty(); + isNotEmpty(); + } + + private void isEmpty() { + + Query query = Ebean.find(MUser.class) + .where().isEmpty("roles") + .query(); + + List usersWithNoRoles = query.findList(); + + assertThat(query.getGeneratedSql()).contains("select t0.userid c0, t0.user_name c1, t0.user_type_id c2 from muser t0 where not exists (select 1 from mrole_muser where muser_userid = t0.userid)"); + assertThat(usersWithNoRoles).isNotEmpty(); + } + + private void isNotEmpty() { + + Query query = Ebean.find(MUser.class) + .select("userName") + .where().isNotEmpty("roles") + .query(); + + List usersWithRoles = query.findList(); + + assertThat(query.getGeneratedSql()).contains("select t0.userid c0, t0.user_name c1 from muser t0 where exists (select 1 from mrole_muser where muser_userid = t0.userid)"); + assertThat(usersWithRoles).isNotEmpty(); } }