Skip to content

Commit 720e033

Browse files
mipo256Михаил Поливаха
authored andcommitted
GH-2099 GH-1986 Consider Embedded properties in QBE
Signed-off-by: mipo256 <[email protected]> Signed-off-by: Михаил Поливаха <[email protected]>
1 parent 38f2af0 commit 720e033

File tree

3 files changed

+250
-41
lines changed

3 files changed

+250
-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: 144 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@
2222
import java.util.List;
2323
import java.util.Optional;
2424

25+
import org.jspecify.annotations.NonNull;
26+
import org.jspecify.annotations.Nullable;
27+
2528
import org.springframework.data.domain.Example;
29+
import org.springframework.data.domain.ExampleMatcher;
2630
import org.springframework.data.mapping.PersistentPropertyAccessor;
2731
import org.springframework.data.mapping.PropertyHandler;
32+
import org.springframework.data.mapping.PropertyPath;
2833
import org.springframework.data.mapping.context.MappingContext;
2934
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
3035
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -39,6 +44,7 @@
3944
* @since 2.2
4045
* @author Greg Turnquist
4146
* @author Jens Schauder
47+
* @author Mikhail Polivakha
4248
*/
4349
public class RelationalExampleMapper {
4450

@@ -64,92 +70,193 @@ public <T> Query getMappedExample(Example<T> example) {
6470
* {@link Query}.
6571
*
6672
* @param example
67-
* @param entity
73+
* @param persistentEntity
6874
* @return query
6975
*/
70-
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> entity) {
76+
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> persistentEntity) {
7177

7278
Assert.notNull(example, "Example must not be null");
73-
Assert.notNull(entity, "RelationalPersistentEntity must not be null");
79+
Assert.notNull(persistentEntity, "RelationalPersistentEntity must not be null");
7480

75-
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(example.getProbe());
81+
PersistentPropertyAccessor<T> probePropertyAccessor = persistentEntity.getPropertyAccessor(example.getProbe());
7682
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher());
7783

78-
final List<Criteria> criteriaBasedOnProperties = new ArrayList<>();
84+
final List<Criteria> criteriaBasedOnProperties = buildCriteria( //
85+
persistentEntity, //
86+
matcherAccessor, //
87+
probePropertyAccessor //
88+
);
7989

80-
entity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) property -> {
90+
// Criteria, assemble!
91+
Criteria criteria = Criteria.empty();
8192

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

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

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

94173
// If the value is empty, don't try to match against it
95-
if (!optionalConvertedPropValue.isPresent()) {
174+
if (optionalConvertedPropValue.isEmpty()) {
96175
return;
97176
}
98177

99178
Object convPropValue = optionalConvertedPropValue.get();
100-
boolean ignoreCase = matcherAccessor.isIgnoreCaseForPath(property.getName());
179+
boolean ignoreCase = matcherAccessor.isIgnoreCaseForPath(currentPropertyDotPath);
101180

102181
String column = property.getName();
103182

104-
switch (matcherAccessor.getStringMatcherForPath(property.getName())) {
183+
switch (matcherAccessor.getStringMatcherForPath(currentPropertyDotPath)) {
105184
case DEFAULT:
106185
case EXACT:
107-
criteriaBasedOnProperties.add(includeNulls(example) //
186+
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) //
108187
? Criteria.where(column).isNull().or(column).is(convPropValue).ignoreCase(ignoreCase)
109188
: Criteria.where(column).is(convPropValue).ignoreCase(ignoreCase));
110189
break;
111190
case ENDING:
112-
criteriaBasedOnProperties.add(includeNulls(example) //
191+
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) //
113192
? Criteria.where(column).isNull().or(column).like("%" + convPropValue).ignoreCase(ignoreCase)
114193
: Criteria.where(column).like("%" + convPropValue).ignoreCase(ignoreCase));
115194
break;
116195
case STARTING:
117-
criteriaBasedOnProperties.add(includeNulls(example) //
196+
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) //
118197
? Criteria.where(column).isNull().or(column).like(convPropValue + "%").ignoreCase(ignoreCase)
119198
: Criteria.where(column).like(convPropValue + "%").ignoreCase(ignoreCase));
120199
break;
121200
case CONTAINING:
122-
criteriaBasedOnProperties.add(includeNulls(example) //
201+
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) //
123202
? Criteria.where(column).isNull().or(column).like("%" + convPropValue + "%").ignoreCase(ignoreCase)
124203
: Criteria.where(column).like("%" + convPropValue + "%").ignoreCase(ignoreCase));
125204
break;
126205
default:
127-
throw new IllegalStateException(example.getMatcher().getDefaultStringMatcher() + " is not supported");
206+
throw new IllegalStateException(matcherAccessor.getDefaultStringMatcher() + " is not supported");
128207
}
129-
});
208+
}
130209

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

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

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

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

146254
/**
147-
* Does this {@link Example} need to include {@literal NULL} values in its {@link Criteria}?
255+
* Does this {@link ExampleMatcherAccessor} need to include {@literal NULL} values in its {@link Criteria}?
148256
*
149-
* @param example
150-
* @return whether or not to include nulls.
257+
* @return whether to include nulls.
151258
*/
152-
private static <T> boolean includeNulls(Example<T> example) {
153-
return example.getMatcher().getNullHandler() == NullHandler.INCLUDE;
259+
private static <T> boolean includeNulls(ExampleMatcherAccessor exampleMatcher) {
260+
return exampleMatcher.getNullHandler() == NullHandler.INCLUDE;
154261
}
155262
}

0 commit comments

Comments
 (0)