Skip to content

Commit 3d8fa6e

Browse files
committed
GH-2099 GH-1986 Consider Embedded properties in QBE
Signed-off-by: mipo256 <[email protected]> Polishing Signed-off-by: mipo256 <[email protected]>
1 parent 38f2af0 commit 3d8fa6e

File tree

3 files changed

+253
-41
lines changed

3 files changed

+253
-41
lines changed

spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,11 @@ public boolean isOrdered() {
262262

263263
@Override
264264
public boolean isEmbedded() {
265-
return isEmbedded || (isIdProperty() && isEntity());
265+
return isEmbedded || isCompositeId();
266+
}
267+
268+
private boolean isCompositeId() {
269+
return isIdProperty() && isEntity();
266270
}
267271

268272
@Override

spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalExampleMapper.java

Lines changed: 147 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,31 @@
2222
import java.util.List;
2323
import java.util.Optional;
2424

25+
import org.jetbrains.annotations.NotNull;
26+
import org.jspecify.annotations.NonNull;
27+
import org.jspecify.annotations.Nullable;
28+
2529
import org.springframework.data.domain.Example;
30+
import org.springframework.data.domain.ExampleMatcher;
2631
import org.springframework.data.mapping.PersistentPropertyAccessor;
2732
import org.springframework.data.mapping.PropertyHandler;
33+
import org.springframework.data.mapping.PropertyPath;
2834
import org.springframework.data.mapping.context.MappingContext;
2935
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3036
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
3137
import org.springframework.data.relational.core.query.Criteria;
3238
import org.springframework.data.relational.core.query.Query;
3339
import org.springframework.data.support.ExampleMatcherAccessor;
3440
import org.springframework.util.Assert;
41+
import org.springframework.util.StringUtils;
3542

3643
/**
3744
* Transform an {@link Example} into a {@link Query}.
3845
*
3946
* @since 2.2
4047
* @author Greg Turnquist
4148
* @author Jens Schauder
49+
* @author Mikhail Polivakha
4250
*/
4351
public class RelationalExampleMapper {
4452

@@ -64,92 +72,194 @@ public <T> Query getMappedExample(Example<T> example) {
6472
* {@link Query}.
6573
*
6674
* @param example
67-
* @param entity
75+
* @param persistentEntity
6876
* @return query
6977
*/
70-
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> entity) {
78+
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> persistentEntity) {
7179

7280
Assert.notNull(example, "Example must not be null");
73-
Assert.notNull(entity, "RelationalPersistentEntity must not be null");
81+
Assert.notNull(persistentEntity, "RelationalPersistentEntity must not be null");
7482

75-
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(example.getProbe());
83+
PersistentPropertyAccessor<T> probePropertyAccessor = persistentEntity.getPropertyAccessor(example.getProbe());
7684
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher());
7785

78-
final List<Criteria> criteriaBasedOnProperties = new ArrayList<>();
86+
final List<Criteria> criteriaBasedOnProperties = buildCriteriaRecursive( //
87+
persistentEntity, //
88+
matcherAccessor, //
89+
probePropertyAccessor //
90+
);
7991

80-
entity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) property -> {
92+
// Criteria, assemble!
93+
Criteria criteria = Criteria.empty();
8194

82-
if (property.isCollectionLike() || property.isMap()) {
83-
return;
84-
}
95+
for (Criteria propertyCriteria : criteriaBasedOnProperties) {
8596

86-
if (matcherAccessor.isIgnoredPath(property.getName())) {
87-
return;
97+
if (example.getMatcher().isAllMatching()) {
98+
criteria = criteria.and(propertyCriteria);
99+
} else {
100+
criteria = criteria.or(propertyCriteria);
88101
}
102+
}
103+
104+
return Query.query(criteria);
105+
}
106+
107+
private <T> @NotNull List<Criteria> buildCriteriaRecursive( //
108+
RelationalPersistentEntity<?> persistentEntity, //
109+
ExampleMatcherAccessor matcherAccessor, //
110+
PersistentPropertyAccessor<T> probePropertyAccessor //
111+
) {
112+
final List<Criteria> criteriaBasedOnProperties = new ArrayList<>();
113+
114+
persistentEntity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) property -> {
115+
potentiallyEnrichCriteriaByProcessingProperty(
116+
null,
117+
matcherAccessor, //
118+
probePropertyAccessor, //
119+
property, //
120+
criteriaBasedOnProperties //
121+
);
122+
});
123+
return criteriaBasedOnProperties;
124+
}
125+
126+
/**
127+
* Analyzes the incoming {@code property} and potentially enriches the {@code criteriaBasedOnProperties} with the new
128+
* {@link Criteria} for this property.
129+
* <p>
130+
* This algorithm is recursive in order to take the embedded properties into account. The caller can expect that the result
131+
* of this method call is fully processed subtree of an aggreagte where the passed {@code property} serves as the root.
132+
*
133+
* @param propertyPath the {@link PropertyPath} of the passed {@code property}.
134+
* @param matcherAccessor the accessor for the original {@link ExampleMatcher}.
135+
* @param entityPropertiesAccessor the accessor for the properties of the current entity that holds the given {@code property}
136+
* @param property the property under analysis
137+
* @param criteriaBasedOnProperties the {@link List} of criteria objects that potentially gets enriched as a
138+
* result of the incoming {@code property} processing
139+
*/
140+
private <T> void potentiallyEnrichCriteriaByProcessingProperty(
141+
@Nullable PropertyPath propertyPath,
142+
ExampleMatcherAccessor matcherAccessor, //
143+
PersistentPropertyAccessor<T> entityPropertiesAccessor, //
144+
RelationalPersistentProperty property, //
145+
List<Criteria> criteriaBasedOnProperties //
146+
) {
147+
148+
// QBE do not support queries on Child aggregates yet
149+
if (property.isCollectionLike() || property.isMap()) {
150+
return;
151+
}
152+
153+
PropertyPath currentPropertyPath = resolveCurrentPropertyPath(propertyPath, property);
154+
String currentPropertyDotPath = currentPropertyPath.toDotPath();
155+
156+
if (matcherAccessor.isIgnoredPath(currentPropertyDotPath)) {
157+
return;
158+
}
89159

160+
Object actualPropertyValue = entityPropertiesAccessor.getProperty(property);
161+
162+
if (property.isEmbedded() && actualPropertyValue != null) {
163+
processEmbeddedRecursively( //
164+
matcherAccessor, //
165+
actualPropertyValue,
166+
property, //
167+
criteriaBasedOnProperties, //
168+
currentPropertyPath //
169+
);
170+
} else {
90171
Optional<?> optionalConvertedPropValue = matcherAccessor //
91-
.getValueTransformerForPath(property.getName()) //
92-
.apply(Optional.ofNullable(propertyAccessor.getProperty(property)));
172+
.getValueTransformerForPath(currentPropertyDotPath) //
173+
.apply(Optional.ofNullable(actualPropertyValue));
93174

94175
// If the value is empty, don't try to match against it
95-
if (!optionalConvertedPropValue.isPresent()) {
176+
if (optionalConvertedPropValue.isEmpty()) {
96177
return;
97178
}
98179

99180
Object convPropValue = optionalConvertedPropValue.get();
100-
boolean ignoreCase = matcherAccessor.isIgnoreCaseForPath(property.getName());
181+
boolean ignoreCase = matcherAccessor.isIgnoreCaseForPath(currentPropertyDotPath);
101182

102183
String column = property.getName();
103184

104-
switch (matcherAccessor.getStringMatcherForPath(property.getName())) {
185+
switch (matcherAccessor.getStringMatcherForPath(currentPropertyDotPath)) {
105186
case DEFAULT:
106187
case EXACT:
107-
criteriaBasedOnProperties.add(includeNulls(example) //
188+
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) //
108189
? Criteria.where(column).isNull().or(column).is(convPropValue).ignoreCase(ignoreCase)
109190
: Criteria.where(column).is(convPropValue).ignoreCase(ignoreCase));
110191
break;
111192
case ENDING:
112-
criteriaBasedOnProperties.add(includeNulls(example) //
193+
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) //
113194
? Criteria.where(column).isNull().or(column).like("%" + convPropValue).ignoreCase(ignoreCase)
114195
: Criteria.where(column).like("%" + convPropValue).ignoreCase(ignoreCase));
115196
break;
116197
case STARTING:
117-
criteriaBasedOnProperties.add(includeNulls(example) //
198+
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) //
118199
? Criteria.where(column).isNull().or(column).like(convPropValue + "%").ignoreCase(ignoreCase)
119200
: Criteria.where(column).like(convPropValue + "%").ignoreCase(ignoreCase));
120201
break;
121202
case CONTAINING:
122-
criteriaBasedOnProperties.add(includeNulls(example) //
203+
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) //
123204
? Criteria.where(column).isNull().or(column).like("%" + convPropValue + "%").ignoreCase(ignoreCase)
124205
: Criteria.where(column).like("%" + convPropValue + "%").ignoreCase(ignoreCase));
125206
break;
126207
default:
127-
throw new IllegalStateException(example.getMatcher().getDefaultStringMatcher() + " is not supported");
208+
throw new IllegalStateException(matcherAccessor.getDefaultStringMatcher() + " is not supported");
128209
}
129-
});
210+
}
130211

