diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java index d56de43fff..0982035a4c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryDetail.java @@ -5,11 +5,13 @@ import io.ebean.util.SplitName; import io.ebeaninternal.api.SpiQueryManyJoin; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssoc; +import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; import io.ebeaninternal.server.el.ElPropertyDeploy; import io.ebeaninternal.server.el.ElPropertyValue; - import jakarta.persistence.PersistenceException; + import java.io.Serializable; import java.util.*; @@ -326,6 +328,41 @@ private void sortFetchPaths(BeanDescriptor d, OrmQueryProperties p, LinkedHas } } + /** + * After sorting, we try to convert id only fetches of a bean into a select of the parent bean. + */ + private void convertIdFetches(BeanDescriptor desc, Set nonRemovable) { + String[] paths = fetchPaths.keySet().toArray(new String[0]); + int i = paths.length; + // iterate backwards - otherwise we might detect id only fetches, which are later converted + while (i-- > 0) { + String path = paths[i]; + ElPropertyDeploy el = desc.elPropertyDeploy(path); + OrmQueryProperties prop = fetchPaths.get(path); + if (nonRemovable.contains(path)) { + // do not remove + } else if (el == null) { + throw new PersistenceException("Invalid fetch path " + path + " from " + desc.fullName()); + } else if (el.beanProperty() instanceof BeanPropertyAssocOne) { + BeanPropertyAssocOne assoc = (BeanPropertyAssocOne) el.beanProperty(); + if (assoc.hasForeignKeyConstraint()) { + // check, if we have exactly the ID selected and convert these fetches to a select of the parent bean + if (prop.includesExactly(assoc.descriptor().idName())) { + OrmQueryProperties parentProp = prop.getParentPath() == null ? baseProps : fetchPaths.get(prop.getParentPath()); + if (parentProp.hasProperties()) { + parentProp.addInclude(assoc.name()); + fetchPaths.remove(path); + prop = null; + } + } + } + } + if (prop != null && prop.getParentPath() != null) { + nonRemovable.add(prop.getParentPath()); + } + } + } + /** * Mark 'fetch joins' to 'many' properties over to 'query joins' where needed. * @@ -343,6 +380,8 @@ SpiQueryManyJoin markQueryJoins(BeanDescriptor beanDescriptor, String lazyLoa boolean fetchJoinFirstMany = allowOne; sortFetchPaths(beanDescriptor, addIds); + + convertIdFetches(beanDescriptor, new HashSet<>()); List pairs = sortByFetchPreference(beanDescriptor); for (FetchEntry pair : pairs) { 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..5648ec3c91 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 @@ -87,7 +87,7 @@ public OrmQueryProperties(String path, String rawProperties, FetchConfig fetchCo this.parentPath = SplitName.parent(path); OrmQueryPropertiesParser.Response response = OrmQueryPropertiesParser.parse(rawProperties); this.allProperties = response.allProperties; - this.included = response.included; + this.included = response.included; // modifiable if (fetchConfig != null) { this.fetchConfig = fetchConfig; this.cache = fetchConfig.isCache(); @@ -104,7 +104,7 @@ public OrmQueryProperties(String path, Set included) { OrmQueryProperties(String path, Set included, FetchConfig fetchConfig) { this.path = path; this.parentPath = SplitName.parent(path); - this.included = included; + this.included = (included == null) ? null : new LinkedHashSet<>(included); this.allProperties = false; this.fetchConfig = fetchConfig; this.cache = fetchConfig.isCache(); @@ -114,7 +114,7 @@ public OrmQueryProperties(String path, Set included) { this.path = path; this.parentPath = SplitName.parent(path); this.allProperties = other.allProperties; - this.included = other.included; + this.included = (other.included == null) ? null : new LinkedHashSet<>(other.included); this.fetchConfig = fetchConfig; this.cache = fetchConfig.isCache(); } @@ -191,7 +191,7 @@ private SpiExpressionList getFilterManyTrimPath(int trimPath) { * Adjust filterMany expressions for inclusion in main query. */ public void filterManyInline() { - if (filterMany != null){ + if (filterMany != null) { filterMany.prefixProperty(path); } } @@ -431,4 +431,15 @@ public void queryPlanHash(StringBuilder builder) { builder.append('}'); } + boolean includesExactly(String property) { + return included != null + && included.size() == 1 + && included.contains(property); + } + + void addInclude(String prop) { + if (!allProperties) { + included.add(prop); + } + } } diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/server/grammer/EqlParserTest.java b/ebean-test/src/test/java/io/ebean/xtest/internal/server/grammer/EqlParserTest.java index 16b64b7d9f..6f48d51464 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/server/grammer/EqlParserTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/server/grammer/EqlParserTest.java @@ -629,7 +629,7 @@ void select_agg_sum() { List details = query.findList(); assertThat(details).isNotEmpty(); - assertSql(query).contains("select sum(t0.order_qty), t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id group by t1.id"); + assertSql(query).contains("select sum(t0.order_qty), t0.order_id from o_order_detail t0 group by t0.order_id"); } @Test diff --git a/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java b/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java index f88f148297..97f861ca20 100644 --- a/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java +++ b/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java @@ -77,7 +77,7 @@ public void testFindListWithSelect() { List list = query.findList(); assertEquals(1, list.size()); - assertSql(query).contains("t0.id, t0.attr1, t0.id1, t0.id2, t1.id, t2.id"); + assertSql(query).isEqualTo("select t0.id, t0.attr1, t0.id1, t0.id2, t1.id, t2.id from main_entity_relation t0 left join main_entity t1 on t1.id = t0.id1 left join main_entity t2 on t2.id = t0.id2"); MainEntityRelation rel1 = list.get(0); assertEquals("ent1", rel1.getEntity1().getId()); diff --git a/ebean-test/src/test/java/org/tests/merge/TestMergeCustomer.java b/ebean-test/src/test/java/org/tests/merge/TestMergeCustomer.java index e18f2983ea..197c3d60ba 100644 --- a/ebean-test/src/test/java/org/tests/merge/TestMergeCustomer.java +++ b/ebean-test/src/test/java/org/tests/merge/TestMergeCustomer.java @@ -93,7 +93,7 @@ public void customerWithAddresses_setClientGeneratedIds_expect_selectAndUpdate() List sql = LoggedSql.stop(); assertThat(sql).hasSize(6); - assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?"); + assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?"); assertSql(sql.get(1)).contains("update maddress set street=?, city=?, version=? where id=? and version=?"); assertSqlBind(sql, 2, 3); assertThat(sql.get(5)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?"); @@ -122,7 +122,7 @@ public void customerWithAddresses_newAddress_setClientGeneratedIds_expect_insert List sql = LoggedSql.stop(); assertThat(sql).hasSize(8); - assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?"); + assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?"); assertSql(sql.get(1)).contains("insert into maddress (id, street, city, version) values (?,?,?,?)"); assertSqlBind(sql.get(2)); assertThat(sql.get(4)).contains("update maddress set street=?, city=?, version=? where id=? and version=?"); @@ -155,7 +155,7 @@ public void customerWithAddresses_newAddressWithId_setClientGeneratedIds_expect_ List sql = LoggedSql.stop(); assertThat(sql).hasSize(9); - assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?"); + assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?"); // Additional check to see if the address with the unknown UUID is 'insert' or 'update' assertSql(sql.get(1)).contains("select t0.id from maddress t0 where t0.id = ?"); @@ -317,7 +317,7 @@ public void fullMonty() { List sql = LoggedSql.stop(); if (isPersistBatchOnCascade()) { - assertSql(sql.get(0)).contains("select t0.id, t3.id, t1.id, t2.id from mcustomer t0 left join maddress t3 on t3.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id left join mcontact t2 on t2.customer_id = t0.id where t0.id = ?"); + assertSql(sql.get(0)).contains("select t0.id, t0.shipping_address_id, t0.billing_address_id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ? order by t0.id"); if (isH2() || isHana()) { // with nested OneToMany .. we need a second query to read the contact message ids assertSql(sql.get(1)).contains("select t0.contact_id, t0.id from mcontact_message t0 where (t0.contact_id) in (?,?,?,?,?,?,?,?,?,?)"); diff --git a/ebean-test/src/test/java/org/tests/query/TestFetchIdOnly.java b/ebean-test/src/test/java/org/tests/query/TestFetchIdOnly.java new file mode 100644 index 0000000000..c195206164 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/query/TestFetchIdOnly.java @@ -0,0 +1,57 @@ +package org.tests.query; + +import io.ebean.DB; +import io.ebean.Query; +import io.ebean.test.LoggedSql; +import io.ebean.text.PathProperties; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Test; +import org.tests.model.basic.Order; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestFetchIdOnly extends BaseTestCase { + + @Test + void test_withFetchPath() { + PathProperties root = PathProperties.parse("status,customer(id)"); + LoggedSql.start(); + Query query = DB.find(Order.class).apply(root); + query.findList(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0;"); + } + + @Test + void test_withSelect() { + LoggedSql.start(); + Query query = DB.find(Order.class).select("status, customer"); + query.findList(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0;"); + } + + @Test + void test_withFetch() { + LoggedSql.start(); + Query query = DB.find(Order.class).select("status").fetch("customer", "id"); + query.findList(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0;"); + } + + @Test + void test_withChildFetch() { + LoggedSql.start(); + Query query = DB.find(Order.class).select("status").fetch("customer.shippingAddress", "id"); + query.findList(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.id, t0.status, t1.id, t1.shipping_address_id from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id;"); + } +} diff --git a/ebean-test/src/test/java/org/tests/query/TestSubQuery.java b/ebean-test/src/test/java/org/tests/query/TestSubQuery.java index a5af4828c5..ba06bdae80 100644 --- a/ebean-test/src/test/java/org/tests/query/TestSubQuery.java +++ b/ebean-test/src/test/java/org/tests/query/TestSubQuery.java @@ -75,9 +75,9 @@ public void test_IsInWithFetchSubQuery1() { Query debugSq = sq.copy(); debugSq.findSingleAttribute(); if (isPostgresCompatible()) { - assertThat(debugSq.getGeneratedSql()).isEqualTo("select t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id where t0.product_id = any(?)"); + assertThat(debugSq.getGeneratedSql()).isEqualTo("select t0.order_id from o_order_detail t0 where t0.product_id = any(?)"); } else { - assertSql(debugSq.getGeneratedSql()).isEqualTo("select t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id where t0.product_id in (?)"); + assertSql(debugSq.getGeneratedSql()).isEqualTo("select t0.order_id from o_order_detail t0 where t0.product_id in (?)"); } Query query = DB.find(Order.class).select("shipDate").where().isIn("id", sq).query(); diff --git a/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java b/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java index e757bbce55..ca6e76c6e6 100644 --- a/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java +++ b/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java @@ -333,6 +333,20 @@ public void test_ChildPersonParentFindCount() { assertThat(loggedSql.get(0)).contains("where coalesce(f2.child_age, 0) = ?"); } + @Test + public void test_fetch_only() { + + LoggedSql.start(); + + DB.find(ChildPerson.class).select("name").fetch("parent.effectiveBean").findList(); + + List loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)).contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id)"); + } + @Test public void test_softRef() {