diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java index 1a7372ebc..14a1e92b5 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java @@ -1,5 +1,6 @@ package io.ebean.typequery; +import io.avaje.lang.Nullable; import io.ebean.ExpressionList; import io.ebean.FetchConfig; import io.ebean.FetchGroup; @@ -7,6 +8,7 @@ import io.ebeaninternal.api.SpiQueryFetch; import io.ebeaninternal.server.querydefn.OrmQueryDetail; import io.ebeaninternal.server.querydefn.SpiFetchGroup; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.Set; @@ -193,6 +195,17 @@ public abstract class TQAssocBean extends TQProperty { return _root; } + /** + * Is EQUAL TO if value is non-null and otherwise no expression is added to the query. + *

+ * This is effectively a helper method that allows a query to be built in fluid style where some predicates are + * effectively optional. We can use eqIfPresent() rather than having a separate if block. + */ + public final R eqIfPresent(@Nullable T other) { + expr().eqIfPresent(_name, other); + return _root; + } + /** * Is equal to by ID property. */ @@ -215,6 +228,95 @@ public abstract class TQAssocBean extends TQProperty { return ne(other); } + /** + * Is in a list of values. + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + @SafeVarargs + public final R in(T... values) { + expr().in(_name, (Object[]) values); + return _root; + } + + /** + * Is in a list of values. + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + public final R in(Collection values) { + expr().in(_name, values); + return _root; + } + + /** + * In where null or empty values means that no predicate is added to the query. + *

+ * That is, only add the IN predicate if the values are not null or empty. + *

+ * Without this we typically need to code an if block to only add + * the IN predicate if the collection is not empty like: + *

+ * + *

Without inOrEmpty()

+ *
{@code
+   *
+   *   List names = Arrays.asList("foo", "bar");
+   *
+   *   QCustomer query = new QCustomer()
+   *       .registered.before(LocalDate.now())
+   *
+   *   // conditionally add the IN expression to the query
+   *   if (names != null && !names.isEmpty()) {
+   *       query.name.in(names)
+   *   }
+   *
+   *   query.findList();
+   *
+   * }
+ * + *

Using inOrEmpty()

+ *
{@code
+   *
+   *   List names = Arrays.asList("foo", "bar");
+   *
+   *   new QCustomer()
+   *       .registered.before(LocalDate.now())
+   *       .name.inOrEmpty(names)
+   *       .findList();
+   *
+   * }
+ */ + public final R inOrEmpty(Collection values) { + expr().inOrEmpty(_name, values); + return _root; + } + + /** + * Is NOT in a list of values. + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + public final R notIn(Collection values) { + expr().notIn(_name, values); + return _root; + } + + /** + * Is NOT in a list of values. + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + @SafeVarargs + public final R notIn(T... values) { + expr().notIn(_name, (Object[]) values); + return _root; + } + /** * Apply a filter when fetching these beans. */ diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java index 01d3d1ccb..8ba6e51f7 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java @@ -930,7 +930,7 @@ public abstract class TQRootBean { /** * Specify the PersistenceContextScope to use for this query. *

- * When this is not set the 'default' configured on {@link io.ebean.config.ServerConfig#setPersistenceContextScope(PersistenceContextScope)} + * When this is not set the 'default' configured on {@link io.ebean.config.DatabaseConfig#setPersistenceContextScope(PersistenceContextScope)} * is used - this value defaults to {@link io.ebean.PersistenceContextScope#TRANSACTION}. *

* Note that the same persistence Context is used for subsequent lazy loading and query join queries. @@ -2117,12 +2117,10 @@ public abstract class TQRootBean { * Return the current expression list that expressions should be added to. */ protected ExpressionList peekExprList() { - if (textMode) { // return the current text expression list return _peekText(); } - if (whereStack == null) { whereStack = new ArrayStack<>(); whereStack.push(query.where()); diff --git a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java index d8f0dff38..330de2f19 100644 --- a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java @@ -2,6 +2,7 @@ package org.querytest; import io.ebean.*; import io.ebean.annotation.Transactional; +import io.ebean.test.LoggedSql; import io.ebean.types.Inet; import org.example.domain.*; import org.example.domain.otherpackage.PhoneNumber; @@ -315,10 +316,11 @@ public class QCustomerTest { @Test public void testAssocOne() { - + DB.getDefault(); Address address = new Address(); address.setId(41L); + LoggedSql.start(); new QCustomer() .billingAddress.eq(address) .findList(); @@ -334,6 +336,85 @@ public class QCustomerTest { new QCustomer() .billingAddress.notEqualTo(address) .findList(); + + new QCustomer() + .billingAddress.eqIfPresent(address) + .name.isNull() + .findList(); + + new QCustomer() + .billingAddress.eqIfPresent(null) + .name.isNull() + .findList(); + + + List sql = LoggedSql.stop(); + + assertThat(sql).hasSize(6); + assertThat(sql.get(0)).contains("where t0.billing_address_id = ?"); + assertThat(sql.get(1)).contains("where t0.billing_address_id = ?"); + assertThat(sql.get(2)).contains("where t0.billing_address_id <> ?"); + assertThat(sql.get(3)).contains("where t0.billing_address_id <> ?"); + assertThat(sql.get(4)).contains("where t0.billing_address_id = ? and t0.name is null"); + assertThat(sql.get(5)).contains("where t0.name is null"); + } + + @Test + public void testAssocOne_in() { + DB.getDefault(); + Address address = new Address(); + address.setId(41L); + Address address2 = new Address(); + address2.setId(41L); + + LoggedSql.start(); + new QCustomer() + .billingAddress.in(address, address2) + .findList(); + + new QCustomer() + .billingAddress.in(List.of(address, address2)) + .findList(); + + new QCustomer() + .billingAddress.inOrEmpty(List.of(address, address2)) + .name.isNull() + .findList(); + + new QCustomer() + .billingAddress.inOrEmpty(List.of()) + .name.isNull() + .findList(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(4); + assertThat(sql.get(0)).contains("where t0.billing_address_id in (?,?)"); + assertThat(sql.get(1)).contains("where t0.billing_address_id in (?,?)"); + assertThat(sql.get(2)).contains("where t0.billing_address_id in (?,?) and t0.name is null"); + assertThat(sql.get(3)).contains("where t0.name is null"); + } + + @Test + public void testAssocOne_notIn() { + DB.getDefault(); + Address address = new Address(); + address.setId(41L); + Address address2 = new Address(); + address2.setId(41L); + + LoggedSql.start(); + new QCustomer() + .billingAddress.notIn(address, address2) + .findList(); + + new QCustomer() + .billingAddress.notIn(List.of(address, address2)) + .findList(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("where t0.billing_address_id not in (?,?)"); + assertThat(sql.get(1)).contains("where t0.billing_address_id not in (?,?)"); } @Test