From 99c3d8b07b4bd3314cd7a18acaf77a41d8ea99c5 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Wed, 15 Jul 2026 23:08:51 +1200 Subject: [PATCH] Fix OrmQueryProperties.filterManyHasNestedProperty() with real nested property check --- .../api/SpiExpressionValidation.java | 17 ++++++++--------- .../server/querydefn/OrmQueryDetail.java | 9 +++++---- .../server/querydefn/OrmQueryProperties.java | 19 ++++++++++++++----- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpressionValidation.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpressionValidation.java index e3df6a717..2c62437bb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpressionValidation.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiExpressionValidation.java @@ -12,7 +12,7 @@ public final class SpiExpressionValidation { private final BeanType desc; private final LinkedHashSet unknown = new LinkedHashSet<>(); - private boolean nestedProperty; + private final LinkedHashSet all = new LinkedHashSet<>(); public SpiExpressionValidation(BeanType desc) { this.desc = desc; @@ -22,9 +22,7 @@ public final class SpiExpressionValidation { * Validate that the property expression (path) is valid. */ public void validate(String propertyName) { - if (!nestedProperty && propertyName.indexOf('.') > -1) { - nestedProperty = true; - } + all.add(propertyName); if (!desc.isValidExpression(propertyName)) { unknown.add(propertyName); } @@ -38,12 +36,13 @@ public final class SpiExpressionValidation { } /** - * Return true if any property visited during this validation referenced a nested/associated - * path (i.e. contained a '.'). Used to inspect the shape of an expression without needing a - * correctly-typed bean descriptor. + * Return the set of all property names visited during this validation, regardless of + * whether they were considered valid against the bean type. Used to inspect the shape of + * an expression (for example, to check whether it references any associated/joined path) + * without needing a correctly-typed bean descriptor. */ - public boolean hasNestedProperty() { - return nestedProperty; + public Set allProperties() { + return all; } } 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 a45023c27..9fa2411b0 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 @@ -4,7 +4,6 @@ import io.ebean.FetchConfig; import io.ebean.event.BeanQueryRequest; import io.ebean.util.SplitName; import io.ebeaninternal.api.SpiExpressionList; -import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.api.SpiQueryManyJoin; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanPropertyAssoc; @@ -385,7 +384,8 @@ public final class OrmQueryDetail implements Serializable { OrmQueryProperties chunk = pair.getProperties(); if (isQueryJoinCandidate(lazyLoadManyPath, chunk)) { // this is a 'fetch join' (included in main query) - if (fetchJoinFirstMany && !chunk.filterManyHasNestedProperty(new SpiExpressionValidation(beanDescriptor))) { + BeanDescriptor targetDescriptor = ((BeanPropertyAssoc) elProp.beanProperty()).targetDescriptor(); + if (fetchJoinFirstMany && !chunk.filterManyHasNestedProperty(targetDescriptor)) { // letting the first one remain a 'fetch join' fetchJoinFirstMany = false; manyFetchProperty = pair.getPath(); @@ -393,8 +393,9 @@ public final class OrmQueryDetail implements Serializable { many = elProp; } else { // convert this one over to a 'query join' - either because another many has already claimed the - // 'fetch join' slot, or because its filterMany references a nested property that can't safely be - // included as a JOIN predicate (see OrmQueryProperties.filterManyHasNestedProperty) + // 'fetch join' slot, or because its filterMany references a property that requires crossing into + // an associated bean and can't safely be included as a JOIN predicate (see + // OrmQueryProperties.filterManyHasNestedProperty) chunk.markForQueryJoin(); } } 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 88dc02df4..97003ae38 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 @@ -11,6 +11,8 @@ import io.ebeaninternal.api.SpiExpressionFactory; import io.ebeaninternal.api.SpiExpressionList; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.el.ElPropertyValue; import io.ebeaninternal.server.expression.FilterExprPath; import io.ebeaninternal.server.expression.FilterExpressionList; @@ -236,16 +238,23 @@ public final class OrmQueryProperties implements Serializable { } /** - * Return true if the filterMany expression (if any) references a nested/associated - * property - a dotted path such as {@code "group.name"} - rather than only direct - * properties of the many bean itself. + * Return true if the filterMany expression (if any) references a property that requires + * crossing into an associated bean/join - e.g. {@code "group.name"} - rather than only + * plain/embedded properties resolving to columns on the many bean's own base table. */ - boolean filterManyHasNestedProperty(SpiExpressionValidation validation) { + boolean filterManyHasNestedProperty(BeanDescriptor targetDescriptor) { if (filterMany == null) { return false; } + SpiExpressionValidation validation = new SpiExpressionValidation(targetDescriptor); filterMany.validate(validation); - return validation.hasNestedProperty(); + for (String property : validation.allProperties()) { + ElPropertyValue elProp = targetDescriptor.elGetValue(property); + if (elProp != null && elProp.isAssocProperty()) { + return true; + } + } + return false; } /**