Skip to content

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,11 @@ public boolean isOrdered() {

@Override
public boolean isEmbedded() {
return isEmbedded || (isIdProperty() && isEntity());
return isEmbedded || isCompositeId();
}

private boolean isCompositeId() {
return isIdProperty() && isEntity();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -64,92 +72,192 @@ 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 = 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( //
RelationalPersistentEntity<?> persistentEntity, //
ExampleMatcherAccessor matcherAccessor, //
PersistentPropertyAccessor<T> probePropertyAccessor //
) {
final List<Criteria> criteriaBasedOnProperties = new ArrayList<>();

persistentEntity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) property -> {
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;
}

if (property.isEmbedded()) {
processEmbeddedRecursively( //
matcherAccessor, //
entityPropertiesAccessor.getProperty(property),
property, //
criteriaBasedOnProperties, //
currentPropertyPath //
);
} else {
Optional<?> optionalConvertedPropValue = matcherAccessor //
.getValueTransformerForPath(property.getName()) //
.apply(Optional.ofNullable(propertyAccessor.getProperty(property)));
.getValueTransformerForPath(currentPropertyDotPath) //
.apply(Optional.ofNullable(entityPropertiesAccessor.getProperty(property)));

// 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(
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;
}
}
Loading