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 b60b060b9..293daa3a4 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanFkeyProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanFkeyProperty.java @@ -129,13 +129,20 @@ public final class BeanFkeyProperty implements ElPropertyValue { throw new RuntimeException("Not Supported or Expected"); } + @Override + public boolean isAssocMany() { + return false; + } + /** * Returns false as not an AssocOne. */ + @Override public boolean isAssocId() { return false; } + @Override public boolean isAssocProperty() { return false; } 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 6e5faed8f..c892599d1 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanProperty.java @@ -888,6 +888,12 @@ public class BeanProperty implements ElPropertyValue, Property { return null; } + @Override + public boolean isAssocMany() { + // Returns false - override in BeanPropertyAssocMany. + return false; + } + public boolean isAssocId() { // Returns false - override in BeanPropertyAssocOne. return false; 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 97af7847e..9331bd896 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -549,6 +549,11 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { return true; } + @Override + public boolean isAssocMany() { + return true; + } + @Override public boolean isAssocId() { return true; 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 38cad3d34..26a8bd57f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyChain.java +++ b/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyChain.java @@ -178,6 +178,11 @@ public class ElPropertyChain implements ElPropertyValue { return lastElPropertyValue.getAssocIdInValueExpr(size); } + @Override + public boolean isAssocMany() { + return lastElPropertyValue.isAssocMany(); + } + public boolean isAssocId() { return assocId; } 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 9d2a7a1c2..ac0bd8334 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyValue.java +++ b/src/main/java/com/avaje/ebeaninternal/server/el/ElPropertyValue.java @@ -31,6 +31,11 @@ public interface ElPropertyValue extends ElPropertyDeploy, ExpressionPath { */ boolean isAssocId(); + /** + * Return true if the property is a OneToMany or ManyToMany associated bean property. + */ + boolean isAssocMany(); + /** * Return true if any path of this path contains a Associated One or Many. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java index 9a887dea2..1b36411bc 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java @@ -111,6 +111,7 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins whereManyJoins) { + list = buildExpressions(desc); if (list != null) { for (int i = 0; i < list.size(); i++) { list.get(i).containsMany(desc, whereManyJoins); @@ -118,6 +119,11 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio } } + @Override + public void prepareExpression(BeanQueryRequest request) { + // do nothing + } + @Override public ExampleExpression includeZeros() { includeZeros = true; @@ -194,11 +200,6 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio } } - @Override - public void prepareExpression(BeanQueryRequest request) { - list = buildExpressions(request); - } - /** * Return a hash for AutoTune query identification. */ @@ -259,15 +260,10 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio /** * Build the List of expressions. */ - private ArrayList buildExpressions(BeanQueryRequest request) { + private ArrayList buildExpressions(BeanDescriptor beanDescriptor) { ArrayList list = new ArrayList(); - - OrmQueryRequest r = (OrmQueryRequest) request; - BeanDescriptor beanDescriptor = r.getBeanDescriptor(); - addExpressions(list, beanDescriptor, entity, null); - return list; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpression.java index 31ed2eaa1..98af042d6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/IsEmptyExpression.java @@ -50,10 +50,17 @@ class IsEmptyExpression extends AbstractExpression { throw new IllegalStateException("Property [" + propName + "] not found"); } + isEmptySql(request, prop, empty, propertyPath); + } + + /** + * Append an exists subQuery for the property. + */ + static void isEmptySql(SpiExpressionRequest request, ElPropertyValue prop, boolean empty, String propertyPath) { + if (empty) { request.append("not "); } - request .append("exists (select 1 from ") .append(prop.getAssocIsEmpty(request, propertyPath)) diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/NullExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/NullExpression.java index 87d593c5d..ce05680c4 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/NullExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/NullExpression.java @@ -1,25 +1,50 @@ 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; /** - * Slightly redundant as Query.setId() ultimately also does the same job. + * Null / Not Null expression. + *

+ * Note that for OneToMany/ManyToMany this effectively gets translated into isEmpty()/isNotEmpty(). + *

*/ class NullExpression extends AbstractExpression { private final boolean notNull; + private ElPropertyValue elProperty; + + private boolean assocMany; + + private String propertyPath; + NullExpression(String propertyName, boolean notNull) { super(propertyName); this.notNull = notNull; } + @Override + public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { + elProperty = desc.getElGetValue(propName); + if (elProperty != null && elProperty.isAssocMany()) { + // it is OneToMany or ManyToMany so going to be treated as isEmpty() expression + assocMany = true; + propertyPath = SplitName.split(propName)[0]; + propertyContainsMany(propertyPath, desc, manyWhereJoin); + } else { + propertyContainsMany(propName, desc, manyWhereJoin); + } + } + @Override public void writeDocQuery(DocQueryContext context) throws IOException { context.writeExists(notNull, propName); @@ -33,17 +58,18 @@ class NullExpression extends AbstractExpression { @Override public void addSql(SpiExpressionRequest request) { - String propertyName = propName; - - String nullExpr = notNull ? " is not null " : " is null "; - - ElPropertyValue prop = getElProp(request); - if (prop != null && prop.isAssocId()) { - request.append(prop.getAssocIdExpression(propertyName, nullExpr)); + if (assocMany) { + // translate to exists subquery + IsEmptyExpression.isEmptySql(request, elProperty, !notNull, propertyPath); return; } - request.append(propertyName).append(nullExpr); + String nullExpr = notNull ? " is not null " : " is null "; + if (elProperty != null && elProperty.isAssocId()) { + request.append(elProperty.getAssocIdExpression(propName, nullExpr)); + } else { + request.append(propName).append(nullExpr); + } } @Override 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 19d26c993..bd11ecaf0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/type/CtCompoundPropertyElAdapter.java +++ b/src/main/java/com/avaje/ebeaninternal/server/type/CtCompoundPropertyElAdapter.java @@ -86,6 +86,12 @@ public class CtCompoundPropertyElAdapter implements ElPropertyValue { return false; } + @Override + public boolean isAssocMany() { + return false; + } + + @Override public boolean isAssocId() { return false; } diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpressionTest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpressionTest.java index 0c8a3b989..a0f637028 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpressionTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpressionTest.java @@ -6,6 +6,7 @@ import com.avaje.ebean.Query; import com.avaje.ebean.bean.EntityBean; import com.avaje.ebean.event.BeanQueryRequest; import com.avaje.ebeaninternal.api.HashQueryPlanBuilder; +import com.avaje.ebeaninternal.api.ManyWhereJoins; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.core.OrmQueryRequest; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; @@ -55,8 +56,8 @@ public class DefaultExampleExpressionTest extends BaseTestCase { SpiQuery query = (SpiQuery)spiEbeanServer().find(Customer.class); BeanQueryRequest request = create(query); + expr.containsMany(customerBeanDescriptor(), new ManyWhereJoins()); expr.prepareExpression(request); - return expr; } diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/NullExpressionTest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/NullExpressionTest.java index ec2187d14..b5aa39a96 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/expression/NullExpressionTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/NullExpressionTest.java @@ -1,6 +1,8 @@ package com.avaje.ebeaninternal.server.expression; +import com.avaje.ebeaninternal.api.ManyWhereJoins; import com.avaje.ebeaninternal.api.SpiExpression; +import com.avaje.tests.model.basic.Order; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -8,7 +10,9 @@ import static org.assertj.core.api.Assertions.assertThat; public class NullExpressionTest extends BaseExpressionTest { NullExpression nullExp(String propertyName, boolean notNull) { - return new NullExpression(propertyName, notNull); + NullExpression expr = new NullExpression(propertyName, notNull); + expr.containsMany(getBeanDescriptor(Order.class), new ManyWhereJoins()); + return expr; } @Test diff --git a/src/test/java/com/avaje/tests/query/TestQueryIsNull.java b/src/test/java/com/avaje/tests/query/TestQueryIsNull.java index 5998df113..9585dd67e 100644 --- a/src/test/java/com/avaje/tests/query/TestQueryIsNull.java +++ b/src/test/java/com/avaje/tests/query/TestQueryIsNull.java @@ -6,53 +6,101 @@ import com.avaje.ebean.Query; import com.avaje.tests.model.basic.Order; import com.avaje.tests.model.basic.ResetBasicData; import com.avaje.tests.model.m2m.Role; -import org.junit.Assert; import org.junit.Test; +import static org.junit.Assert.assertTrue; + public class TestQueryIsNull extends BaseTestCase { - @Test - public void queryShouldContainIsNullOnColumn() { - ResetBasicData.reset(); + @Test + public void queryShouldContainIsNullOnColumn() { + ResetBasicData.reset(); - Query query = Ebean.find(Order.class).where().isNull("customerName").query(); - query.findList(); + Query query = Ebean.find(Order.class).where().isNull("customerName").query(); + query.findList(); - Assert.assertTrue(query.getGeneratedSql().contains("name is null")); - } + assertTrue(query.getGeneratedSql().contains("name is null")); + } - @Test - public void queryShouldLeftJoinForOneToManyInDisjunction() { - ResetBasicData.reset(); + @Test + public void isNotNull_when_OneToMany_expect_existsSubquery() { + ResetBasicData.reset(); - Query query = Ebean.find(Order.class).where().disjunction().isNull("details").endJunction().query(); - query.findList(); + Query query = Ebean.find(Order.class).where().isNotNull("details").query(); + query.findList(); - Assert.assertTrue(query.getGeneratedSql().contains("left outer join")); - Assert.assertTrue(query.getGeneratedSql().contains("is null")); - } + assertTrue(query.getGeneratedSql().contains(" where exists (select 1 from o_order_detail where order_id = t0.id)")); + } - @Test - public void queryShouldLeftJoinForOneToManyRelation() { - ResetBasicData.reset(); + @Test + public void isNotEmpty_when_OneToMany_expect_existsSubquery() { + ResetBasicData.reset(); - Query query = Ebean.find(Order.class).where().isNull("details").query(); - query.findList(); + Query query = Ebean.find(Order.class).where().isNotEmpty("details").query(); + query.findList(); - Assert.assertTrue(query.getGeneratedSql().contains("left outer join")); - Assert.assertTrue(query.getGeneratedSql().contains("is null")); + assertTrue(query.getGeneratedSql().contains(" where exists (select 1 from o_order_detail where order_id = t0.id)")); + } - } + @Test + public void isNull_when_OneToMany_expect_notExistsSubquery() { + ResetBasicData.reset(); - @Test - public void queryShouldLeftJoinForManyToManyRelation() { - ResetBasicData.reset(); + Query query = Ebean.find(Order.class).where().isNull("details").query(); + query.findList(); - Query query = Ebean.find(Role.class).where().isNull("permissions").query(); - query.findList(); + assertTrue(query.getGeneratedSql().contains(" where not exists (select 1 from o_order_detail where order_id = t0.id)")); + } + + @Test + public void isEmpty_when_OneToMany_expect_notExistsSubquery() { + ResetBasicData.reset(); + + Query query = Ebean.find(Order.class).where().isEmpty("details").query(); + query.findList(); + + assertTrue(query.getGeneratedSql().contains(" where not exists (select 1 from o_order_detail where order_id = t0.id)")); + } + + @Test + public void isEmpty_when_ManyToMany_expect_notExistsSubqueryAndNoJoin() { + ResetBasicData.reset(); + + Query query = Ebean.find(Role.class).where().isEmpty("permissions").query(); + query.findList(); + + assertTrue(query.getGeneratedSql().contains("from mt_role t0 where not exists (select 1 from mt_role_permission where mt_role_id = t0.id)")); + } + + @Test + public void isNull_when_ManyToMany_expect_notExistsSubqueryAndNoJoin() { + ResetBasicData.reset(); + + Query query = Ebean.find(Role.class).where().isNull("permissions").query(); + query.findList(); + + assertTrue(query.getGeneratedSql().contains("from mt_role t0 where not exists (select 1 from mt_role_permission where mt_role_id = t0.id)")); + } + + @Test + public void isNotEmpty_when_ManyToMany_expect_existsSubqueryAndNoJoin() { + ResetBasicData.reset(); + + Query query = Ebean.find(Role.class).where().isNotEmpty("permissions").query(); + query.findList(); + + assertTrue(query.getGeneratedSql().contains("from mt_role t0 where exists (select 1 from mt_role_permission where mt_role_id = t0.id)")); + } + + @Test + public void isNotNull_when_ManyToMany_expect_existsSubqueryAndNoJoin() { + ResetBasicData.reset(); + + Query query = Ebean.find(Role.class).where().isNotNull("permissions").query(); + query.findList(); + + assertTrue(query.getGeneratedSql().contains("from mt_role t0 where exists (select 1 from mt_role_permission where mt_role_id = t0.id)")); + } - Assert.assertTrue(query.getGeneratedSql().contains("left outer join")); - Assert.assertTrue(query.getGeneratedSql().contains("is null")); - } }