diff --git a/src/main/java/io/ebeaninternal/api/ManyWhereJoins.java b/src/main/java/io/ebeaninternal/api/ManyWhereJoins.java index 0b6499fec..0c6b128a2 100644 --- a/src/main/java/io/ebeaninternal/api/ManyWhereJoins.java +++ b/src/main/java/io/ebeaninternal/api/ManyWhereJoins.java @@ -7,7 +7,9 @@ import io.ebean.util.SplitName; import io.ebeaninternal.server.query.SqlJoinType; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.TreeMap; import java.util.TreeSet; @@ -21,9 +23,7 @@ public class ManyWhereJoins implements Serializable { private final TreeMap joins = new TreeMap<>(); - private StringBuilder formulaProperties = new StringBuilder(); - - private boolean formulaWithJoin; + private List formulaJoinProperties; private boolean aggregation; @@ -125,27 +125,24 @@ public class ManyWhereJoins implements Serializable { * specifically for the findCount query. */ public void addFormulaWithJoin(String propertyName) { - if (formulaWithJoin) { - formulaProperties.append(","); - } else { - formulaProperties = new StringBuilder(); - formulaWithJoin = true; + if (formulaJoinProperties == null) { + formulaJoinProperties = new ArrayList<>(); } - formulaProperties.append(propertyName); + formulaJoinProperties.add(propertyName); } /** * Return true if the query select includes a formula with join. */ public boolean isFormulaWithJoin() { - return formulaWithJoin; + return formulaJoinProperties != null; } /** * Return the formula properties to build the select clause for a findCount query. */ - public String getFormulaProperties() { - return formulaProperties.toString(); + public List getFormulaJoinProperties() { + return formulaJoinProperties; } /** diff --git a/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java b/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java index ee2732cbb..a7d0bd779 100644 --- a/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java +++ b/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java @@ -136,8 +136,7 @@ public class ElPropertyChain implements ElPropertyValue { @Override public boolean containsFormulaWithJoin() { - // Not cascading the check at this stage - return false; + return lastBeanProperty.containsFormulaWithJoin(); } @Override diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java index 2e1511548..d73458791 100644 --- a/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java @@ -265,7 +265,9 @@ class CQueryBuilder { if (!countDistinct) { // minimise select clause for standard count if (manyWhereJoins.isFormulaWithJoin()) { - query.select(manyWhereJoins.getFormulaProperties()); + // FIXME: we join the strings here and on the other side we split them again + // this is not yet optimal + query.select(String.join(",",manyWhereJoins.getFormulaJoinProperties())); } else { query.setSelectId(); } diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 7b7530dfd..6983de938 100644 --- a/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -296,6 +296,13 @@ public final class SqlTreeBuilder { SqlTreeNodeManyWhereJoin nodeJoin = new SqlTreeNodeManyWhereJoin(joinProp.getProperty(), beanProperty, joinProp.getSqlJoinType()); myJoinList.add(nodeJoin); } + if (manyWhereJoins.isFormulaWithJoin()) { + for (String property: manyWhereJoins.getFormulaJoinProperties()) { + STreeProperty beanProperty = desc.findPropertyFromPath(property); + SqlTreeNodeFormulaWhereJoin nodeJoin = new SqlTreeNodeFormulaWhereJoin(beanProperty, SqlJoinType.OUTER); + myJoinList.add(nodeJoin); + } + } } private SqlTreeNode buildNode(String prefix, STreePropertyAssoc prop, STreeType desc, List myList, SqlTreeProperties props) { diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeFormulaWhereJoin.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeFormulaWhereJoin.java new file mode 100644 index 000000000..4ec6708b9 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeFormulaWhereJoin.java @@ -0,0 +1,115 @@ +package io.ebeaninternal.server.query; + +import io.ebean.Version; +import io.ebean.bean.EntityBean; +import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.deploy.DbReadContext; +import io.ebeaninternal.server.deploy.DbSqlContext; +import io.ebeaninternal.server.type.ScalarType; + +import java.util.List; +import java.util.Set; + +/** + * Join to Many (or child of a many) to support where clause predicates on many properties. + */ +class SqlTreeNodeFormulaWhereJoin implements SqlTreeNode { + + private final STreeProperty nodeBeanProp; + + /** + * The many where join which is either INNER or OUTER. + */ + private final SqlJoinType manyJoinType; + + SqlTreeNodeFormulaWhereJoin(STreeProperty prop, SqlJoinType manyJoinType) { + this.nodeBeanProp = prop; + this.manyJoinType = manyJoinType; + } + + @Override + public boolean isSingleProperty() { + return true; + } + + @Override + public ScalarType getSingleAttributeReader() { + throw new IllegalStateException("No expected"); + } + + @Override + public void addAsOfTableAlias(SpiQuery query) { + // do nothing here ... + } + + @Override + public void addSoftDeletePredicate(SpiQuery query) { + // do nothing here ... + } + + @Override + public boolean isAggregation() { + return false; + } + + @Override + public void appendDistinctOn(DbSqlContext ctx, boolean subQuery) { + // do nothing here ... + } + + @Override + public void appendGroupBy(DbSqlContext ctx, boolean subQuery) { + // do nothing here + } + + /** + * Append to the FROM clause for this node. + */ + @Override + public void appendFrom(DbSqlContext ctx, SqlJoinType currentJoinType) { + + // always use the join type as per this many where join + // (OUTER for disjunction and otherwise INNER) + nodeBeanProp.appendFrom(ctx, manyJoinType); + } + + + + @Override + public void dependentTables(Set tables) { + //FIXME: we cannot easily determine the dependent tables, this would require an enhancement + //of the @Formula(dependentTables=...) annotation + } + + @Override + public void buildRawSqlSelectChain(List selectChain) { + // nothing to add + } + + @Override + public void appendSelect(DbSqlContext ctx, boolean subQuery) { + // nothing to do here + } + + @Override + public void appendWhere(DbSqlContext ctx) { + // nothing to do here + } + + @Override + public EntityBean load(DbReadContext ctx, EntityBean localBean, EntityBean parentBean) { + // nothing to do here + return null; + } + + @Override + public Version loadVersion(DbReadContext ctx) { + // nothing to do here + return null; + } + + @Override + public boolean hasMany() { + return true; + } +} diff --git a/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java b/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java new file mode 100644 index 000000000..3ce0e6051 --- /dev/null +++ b/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java @@ -0,0 +1,209 @@ +package org.tests.query.joins; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import org.tests.model.basic.Order; +import org.tests.model.basic.ResetBasicData; +import org.tests.model.family.ChildPerson; +import org.tests.model.family.ParentPerson; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Before; +import org.junit.Test; + +import java.sql.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assert.assertEquals; + +public class TestQueryJoinOnFormula extends BaseTestCase { + + + @Before + public void init() { + ResetBasicData.reset(); + } + + @Test + public void test_OrderFindIds() { + + LoggedSqlCollector.start(); + + List orderIds = Ebean.find(Order.class) + .where().eq("totalItems", 3) + .findIds(); + assertThat(orderIds).hasSize(2); + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + assertThat(loggedSql.get(0)).contains("join (select order_id, count(*) as total_items,"); + } + + @Test + public void test_OrderFindList() { + + LoggedSqlCollector.start(); + + List orders = Ebean.find(Order.class) + .where().eq("totalItems", 3) + .findList(); + assertThat(orders).hasSize(2); + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + assertThat(loggedSql.get(0)).contains("join (select order_id, count(*) as total_items,"); + } + + @Test + public void test_OrderFindCount() { + + LoggedSqlCollector.start(); + + int orders = Ebean.find(Order.class) + .where().eq("totalItems", 3) + .findCount(); + assertThat(orders).isEqualTo(2); + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + assertThat(loggedSql.get(0)).contains("join (select order_id, count(*) as total_items,"); + } + + @Test + public void test_OrderFindSingleAttributeList() { + + LoggedSqlCollector.start(); + + List orderDates = Ebean.find(Order.class) + .select("orderDate") + .where().eq("totalItems", 3) + .findSingleAttributeList(); + assertThat(orderDates).hasSize(2); + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + assertThat(loggedSql.get(0)).contains("join (select order_id, count(*) as total_items,"); + } + + @Test + public void test_OrderFindOne() { + + LoggedSqlCollector.start(); + + Order order = Ebean.find(Order.class) + .select("totalItems") + .where().eq("totalItems", 3) + .setMaxRows(1) + .orderById(true) + .findOne(); + + assertThat(order.getTotalItems()).isEqualTo(3); + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + assertThat(loggedSql.get(0)).contains("join (select order_id, count(*) as total_items,"); + } + + @Test + public void test_ParentPersonFindIds() { + + LoggedSqlCollector.start(); + + List orderIds = Ebean.find(ParentPerson.class) + .where().eq("totalAge", 3) + .findIds(); + // TODO: There are no beans in database, so for now only the query must run. + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + } + + @Test + public void test_ParentPersonFindList() { + + LoggedSqlCollector.start(); + + Ebean.find(ParentPerson.class) + .select("identifier") + //.where().eq("totalAge", 3) + .where().eq("familyName", "foo") + .findList(); + // TODO: There are no beans in database, so for now only the query must run. + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + } + + @Test + public void test_ParentPersonFindCount() { + + LoggedSqlCollector.start(); + + Ebean.find(ParentPerson.class) + .where().eq("totalAge", 3) + .findCount(); + // TODO: There are no beans in database, so for now only the query must run. + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + } + + @Test + public void test_ParentPersonFindSingleAttributeList() { + + LoggedSqlCollector.start(); + + Ebean.find(ParentPerson.class) + .select("address") // .select("address, totalAge") would work + .where().eq("totalAge", 3) + .findSingleAttributeList(); + // TODO: There are no beans in database, so for now only the query must run. + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + } + + @Test + public void test_ParentPersonFindOne() { + + LoggedSqlCollector.start(); + + Ebean.find(ParentPerson.class) + .where().eq("totalAge", 3) + .setMaxRows(1) + .orderById(true) + .findOne(); + // TODO: There are no beans in database, so for now only the query must run. + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + } + + @Test + public void test_ChildPersonParentFindIds() { + + LoggedSqlCollector.start(); + + Ebean.find(ChildPerson.class) + .where().eq("parent.totalAge", 3) + .findIds(); + // TODO: There are no beans in database, so for now only the query must run. + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + } + + @Test + public void test_ChildPersonParentFindCount() { + + LoggedSqlCollector.start(); + + Ebean.find(ChildPerson.class) + .where().eq("parent.totalAge", 3) + .findCount(); + // TODO: There are no beans in database, so for now only the query must run. + + List loggedSql = LoggedSqlCollector.stop(); + assertEquals(1, loggedSql.size()); + } +}