Fix OrmQueryProperties.filterManyHasNestedProperty() with real nested property check

This commit is contained in:
robin.bygrave
2026-07-15 23:08:51 +12:00
parent 9947a41a9b
commit 99c3d8b07b
3 changed files with 27 additions and 18 deletions
@@ -12,7 +12,7 @@ public final class SpiExpressionValidation {
private final BeanType<?> desc;
private final LinkedHashSet<String> unknown = new LinkedHashSet<>();
private boolean nestedProperty;
private final LinkedHashSet<String> 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<String> allProperties() {
return all;
}
}
@@ -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();
}
}
@@ -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;
}
/**