-
Notifications
You must be signed in to change notification settings - Fork 365
Consider embedded properties in the QBE queries #2100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,23 +22,31 @@ | |
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import org.jetbrains.annotations.NotNull; | ||
import org.jspecify.annotations.NonNull; | ||
import org.jspecify.annotations.Nullable; | ||
|
||
import org.springframework.data.domain.Example; | ||
import org.springframework.data.domain.ExampleMatcher; | ||
import org.springframework.data.mapping.PersistentPropertyAccessor; | ||
import org.springframework.data.mapping.PropertyHandler; | ||
import org.springframework.data.mapping.PropertyPath; | ||
import org.springframework.data.mapping.context.MappingContext; | ||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; | ||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; | ||
import org.springframework.data.relational.core.query.Criteria; | ||
import org.springframework.data.relational.core.query.Query; | ||
import org.springframework.data.support.ExampleMatcherAccessor; | ||
import org.springframework.util.Assert; | ||
import org.springframework.util.StringUtils; | ||
|
||
/** | ||
* Transform an {@link Example} into a {@link Query}. | ||
* | ||
* @since 2.2 | ||
* @author Greg Turnquist | ||
* @author Jens Schauder | ||
* @author Mikhail Polivakha | ||
*/ | ||
public class RelationalExampleMapper { | ||
|
||
|
@@ -64,92 +72,194 @@ public <T> Query getMappedExample(Example<T> example) { | |
* {@link Query}. | ||
* | ||
* @param example | ||
* @param entity | ||
* @param persistentEntity | ||
* @return query | ||
*/ | ||
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> entity) { | ||
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> persistentEntity) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason behind the renaming? |
||
|
||
Assert.notNull(example, "Example must not be null"); | ||
Assert.notNull(entity, "RelationalPersistentEntity must not be null"); | ||
Assert.notNull(persistentEntity, "RelationalPersistentEntity must not be null"); | ||
|
||
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(example.getProbe()); | ||
PersistentPropertyAccessor<T> probePropertyAccessor = persistentEntity.getPropertyAccessor(example.getProbe()); | ||
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher()); | ||
|
||
final List<Criteria> criteriaBasedOnProperties = new ArrayList<>(); | ||
final List<Criteria> criteriaBasedOnProperties = buildCriteriaRecursive( // | ||
persistentEntity, // | ||
matcherAccessor, // | ||
probePropertyAccessor // | ||
); | ||
|
||
entity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) property -> { | ||
// Criteria, assemble! | ||
Criteria criteria = Criteria.empty(); | ||
|
||
if (property.isCollectionLike() || property.isMap()) { | ||
return; | ||
} | ||
for (Criteria propertyCriteria : criteriaBasedOnProperties) { | ||
|
||
if (matcherAccessor.isIgnoredPath(property.getName())) { | ||
return; | ||
if (example.getMatcher().isAllMatching()) { | ||
criteria = criteria.and(propertyCriteria); | ||
} else { | ||
criteria = criteria.or(propertyCriteria); | ||
} | ||
} | ||
|
||
return Query.query(criteria); | ||
} | ||
|
||
private <T> @NotNull List<Criteria> buildCriteriaRecursive( // | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Care to remove all those lingering |
||
RelationalPersistentEntity<?> persistentEntity, // | ||
ExampleMatcherAccessor matcherAccessor, // | ||
PersistentPropertyAccessor<T> probePropertyAccessor // | ||
) { | ||
final List<Criteria> criteriaBasedOnProperties = new ArrayList<>(); | ||
|
||
persistentEntity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) property -> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Use a for-loop instead of callback-style to reduce stack frames. |
||
potentiallyEnrichCriteriaByProcessingProperty( | ||
null, | ||
matcherAccessor, // | ||
probePropertyAccessor, // | ||
property, // | ||
criteriaBasedOnProperties // | ||
); | ||
}); | ||
return criteriaBasedOnProperties; | ||
} | ||
|
||
/** | ||
* Analyzes the incoming {@code property} and potentially enriches the {@code criteriaBasedOnProperties} with the new | ||
* {@link Criteria} for this property. | ||
* <p> | ||
* This algorithm is recursive in order to take the embedded properties into account. The caller can expect that the result | ||
* of this method call is fully processed subtree of an aggreagte where the passed {@code property} serves as the root. | ||
* | ||
* @param propertyPath the {@link PropertyPath} of the passed {@code property}. | ||
* @param matcherAccessor the accessor for the original {@link ExampleMatcher}. | ||
* @param entityPropertiesAccessor the accessor for the properties of the current entity that holds the given {@code property} | ||
* @param property the property under analysis | ||
* @param criteriaBasedOnProperties the {@link List} of criteria objects that potentially gets enriched as a | ||
* result of the incoming {@code property} processing | ||
*/ | ||
private <T> void potentiallyEnrichCriteriaByProcessingProperty( | ||
@Nullable PropertyPath propertyPath, | ||
ExampleMatcherAccessor matcherAccessor, // | ||
PersistentPropertyAccessor<T> entityPropertiesAccessor, // | ||
RelationalPersistentProperty property, // | ||
List<Criteria> criteriaBasedOnProperties // | ||
) { | ||
|
||
// QBE do not support queries on Child aggregates yet | ||
if (property.isCollectionLike() || property.isMap()) { | ||
return; | ||
} | ||
|
||
PropertyPath currentPropertyPath = resolveCurrentPropertyPath(propertyPath, property); | ||
String currentPropertyDotPath = currentPropertyPath.toDotPath(); | ||
|
||
if (matcherAccessor.isIgnoredPath(currentPropertyDotPath)) { | ||
return; | ||
} | ||
|
||
Object actualPropertyValue = entityPropertiesAccessor.getProperty(property); | ||
|
||
if (property.isEmbedded() && actualPropertyValue != null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, we don't distinguish based on whether the property is an embedded one but rather whether it is an entity. At some point it should be possible to express criteria based across objects that are part of the aggregate (like it is done in JPA, we consider embeddables and references). |
||
processEmbeddedRecursively( // | ||
matcherAccessor, // | ||
actualPropertyValue, | ||
property, // | ||
criteriaBasedOnProperties, // | ||
currentPropertyPath // | ||
); | ||
} else { | ||
Optional<?> optionalConvertedPropValue = matcherAccessor // | ||
.getValueTransformerForPath(property.getName()) // | ||
.apply(Optional.ofNullable(propertyAccessor.getProperty(property))); | ||
.getValueTransformerForPath(currentPropertyDotPath) // | ||
.apply(Optional.ofNullable(actualPropertyValue)); | ||
|
||
// If the value is empty, don't try to match against it | ||
if (!optionalConvertedPropValue.isPresent()) { | ||
if (optionalConvertedPropValue.isEmpty()) { | ||
return; | ||
} | ||
|
||
Object convPropValue = optionalConvertedPropValue.get(); | ||
boolean ignoreCase = matcherAccessor.isIgnoreCaseForPath(property.getName()); | ||
boolean ignoreCase = matcherAccessor.isIgnoreCaseForPath(currentPropertyDotPath); | ||
|
||
String column = property.getName(); | ||
|
||
switch (matcherAccessor.getStringMatcherForPath(property.getName())) { | ||
switch (matcherAccessor.getStringMatcherForPath(currentPropertyDotPath)) { | ||
case DEFAULT: | ||
case EXACT: | ||
criteriaBasedOnProperties.add(includeNulls(example) // | ||
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) // | ||
? Criteria.where(column).isNull().or(column).is(convPropValue).ignoreCase(ignoreCase) | ||
: Criteria.where(column).is(convPropValue).ignoreCase(ignoreCase)); | ||
break; | ||
case ENDING: | ||
criteriaBasedOnProperties.add(includeNulls(example) // | ||
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) // | ||
? Criteria.where(column).isNull().or(column).like("%" + convPropValue).ignoreCase(ignoreCase) | ||
: Criteria.where(column).like("%" + convPropValue).ignoreCase(ignoreCase)); | ||
break; | ||
case STARTING: | ||
criteriaBasedOnProperties.add(includeNulls(example) // | ||
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) // | ||
? Criteria.where(column).isNull().or(column).like(convPropValue + "%").ignoreCase(ignoreCase) | ||
: Criteria.where(column).like(convPropValue + "%").ignoreCase(ignoreCase)); | ||
break; | ||
case CONTAINING: | ||
criteriaBasedOnProperties.add(includeNulls(example) // | ||
criteriaBasedOnProperties.add(includeNulls(matcherAccessor) // | ||
? Criteria.where(column).isNull().or(column).like("%" + convPropValue + "%").ignoreCase(ignoreCase) | ||
: Criteria.where(column).like("%" + convPropValue + "%").ignoreCase(ignoreCase)); | ||
break; | ||
default: | ||
throw new IllegalStateException(example.getMatcher().getDefaultStringMatcher() + " is not supported"); | ||
throw new IllegalStateException(matcherAccessor.getDefaultStringMatcher() + " is not supported"); | ||
} | ||
}); | ||
} | ||
|
||
// Criteria, assemble! | ||
Criteria criteria = Criteria.empty(); | ||
} | ||
|
||
for (Criteria propertyCriteria : criteriaBasedOnProperties) { | ||
/** | ||
* Processes an embedded entity's properties recursively. | ||
* | ||
* @param matcherAccessor the input matcher on the {@link Example#getProbe() original probe}. | ||
* @param value the actual embedded object. | ||
* @param property the embedded property. | ||
* @param criteriaBasedOnProperties collection of {@link Criteria} objects to potentially enrich. | ||
* @param currentPropertyPath the dot-separated path of the passed {@code property}. | ||
*/ | ||
private void processEmbeddedRecursively( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like a duplication of the general code that collects criteria for example matching. Should be consolidated. |
||
ExampleMatcherAccessor matcherAccessor, | ||
Object value, | ||
RelationalPersistentProperty property, | ||
List<Criteria> criteriaBasedOnProperties, | ||
PropertyPath currentPropertyPath | ||
) { | ||
RelationalPersistentEntity<?> embeddedPersistentEntity = mappingContext.getPersistentEntity(property.getTypeInformation()); | ||
|
||
if (example.getMatcher().isAllMatching()) { | ||
criteria = criteria.and(propertyCriteria); | ||
} else { | ||
criteria = criteria.or(propertyCriteria); | ||
} | ||
} | ||
PersistentPropertyAccessor<?> embeddedEntityPropertyAccessor = embeddedPersistentEntity.getPropertyAccessor(value); | ||
|
||
return Query.query(criteria); | ||
embeddedPersistentEntity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) embeddedProperty -> | ||
potentiallyEnrichCriteriaByProcessingProperty( | ||
currentPropertyPath, | ||
matcherAccessor, | ||
embeddedEntityPropertyAccessor, | ||
embeddedProperty, | ||
criteriaBasedOnProperties | ||
) | ||
); | ||
} | ||
|
||
@NonNull | ||
private static PropertyPath resolveCurrentPropertyPath(@Nullable PropertyPath propertyPath, RelationalPersistentProperty property) { | ||
PropertyPath currentPropertyPath; | ||
|
||
if (propertyPath == null) { | ||
currentPropertyPath = PropertyPath.from(property.getName(), property.getOwner().getTypeInformation()); | ||
} else { | ||
currentPropertyPath = propertyPath.nested(property.getName()); | ||
} | ||
return currentPropertyPath; | ||
} | ||
|
||
/** | ||
* Does this {@link Example} need to include {@literal NULL} values in its {@link Criteria}? | ||
* Does this {@link ExampleMatcherAccessor} need to include {@literal NULL} values in its {@link Criteria}? | ||
* | ||
* @param example | ||
* @return whether or not to include nulls. | ||
* @return whether to include nulls. | ||
*/ | ||
private static <T> boolean includeNulls(Example<T> example) { | ||
return example.getMatcher().getNullHandler() == NullHandler.INCLUDE; | ||
private static <T> boolean includeNulls(ExampleMatcherAccessor exampleMatcher) { | ||
return exampleMatcher.getNullHandler() == NullHandler.INCLUDE; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want to have Jetbrains annotations here.