-
Notifications
You must be signed in to change notification settings - Fork 373
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,9 +22,14 @@ | |
import java.util.List; | ||
import java.util.Optional; | ||
|
||
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; | ||
|
@@ -39,6 +44,7 @@ | |
* @since 2.2 | ||
* @author Greg Turnquist | ||
* @author Jens Schauder | ||
* @author Mikhail Polivakha | ||
*/ | ||
public class RelationalExampleMapper { | ||
|
||
|
@@ -64,92 +70,168 @@ 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) { | ||
|
||
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 = buildCriteria( // | ||
null, // | ||
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> List<Criteria> buildCriteria( // | ||
@Nullable PropertyPath propertyPath, // | ||
RelationalPersistentEntity<?> persistentEntity, // | ||
ExampleMatcherAccessor matcherAccessor, // | ||
PersistentPropertyAccessor<T> probePropertyAccessor // | ||
) { | ||
final List<Criteria> criteriaBasedOnProperties = new ArrayList<>(); | ||
|
||
for (RelationalPersistentProperty property : persistentEntity) { | ||
potentiallyEnrichCriteria( | ||
propertyPath, | ||
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 potentiallyEnrichCriteria( | ||
@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). |
||
RelationalPersistentEntity<?> embeddedPersistentEntity = mappingContext.getRequiredPersistentEntity(property.getTypeInformation()); | ||
|
||
criteriaBasedOnProperties.addAll( | ||
buildCriteria( | ||
currentPropertyPath, | ||
embeddedPersistentEntity, | ||
matcherAccessor, | ||
embeddedPersistentEntity.getPropertyAccessor(actualPropertyValue) | ||
) | ||
); | ||
} 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) { | ||
private static PropertyPath resolveCurrentPropertyPath(@Nullable PropertyPath propertyPath, RelationalPersistentProperty property) { | ||
PropertyPath currentPropertyPath; | ||
|
||
if (example.getMatcher().isAllMatching()) { | ||
criteria = criteria.and(propertyCriteria); | ||
} else { | ||
criteria = criteria.or(propertyCriteria); | ||
} | ||
if (propertyPath == null) { | ||
currentPropertyPath = PropertyPath.from(property.getName(), property.getOwner().getTypeInformation()); | ||
} else { | ||
currentPropertyPath = propertyPath.nested(property.getName()); | ||
} | ||
|
||
return Query.query(criteria); | ||
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.
What's the reason behind the renaming?
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.
Because one can confuse the
PersistenceEntity
with theentity
as the actual instance/probe. I do not insist here at all, just my observation when working with code, what do you think, @mp911de ?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.
I actually do like the rename and the idea behind it.