From 540ceab96f218e2877c75ca53064f5a99986c08d Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 20 Mar 2024 21:48:43 +1300 Subject: [PATCH] feat: Add query.setHint() to support sql hint as inline comment in select queries (#3365) * feat: Add query.setHint() to support sql hint as inline comment in select queries This is for ORM queries only. An ORM query like: new QCustomer() .setHint("FirstRows") .select(QCustomer.Alias.id, QCustomer.Alias.name) .findList(); Produces SQL that includes the hint as an inline comment like: select /*+ FirstRows */ t0.id, t0.name from customer t0 * Fix test QOrderTest --- ebean-api/src/main/java/io/ebean/Query.java | 8 +++++++ .../config/dbplatform/DatabasePlatform.java | 4 ++++ .../java/io/ebeaninternal/api/SpiQuery.java | 5 +++++ .../server/persist/BatchControl.java | 2 +- .../server/query/CQueryBuilder.java | 7 ++++++- .../server/query/DefaultFetchGroupQuery.java | 5 +++++ .../server/querydefn/DefaultOrmQuery.java | 16 ++++++++++++++ .../server/querydefn/OrmQueryPlanKeyTest.java | 21 +++++++++++++++++++ .../java/io/ebean/typequery/TQRootBean.java | 11 ++++++++++ .../test/java/org/querytest/QOrderTest.java | 19 +++++++++++++---- .../ebean/xtest/base/DtoQueryFromOrmTest.java | 3 ++- 11 files changed, 94 insertions(+), 7 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/Query.java b/ebean-api/src/main/java/io/ebean/Query.java index ef941aa1f..c48474ac7 100644 --- a/ebean-api/src/main/java/io/ebean/Query.java +++ b/ebean-api/src/main/java/io/ebean/Query.java @@ -1601,6 +1601,14 @@ public interface Query extends CancelableQuery { */ Query setLabel(String label); + /** + * Set a SQL query hint. + *

+ * This results in an inline comment that immediately follows + * after the select keyword in the form: {@code /*+ hint *\/ } + */ + Query setHint(String hint); + /** * Set to true if this query should execute against the doc store. *

diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java index 67051b51d..29b554c48 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java @@ -776,4 +776,8 @@ public class DatabasePlatform { } return "/* " + label + " */ "; } + + public String inlineSqlHint(String hint) { + return "/*+ " + hint + " */ "; + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java index 5669dcf57..a4696508d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -226,6 +226,11 @@ public interface SpiQuery extends Query, SpiQueryFetch, TxnProfileEventCod */ ProfileLocation profileLocation(); + /** + * Return the SQL hint to include in the query. + */ + String hint(); + /** * Return the label set on the query. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java index 08e6bc6ab..24ed757e4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/BatchControl.java @@ -226,7 +226,7 @@ public final class BatchControl { void executeNow(ArrayList list) throws BatchedSqlException { boolean old = transaction.isFlushOnQuery(); transaction.setFlushOnQuery(false); - // disable flush on query due transsaction callbacks + // disable flush on query due transaction callbacks try { for (int i = 0; i < list.size(); i++) { if (i % batchSize == 0) { 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 d94dde3a9..9147eefe0 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 @@ -595,7 +595,7 @@ final class CQueryBuilder { } private void appendSelectDistinct() { - sb.append("select ").append(inlineSqlComment()); + sb.append("select ").append(hint()).append(inlineSqlComment()); if (distinct && !countSingleAttribute) { if (request.isInlineCountDistinct()) { sb.append("count("); @@ -608,6 +608,11 @@ final class CQueryBuilder { } } + private String hint() { + String hint = query.hint(); + return hint == null ? "" : dbPlatform.inlineSqlHint(hint); + } + private String inlineSqlComment() { if (!includeLabelInSql) { return ""; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java index c4a3b965b..aca22b8bc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java @@ -133,6 +133,11 @@ final class DefaultFetchGroupQuery implements SpiFetchGroupQuery, SpiQuery return this; } + @Override + public Query setHint(String hint) { + throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); + } + // Everything else deemed invalid @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index f877be126..abe89c40a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -57,6 +57,7 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { private ProfilingListener profilingListener; private Type type; private String label; + private String hint; private Mode mode = Mode.NORMAL; private boolean usingFuture; private Object tenantId; @@ -235,6 +236,11 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { return label; } + @Override + public final String hint() { + return hint; + } + @Override public final String planLabel() { if (label != null) { @@ -258,6 +264,12 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { return this; } + @Override + public final Query setHint(String hint) { + this.hint = hint; + return this; + } + @Override public final boolean isAutoTunable() { return nativeSql == null && beanDescriptor.isAutoTunable(); @@ -732,6 +744,7 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { copy.timeout = timeout; copy.mapKey = mapKey; copy.id = id; + copy.hint = hint; copy.label = label; copy.nativeSql = nativeSql; copy.useBeanCache = useBeanCache; @@ -1069,6 +1082,9 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { if (manualId) { sb.append("/md"); } + if (hint != null) { + sb.append("/h:").append(hint); + } if (distinct) { sb.append("/dt"); } diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKeyTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKeyTest.java index 6e181573b..fbe02353d 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKeyTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKeyTest.java @@ -29,6 +29,27 @@ public class OrmQueryPlanKeyTest extends BaseTest { assertSame(query().createQueryPlanKey(), query().createQueryPlanKey()); } + @Test + public void equals_when_hintIsDifferent_expect_different() { + DefaultOrmQuery q1 = query(); + q1.setHint("a"); + assertDifferent(q1, query()); + + DefaultOrmQuery q2 = query(); + q2.setHint("b"); + assertDifferent(q1, q2); + } + + @Test + public void equals_when_hintIsSame() { + DefaultOrmQuery q1 = query(); + q1.setHint("b"); + DefaultOrmQuery q2 = query(); + q2.setHint("b"); + + assertSame(q1, q2); + } + @Test public void equals_when_diffTableJoinNull() { 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 48125150e..4584bea31 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java @@ -912,6 +912,17 @@ public abstract class TQRootBean { return root; } + /** + * Set a SQL query hint. + *

+ * This results in an inline comment that immediately follows + * after the select keyword in the form: {@code /*+ hint *\/ } + */ + public R setHint(String hint) { + query.setHint(hint); + return root; + } + /** * Set the profile location. *

diff --git a/ebean-querybean/src/test/java/org/querytest/QOrderTest.java b/ebean-querybean/src/test/java/org/querytest/QOrderTest.java index 77ee35ac4..9fd331291 100644 --- a/ebean-querybean/src/test/java/org/querytest/QOrderTest.java +++ b/ebean-querybean/src/test/java/org/querytest/QOrderTest.java @@ -79,6 +79,19 @@ class QOrderTest { DB.delete(customer); } + @Test + void hint() { + LoggedSql.start(); + new QCustomer() + .setHint("FirstRows") + .select(QCustomer.Alias.id, QCustomer.Alias.name) + .findList(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select /*+ FirstRows */ /* QOrderTest.hint */ t0.id, t0.name from be_customer t0"); + } + @Test void fetchQueryWithBatch() { LoggedSql.start(); @@ -159,8 +172,7 @@ class QOrderTest { @Test void viaFetchGraph_withNested_fetchQuery() { - - DB.getDefault(); + DB.cacheManager().clearAll(); LoggedSql.start(); final Order found = new QOrder() @@ -181,8 +193,7 @@ class QOrderTest { @Test void viaFetchGraph_withNested_fetchCache() { - - DB.getDefault(); + DB.cacheManager().clearAll(); // ensure the customer is loaded in the L2 cache new QCustomer().id.eq(customer.getId()).findOne(); diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java index 83982fbc1..f9f8d02cd 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java @@ -164,6 +164,7 @@ public class DtoQueryFromOrmTest extends BaseTestCase { DtoQuery query = DB.find(Contact.class) // we must explicitly add the id property for DTO query (if we want it) + .setHint("SomeHint") .select("id, email, " + concat("lastName", ", ", "firstName") + " as fullName") .where() .isNotNull("email") @@ -183,7 +184,7 @@ public class DtoQueryFromOrmTest extends BaseTestCase { } List sql = LoggedSql.stop(); - assertSql(sql.get(0)).contains("select /* explicitId */ t0.id, t0.email, " + concat("t0.last_name", ", ", "t0.first_name") + assertSql(sql.get(0)).contains("select /*+ SomeHint */ /* explicitId */ t0.id, t0.email, " + concat("t0.last_name", ", ", "t0.first_name") + " fullName from contact t0 where t0.email is not null and t0.last_name is not null order by t0.last_name"); }