131-
// Criteria, assemble!
132-
Criteria criteria = Criteria.empty();
212+
}
133213

134-
for (Criteria propertyCriteria : criteriaBasedOnProperties) {
214+
/**
215+
* Processes an embedded entity's properties recursively.
216+
*
217+
* @param matcherAccessor the input matcher on the {@link Example#getProbe() original probe}.
218+
* @param value the actual embedded object.
219+
* @param property the embedded property.
220+
* @param criteriaBasedOnProperties collection of {@link Criteria} objects to potentially enrich.
221+
* @param currentPropertyPath the dot-separated path of the passed {@code property}.
222+
*/
223+
private void processEmbeddedRecursively(
224+
ExampleMatcherAccessor matcherAccessor,
225+
Object value,
226+
RelationalPersistentProperty property,
227+
List<Criteria> criteriaBasedOnProperties,
228+
PropertyPath currentPropertyPath
229+
) {
230+
RelationalPersistentEntity<?> embeddedPersistentEntity = mappingContext.getPersistentEntity(property.getTypeInformation());
135231

136-
if (example.getMatcher().isAllMatching()) {
137-
criteria = criteria.and(propertyCriteria);
138-
} else {
139-
criteria = criteria.or(propertyCriteria);
140-
}
141-
}
232+
PersistentPropertyAccessor<?> embeddedEntityPropertyAccessor = embeddedPersistentEntity.getPropertyAccessor(value);
142233

143-
return Query.query(criteria);
234+
embeddedPersistentEntity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) embeddedProperty ->
235+
potentiallyEnrichCriteriaByProcessingProperty(
236+
currentPropertyPath,
237+
matcherAccessor,
238+
embeddedEntityPropertyAccessor,
239+
embeddedProperty,
240+
criteriaBasedOnProperties
241+
)
242+
);
243+
}
244+
245+
@NonNull
246+
private static PropertyPath resolveCurrentPropertyPath(@Nullable PropertyPath propertyPath, RelationalPersistentProperty property) {
247+
PropertyPath currentPropertyPath;
248+
249+
if (propertyPath == null) {
250+
currentPropertyPath = PropertyPath.from(property.getName(), property.getOwner().getTypeInformation());
251+
} else {
252+
currentPropertyPath = propertyPath.nested(property.getName());
253+
}
254+
return currentPropertyPath;
144255
}
145256

146257
/**
147-
* Does this {@link Example} need to include {@literal NULL} values in its {@link Criteria}?
258+
* Does this {@link ExampleMatcherAccessor} need to include {@literal NULL} values in its {@link Criteria}?
148259
*
149-
* @param example
150-
* @return whether or not to include nulls.
260+
* @return whether to include nulls.
151261
*/
152-
private static <T> boolean includeNulls(Example<T> example) {
153-
return example.getMatcher().getNullHandler() == NullHandler.INCLUDE;
262+
private static <T> boolean includeNulls(ExampleMatcherAccessor exampleMatcher) {
263+
return exampleMatcher.getNullHandler() == NullHandler.INCLUDE;
154264
}
155265
}

0 commit comments

Comments
 (0)