Skip to content

Commit

Permalink
Add support for Query by Example.
Browse files Browse the repository at this point in the history
Resolves #929.
  • Loading branch information
gregturn committed Mar 2, 2021
1 parent 802e4a7 commit 0058752
Show file tree
Hide file tree
Showing 2 changed files with 588 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.data.relational.repository.query;

import static org.springframework.data.domain.ExampleMatcher.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.data.domain.Example;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyHandler;
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;

/**
* Transform an {@link Example} into a {@link Query}.
*
* @since 2.2
* @author Greg Turnquist
*/
public class RelationalExampleMapper {

private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;

public RelationalExampleMapper(
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
}

/**
* Use the {@link Example} to extract a {@link Query}.
*
* @param example
* @return query
*/
public <T> Query getMappedExample(Example<T> example) {
return getMappedExample(example, mappingContext.getRequiredPersistentEntity(example.getProbeType()));
}

/**
* Transform each property of the {@link Example}'s probe into a {@link Criteria} and assemble them into a
* {@link Query}.
*
* @param example
* @param entity
* @return query
*/
private <T> Query getMappedExample(Example<T> example, RelationalPersistentEntity<?> entity) {

Assert.notNull(example, "Example must not be null!");
Assert.notNull(entity, "RelationalPersistentEntity must not be null!");

PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(example.getProbe());
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher());

final List<Criteria> criteriaBasedOnProperties = new ArrayList<>();

entity.doWithProperties((PropertyHandler<RelationalPersistentProperty>) property -> {

if (matcherAccessor.isIgnoredPath(property.getName())) {
return;
}

Optional<?> optionalConvertedPropValue = matcherAccessor //
.getValueTransformerForPath(property.getName()) //
.apply(Optional.ofNullable(propertyAccessor.getProperty(property)));

// If the value is empty, don't try to match against it
if (!optionalConvertedPropValue.isPresent()) {
return;
}

Object convPropValue = optionalConvertedPropValue.get();
boolean ignoreCase = matcherAccessor.isIgnoreCaseForPath(property.getName());

String column = property.getName();

switch (matcherAccessor.getStringMatcherForPath(property.getName())) {
case DEFAULT:
case EXACT:
criteriaBasedOnProperties.add(includeNulls(example) //
? Criteria.where(column).isNull().or(column).is(convPropValue).ignoreCase(ignoreCase)
: Criteria.where(column).is(convPropValue).ignoreCase(ignoreCase));
break;
case ENDING:
criteriaBasedOnProperties.add(includeNulls(example) //
? Criteria.where(column).isNull().or(column).like("%" + convPropValue).ignoreCase(ignoreCase)
: Criteria.where(column).like("%" + convPropValue).ignoreCase(ignoreCase));
break;
case STARTING:
criteriaBasedOnProperties.add(includeNulls(example) //
? Criteria.where(column).isNull().or(column).like(convPropValue + "%").ignoreCase(ignoreCase)
: Criteria.where(column).like(convPropValue + "%").ignoreCase(ignoreCase));
break;
case CONTAINING:
criteriaBasedOnProperties.add(includeNulls(example) //
? 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!");
}
});

// Criteria, assemble!
Criteria criteria = Criteria.empty();

for (Criteria propertyCriteria : criteriaBasedOnProperties) {

if (example.getMatcher().isAllMatching()) {
criteria = criteria.and(propertyCriteria);
} else {
criteria = criteria.or(propertyCriteria);
}
}

return Query.query(criteria);
}

/**
* Does this {@link Example} need to include {@literal NULL} values in its {@link Criteria}?
*
* @param example
* @return whether or not to include nulls.
*/
private static <T> boolean includeNulls(Example<T> example) {
return example.getMatcher().getNullHandler() == NullHandler.INCLUDE;
}
}
Loading

0 comments on commit 0058752

Please sign in to comment.