diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQueryManyJoin.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQueryManyJoin.java index b0b661b720..a7298c62d4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQueryManyJoin.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQueryManyJoin.java @@ -15,8 +15,4 @@ public interface SpiQueryManyJoin { */ String fetchOrderBy(); - /** - * Wrap the filter many expression with a condition allowing lEFT JOIN null matching row. - */ - String idNullOr(String filterManyExpression); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanFkeyProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanFkeyProperty.java index 6052b9656a..01cd82de6f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanFkeyProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanFkeyProperty.java @@ -36,11 +36,6 @@ public String toString() { return "prefix:" + prefix + " name:" + name + " dbColumn:" + dbColumn + " ph:" + placeHolder; } - @Override - public String idNullOr(String filterManyExpression) { - throw new UnsupportedOperationException(); - } - @Override public boolean isAggregation() { return false; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 6fbe4cedc9..2c6443b77f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -513,11 +513,6 @@ public boolean isAssignableFrom(Class type) { return owningType.isAssignableFrom(type); } - @Override - public String idNullOr(String filterManyExpression) { - throw new UnsupportedOperationException(); - } - @Override public void loadIgnore(DbReadContext ctx) { ctx.dataReader().incrementPos(1); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index b273a43460..2374b24ecb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -634,11 +634,6 @@ public String fetchOrderBy() { return fetchOrderBy; } - @Override - public String idNullOr(String filterManyExpression) { - return targetIdBinder.idNullOr(name, filterManyExpression); - } - /** * Return the order by for use when lazy loading the associated collection. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java index 522aff8471..b249cfbbb4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DbSqlContext.java @@ -154,4 +154,9 @@ public interface DbSqlContext { * as it was already added to the query. */ boolean joinAdded(); + + /** + * Include the filter many predicates if specified into the JOIN clause. + */ + void includeFilterMany(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java index 4770036395..07b531f3a5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java @@ -27,11 +27,6 @@ public interface IdBinder { */ void initialise(); - /** - * Wrap the filter many expression with a condition allowing lEFT JOIN null matching row. - */ - String idNullOr(String name, String filterManyExpression); - String idSelect(); /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java index a648e02bcf..86b211fe99 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java @@ -45,20 +45,6 @@ public void initialise() { this.idInValueSql = idInExpandedForm ? idInExpanded() : idInCompressed(); } - @Override - public String idNullOr(String prefix, String filterManyExpression) { - StringBuilder sb = new StringBuilder(100); - sb.append("(("); - for (int i = 0; i < props.length; i++) { - if (i > 0) { - sb.append(" and "); - } - sb.append("${").append(prefix).append('}').append(props[i].dbColumn()).append(" is null"); - } - sb.append(") or (").append(filterManyExpression).append("))"); - return sb.toString(); - } - @Override public String idSelect() { return embIdProperty.name(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java index 247c2bdd50..82b0ca7638 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java @@ -28,11 +28,6 @@ public IdBinderEmpty() { public void initialise() { } - @Override - public String idNullOr(String name, String filterManyExpression) { - throw new UnsupportedOperationException(); - } - @Override public String idSelect() { return ""; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java index 3ec93ef043..a2d776b30f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java @@ -261,9 +261,4 @@ public String cacheKeyFromBean(EntityBean bean) { final Object value = idProperty.getValue(bean); return scalarType.format(value); } - - @Override - public String idNullOr(String prefix, String filterManyExpression) { - return "(${" + prefix + "}" + idProperty.dbColumn() + " is null or (" + filterManyExpression + "))"; - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java b/ebean-core/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java index c701f23fbe..12d376bed0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java @@ -286,8 +286,4 @@ public void pathSet(Object bean, Object value) { } } - @Override - public String idNullOr(String filterManyExpression) { - throw new UnsupportedOperationException(); - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java index 95b353cab5..802ac511dd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java @@ -715,7 +715,7 @@ private SqlLimitResponse buildSql() { appendHistoryAsOfPredicate(); appendFindId(); appendToWhere(predicates.dbWhere()); - appendToWhere(predicates.dbFilterMany()); + appendToWhere(predicates.dbFilterManyWhere()); if (!query.isIncludeSoftDeletes()) { appendSoftDelete(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPredicates.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPredicates.java index fa2c6a18a4..52d8d1f82f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPredicates.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPredicates.java @@ -39,6 +39,7 @@ public final class CQueryPredicates { private final Object idValue; private final BindParams bindParams; private DefaultExpressionRequest filterMany; + private boolean filterManyJoin; /** * Bind values from the where expressions. */ @@ -106,6 +107,9 @@ public String bind(DataBind dataBind) throws SQLException { dataBind.append(", "); } } + if (filterManyJoin) { + filterMany.bind(dataBind); + } if (idValue != null) { // this is a find by id type query... request.descriptor().bindId(dataBind, idValue); @@ -119,7 +123,7 @@ public String bind(DataBind dataBind) throws SQLException { if (where != null) { where.bind(dataBind); } - if (filterMany != null) { + if (!filterManyJoin && filterMany != null) { filterMany.bind(dataBind); } if (having != null) { @@ -189,9 +193,10 @@ public void prepare(boolean buildSql) { if (chunk != null) { SpiExpressionList filterManyExpr = chunk.getFilterMany(); if (filterManyExpr != null) { - this.filterMany = new DefaultExpressionRequest(request, deployParser, binder, filterManyExpr); + filterManyJoin = chunk.isFilterManyJoin(); + filterMany = new DefaultExpressionRequest(request, deployParser, binder, filterManyExpr); if (buildSql) { - dbFilterMany = manyProperty.idNullOr(filterMany.buildSql()); + dbFilterMany = filterMany.buildSql(); } } } @@ -339,10 +344,17 @@ String dbWhere() { } /** - * Return a db filter for filtering many fetch joins. + * Return a db filter to be included in the WHERE. + */ + String dbFilterManyWhere() { + return filterManyJoin ? null : dbFilterMany; + } + + /** + * Return a db filter to be included in the JOIN. */ - String dbFilterMany() { - return dbFilterMany; + String dbFilterManyJoin() { + return filterManyJoin ? dbFilterMany : null; } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java index 3fd2f79221..139ee6ca87 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultDbSqlContext.java @@ -22,6 +22,7 @@ final class DefaultDbSqlContext implements DbSqlContext { private final ArrayStack joinStack = new ArrayStack<>(); private final ArrayStack prefixStack = new ArrayStack<>(); private final String fromForUpdate; + private final String dbFilterManyJoin; private boolean useColumnAlias; private int columnIndex; private int asOfTableCount; @@ -41,7 +42,7 @@ final class DefaultDbSqlContext implements DbSqlContext { private boolean joinSuppressed; DefaultDbSqlContext(SqlTreeAlias alias, String columnAliasPrefix, CQueryHistorySupport historySupport, - CQueryDraftSupport draftSupport, String fromForUpdate) { + CQueryDraftSupport draftSupport, String fromForUpdate, String dbFilterManyJoin) { this.alias = alias; this.columnAliasPrefix = columnAliasPrefix; this.useColumnAlias = columnAliasPrefix != null; @@ -49,6 +50,14 @@ final class DefaultDbSqlContext implements DbSqlContext { this.historySupport = historySupport; this.historyQuery = (historySupport != null); this.fromForUpdate = fromForUpdate; + this.dbFilterManyJoin = dbFilterManyJoin; + } + + @Override + public void includeFilterMany() { + if (dbFilterManyJoin != null) { + sb.append(" and ").append(dbFilterManyJoin); + } } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 4138c3058f..9b828fa9db 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -107,7 +107,7 @@ public final class SqlTreeBuilder { CQueryHistorySupport historySupport = builder.historySupport(query); CQueryDraftSupport draftSupport = builder.draftSupport(query); String colAlias = subQuery || rootNode.isSingleProperty() ? null : columnAliasPrefix; - this.ctx = new DefaultDbSqlContext(alias, colAlias, historySupport, draftSupport, fromForUpdate); + this.ctx = new DefaultDbSqlContext(alias, colAlias, historySupport, draftSupport, fromForUpdate, predicates.dbFilterManyJoin()); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java index ebfa9dd3f0..ec34d53953 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java @@ -45,5 +45,6 @@ protected void appendExtraWhere(DbSqlContext ctx) { @Override public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) { super.appendFrom(ctx, joinType.autoToOuter()); + ctx.includeFilterMany(); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java index 94bb5ab619..66ae3d76b6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryProperties.java @@ -171,8 +171,9 @@ public SpiExpressionList filterMany(Query rootQuery) { if (filterMany == null) { FilterExprPath exprPath = new FilterExprPath(path); SpiExpressionFactory queryEf = (SpiExpressionFactory) rootQuery.getExpressionFactory(); - ExpressionFactory filterEf = queryEf.createExpressionFactory();// exprPath); + ExpressionFactory filterEf = queryEf.createExpressionFactory(); filterMany = new FilterExpressionList(exprPath, filterEf, rootQuery); + // assuming conditions supported in JOIN, not setting markForQueryJoin = true } return filterMany; } @@ -187,6 +188,13 @@ private SpiExpressionList getFilterManyTrimPath(int trimPath) { return filterMany.trimPath(trimPath); } + /** + * Return true if there is a filterMany and it should be included in the JOIN. + */ + public boolean isFilterManyJoin() { + return filterMany != null && !markForQueryJoin; + } + /** * Adjust filterMany expressions for inclusion in main query. */ diff --git a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java index fa41918555..ebc45cc1a2 100644 --- a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java @@ -328,7 +328,7 @@ void filterMany() { .query(); q.findList(); - assertThat(q.getGeneratedSql()).isEqualTo("select /* QCustomerTest.filterMany */ t0.id, t0.name, t1.id, t1.first_name, t1.last_name from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id where (t1.id is null or (t1.first_name like ? escape'|' and t1.email is not null)) order by t0.id"); + assertThat(q.getGeneratedSql()).isEqualTo("select /* QCustomerTest.filterMany */ t0.id, t0.name, t1.id, t1.first_name, t1.last_name from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id and t1.first_name like ? escape'|' and t1.email is not null order by t0.id"); } @Test @@ -340,7 +340,7 @@ void filterManySingle() { .query(); q.findList(); - assertThat(q.getGeneratedSql()).isEqualTo("select /* QCustomerTest.filterManySingle */ t0.id, t0.name, t1.id, t1.first_name, t1.last_name from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id where (t1.id is null or (t1.first_name like ? escape'|')) order by t0.id"); + assertThat(q.getGeneratedSql()).isEqualTo("select /* QCustomerTest.filterManySingle */ t0.id, t0.name, t1.id, t1.first_name, t1.last_name from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id and t1.first_name like ? escape'|' order by t0.id"); } @Test @@ -375,7 +375,7 @@ void filterManySingleQuery() { .query(); q.findList(); - assertThat(q.getGeneratedSql()).contains(" from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id where (t1.id is null or (t1.first_name like ? and t1.first_name like ? escape'|')) order by t0.id"); + assertThat(q.getGeneratedSql()).contains(" from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id and t1.first_name like ? and t1.first_name like ? escape'|' order by t0.id"); } @Test @@ -389,7 +389,7 @@ void filterManyOr() { .query(); q.findList(); - assertThat(q.getGeneratedSql()).contains(" from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id where (t1.id is null or ((t1.first_name like ? escape'|' or t1.last_name like ? escape'|'))) order by t0.id"); + assertThat(q.getGeneratedSql()).contains(" from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id and (t1.first_name like ? escape'|' or t1.last_name like ? escape'|') order by t0.id"); } @Test @@ -406,7 +406,7 @@ void filterManyBeforeSelectFetchGroup_expect_filterManyExpressionRetained() { .query(); q.findList(); - assertThat(q.getGeneratedSql()).contains(" t0.id, t0.name, t1.id, t1.email from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id where (t1.id is null or (t1.first_name like ? escape'|')) order by t0.id"); + assertThat(q.getGeneratedSql()).contains(" t0.id, t0.name, t1.id, t1.email from be_customer t0 left join be_contact t1 on t1.customer_id = t0.id and t1.first_name like ? escape'|' order by t0.id"); } @Test diff --git a/ebean-querybean/src/test/java/org/querytest/QDataWithFormulaMainTest.java b/ebean-querybean/src/test/java/org/querytest/QDataWithFormulaMainTest.java index a1fd33696b..d6fb198a97 100644 --- a/ebean-querybean/src/test/java/org/querytest/QDataWithFormulaMainTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QDataWithFormulaMainTest.java @@ -22,7 +22,7 @@ void testFilterMany() { List sql = LoggedSql.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains(" where ((t1.main_id is null and t1.meta_key is null and t1.value_index is null) or (t1.meta_key = ?)) order by t0.id"); + assertThat(sql.get(0)).contains(" from data_with_formula_main t0 left join data_with_formula t1 on t1.main_id = t0.id and t1.meta_key = ? order by t0.id"); } @Test @@ -36,6 +36,6 @@ void testFilterManyComposite() { List sql = LoggedSql.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains(" where ((t1.main_id is null and t1.meta_key is null and t1.value_index is null) or (t1.meta_key = ?)) order by t0.id"); + assertThat(sql.get(0)).contains(" left join data_with_formula t1 on t1.main_id = t0.id and t1.meta_key = ? order by t0.id"); } } diff --git a/ebean-test/src/test/java/org/tests/model/basic/Building.java b/ebean-test/src/test/java/org/tests/model/basic/Building.java new file mode 100644 index 0000000000..05df04cd97 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/Building.java @@ -0,0 +1,27 @@ +package org.tests.model.basic; + +import io.ebean.Model; +import jakarta.persistence.*; + +@Entity +public class Building extends Model { + public static final String CAFE = "cafe"; + public static final String HOUSE = "house"; + public static final String STORE = "store"; + + @Id + public int id; + @Column(nullable = false) + public String type; + @Column(name = "lvl") + public int level; + public final String name; + @ManyToOne(optional = false) + public final Clan clan; + + public Building(Clan clan, String type, String name) { + this.clan = clan; + this.type = type; + this.name = name; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/Clan.java b/ebean-test/src/test/java/org/tests/model/basic/Clan.java new file mode 100644 index 0000000000..5f86f0e53f --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/Clan.java @@ -0,0 +1,17 @@ +package org.tests.model.basic; + +import io.ebean.Model; +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +public class Clan extends Model { + + @Id + public int id; + + @OneToMany(cascade = CascadeType.ALL) + public List buildings = new ArrayList<>(); +} diff --git a/ebean-test/src/test/java/org/tests/model/basic/ClanQuest.java b/ebean-test/src/test/java/org/tests/model/basic/ClanQuest.java new file mode 100644 index 0000000000..d9da9dba42 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/basic/ClanQuest.java @@ -0,0 +1,18 @@ +package org.tests.model.basic; + +import io.ebean.Model; +import jakarta.persistence.*; + +@Entity +public class ClanQuest extends Model { + + @Id + public int id; + + @ManyToOne(optional = false) + public final Clan clan; + + public ClanQuest(Clan clan) { + this.clan = clan; + } +} diff --git a/ebean-test/src/test/java/org/tests/query/TestQueryFilterMany.java b/ebean-test/src/test/java/org/tests/query/TestQueryFilterMany.java index c5846aa9cc..9203912801 100644 --- a/ebean-test/src/test/java/org/tests/query/TestQueryFilterMany.java +++ b/ebean-test/src/test/java/org/tests/query/TestQueryFilterMany.java @@ -6,13 +6,12 @@ import io.ebean.Query; import io.ebean.test.LoggedSql; import org.junit.jupiter.api.Test; -import org.tests.model.basic.Customer; -import org.tests.model.basic.Order; -import org.tests.model.basic.ResetBasicData; +import org.tests.model.basic.*; import java.time.LocalDate; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -175,7 +174,8 @@ public void filterManyRaw_singleQuery() { assertThat(customers).isNotEmpty(); List sqlList = LoggedSql.stop(); assertEquals(1, sqlList.size()); - assertThat(sqlList.get(0)).contains(" where lower(t0.name) = ? and (t1.id is null or (t1.status = ?)) order by t0.id"); + assertThat(sqlList.get(0)).contains(" left join o_customer t2 on t2.id = t1.kcustomer_id and t1.status = ? where "); + assertThat(sqlList.get(0)).contains(" where lower(t0.name) = ? order by t0.id"); } @Test @@ -211,8 +211,7 @@ public void test_with_findOne_rawSameQuery() { assertThat(result).isNotEmpty(); List sql = LoggedSql.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("from o_customer t0 left join o_order t1"); - assertThat(sql.get(0)).contains("where (t1.id is null or (t1.order_date is not null)) order by t0.id"); + assertThat(sql.get(0)).contains("from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id and t1.order_date is not null order by t0.id"); } @Test @@ -231,25 +230,78 @@ public void test_with_findOneOrEmpty() { } @Test - public void test_filterMany_with_isNotEmpty() { + public void test_filterMany_excludedExplicitly() { + Clan clan = new Clan(); + ClanQuest quest = new ClanQuest(clan); + DB.saveAll(clan, quest); + LoggedSql.start(); + + // fetch when there are no buildings at all + Optional result = DB.find(ClanQuest.class) + .setId(quest.id) + .fetch("clan", "buildings") + .filterMany("clan.buildings").eq("type", Building.CAFE) + .findOneOrEmpty(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains(" left join building t2 on t2.clan_id = t1.id and t2.type = ? where t0.id = ? order by t0.id"); + assertThat(result).isPresent(); + List emptyBuildings = result.map(r -> r.clan.buildings).orElseThrow(); + assertThat(emptyBuildings).isEmpty(); + + // add some buildings + var b0 = new Building(clan, Building.CAFE, "b0"); + var b1 = new Building(clan, Building.CAFE, "b1"); + var b2 = new Building(clan, Building.HOUSE, "b2"); + DB.saveAll(b0, b1, b2); + + // fetch with some buildings to match the filter many predicate + Optional resultWithBuildings = DB.find(ClanQuest.class) + .setId(quest.id) + .fetch("clan", "buildings") + .filterMany("clan.buildings").eq("type", Building.CAFE) + .findOneOrEmpty(); + + List someBuildings = resultWithBuildings.map(r -> r.clan.buildings).orElseThrow(); + assertThat(someBuildings).hasSize(2); + assertThat(someBuildings.stream().map(b -> b.name).collect(Collectors.toList())).contains("b0", "b1"); + + // fetch with no matching buildings + Optional resultWithNoMatchingBuildings = DB.find(ClanQuest.class) + .setId(quest.id) + .fetch("clan", "buildings") + .filterMany("clan.buildings").eq("type", Building.STORE) + .findOneOrEmpty(); + + List noMatchingBuildings = resultWithNoMatchingBuildings.map(r -> r.clan.buildings).orElseThrow(); + assertThat(noMatchingBuildings).isEmpty(); + } + + @Test + void test_filterMany_with_isNotEmpty() { ResetBasicData.reset(); LoggedSql.start(); Query query = DB.find(Customer.class) - .filterMany("orders").raw("1=0") + .setUnmodifiable(true) + .select("id, status, name") + .fetch("orders", "status, orderDate, shipDate") + .filterMany("orders").raw("1 = 0") .where().isNotEmpty("orders") .query(); List list = query.findList(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains(" from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null and 1 = 0 where exists "); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.name, t1.id, t1.status, t1.order_date, t1.ship_date from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null and 1 = 0 where exists (select 1 from o_order x where x.kcustomer_id = t0.id and x.order_date is not null) order by t0.id; --bind()"); for (Customer customer : list) { assertThat(customer.getOrders()).isEmpty(); } - - List sqlList = LoggedSql.stop(); - assertEquals(1, sqlList.size()); - assertThat(sqlList.get(0)).contains("from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id where exists (select 1 from o_order x where x.kcustomer_id = t0.id and x.order_date is not null) and (t1.id is null or (1=0)) order by t0.id"); } @Test @@ -287,11 +339,10 @@ public void test_filterMany_copy_findList() { List sqlList = LoggedSql.stop(); assertEquals(1, sqlList.size()); - assertThat(sqlList.get(0)).contains("from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id where (t1.id is null or (t1.status "); if (isPostgresCompatible()) { - assertThat(sqlList.get(0)).contains("where (t1.id is null or (t1.status = any(?))) order by t0.id"); + assertThat(sqlList.get(0)).contains("left join o_customer t2 on t2.id = t1.kcustomer_id and t1.status = any(?) order by t0.id"); } else { - assertThat(sqlList.get(0)).contains("where (t1.id is null or (t1.status in (?))) order by t0.id"); + assertThat(sqlList.get(0)).contains("left join o_customer t2 on t2.id = t1.kcustomer_id and t1.status in (?) order by t0.id"); } } @@ -335,7 +386,7 @@ public void testDisjunction() { List sql = LoggedSql.stop(); assertEquals(1, sql.size()); - assertSql(sql.get(0)).contains(" from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id where (t1.id is null or ((t1.status = ? or t1.order_date = ?))) order by t0.id"); + assertSql(sql.get(0)).contains(" left join o_customer t2 on t2.id = t1.kcustomer_id and (t1.status = ? or t1.order_date = ?) order by t0.id"); } @Test @@ -351,7 +402,7 @@ public void testNestedFilterMany() { List sql = LoggedSql.stop(); assertThat(sql.size()).isGreaterThan(1); - assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id where (t1.id is null or (t1.first_name is not null)) order by t0.id; --bind()"); + assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id and t1.first_name is not null order by t0.id; --bind()"); platformAssertIn(sql.get(1), " from contact_note t0 where (t0.contact_id)"); assertSql(sql.get(1)).contains(" and lower(t0.title) like"); } @@ -389,9 +440,9 @@ public void testFilterManyUsingExpression() { assertThat(sql).hasSize(1); if (isSqlServer()) { - assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id where (t1.id is null or ((t1.first_name is not null and lower(t1.email) like ?))) order by t0.id; --bind(rob%)"); + assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id and (t1.first_name is not null and lower(t1.email) like ?) order by t0.id; --bind(rob%)"); } else { - assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id where (t1.id is null or ((t1.first_name is not null and lower(t1.email) like ? escape'|'))) order by t0.id; --bind(rob%)"); + assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id and (t1.first_name is not null and lower(t1.email) like ? escape'|') order by t0.id; --bind(rob%)"); } } } diff --git a/ebean-test/src/test/java/org/tests/query/TestQueryFilterManyOnM2M.java b/ebean-test/src/test/java/org/tests/query/TestQueryFilterManyOnM2M.java index ce7c79e9b3..3144c16536 100644 --- a/ebean-test/src/test/java/org/tests/query/TestQueryFilterManyOnM2M.java +++ b/ebean-test/src/test/java/org/tests/query/TestQueryFilterManyOnM2M.java @@ -20,7 +20,7 @@ void test() { List sql = LoggedSql.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("select t0.userid, t0.user_name, t0.user_type_id, t1.role_id, t1.role_name from muser t0 left join mrole_muser t1z_ on t1z_.muser_userid = t0.userid left join mrole t1 on t1.role_id = t1z_.mrole_role_id where (t1.role_id is null or (lower(t1.role_name) like "); + assertThat(sql.get(0)).contains(" left join mrole t1 on t1.role_id = t1z_.mrole_role_id and lower(t1.role_name) like "); assertThat(sql.get(0)).contains("order by t0.userid;"); } diff --git a/ebean-test/src/test/java/org/tests/query/TestQueryFilterManySimple.java b/ebean-test/src/test/java/org/tests/query/TestQueryFilterManySimple.java index 482a70e45b..35275bd5ff 100644 --- a/ebean-test/src/test/java/org/tests/query/TestQueryFilterManySimple.java +++ b/ebean-test/src/test/java/org/tests/query/TestQueryFilterManySimple.java @@ -35,7 +35,7 @@ public void test() { list.get(0).getOrders().size(); List sql = LoggedSql.stop(); assertThat(sql).hasSize(2); - assertThat(sql.get(0)).contains("from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id where"); + assertThat(sql.get(0)).contains("left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id and t1.status = ? and t1.order_date > ?"); assertThat(sql.get(0)).contains("order by t0.id"); if (isPostgresCompatible()) { assertThat(sql.get(1)).contains("from contact t0 where (t0.customer_id) = any(?) and t0.first_name is not null;"); diff --git a/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java b/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java index 2a54a0985a..4a73e3c0c1 100644 --- a/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java +++ b/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java @@ -785,9 +785,10 @@ void oneToMany_notNull() { .query(); List statusList = query.findSingleAttributeList(); - assertSql(query) + String sql = sqlOf(query); + assertThat(sql) .contains("select distinct t1.status from o_customer t0 " - + "left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null where (t1.id is null or (t1.status is not null))") + + "left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null and t1.status is not null") .doesNotContain("order by"); assertThat(statusList).hasSize(4);