DATAREDIS-605 - Query by Example for Redis Repositories.

We now support query by example via Redis repositories. We allow case-sensitive, exact matching of singular simple and nested properties, using any/all match modes, value transformation of the criteria value for indexed properties.

We do not support findAll with sorting, case-insensitive properties or string matchers other than exact.

interface PersonRepository extends QueryByExampleExecutor<Person> {
}

class PersonService {

  @Autowired PersonRepository personRepository;

  List<Person> findPeople(Person probe) {
    return personRepository.findAll(Example.of(probe));
  }
}

Original Pull Request: #301
This commit is contained in:
Mark Paluch
2018-01-09 16:30:27 +01:00
committed by Christoph Strobl
parent 2be6a3b3f7
commit a3f876f926
15 changed files with 947 additions and 20 deletions

View File

@@ -4,6 +4,7 @@ Costin Leau, Jennifer Hickey, Christoph Strobl, Thomas Darimont, Mark Paluch
:revdate: {localdate}
:toc:
:toc-placement!:
:spring-data-commons-include: ../../../../spring-data-commons/src/main/asciidoc
:spring-data-commons-docs: https://raw.githubusercontent.com/spring-projects/spring-data-commons/master/src/main/asciidoc
(C) 2011-2016 The original authors.

View File

@@ -8,6 +8,7 @@ New and noteworthy in the latest releases.
* Unix domain socket connections using <<redis:connectors:lettuce,Lettuce>>.
* <<redis:write-to-master-read-from-slave, Write to Master read from Slave>> support using Lettuce.
* <<query-by-example,Query by Example>> integration
[[new-in-2.0.0]]
== New in Spring Data Redis 2.0

View File

@@ -0,0 +1,42 @@
[[query-by-example.execution]]
== Executing an example
.Query by Example using a Repository
====
[source, java]
----
interface PersonRepository extends QueryByExampleExecutor<Person> {
}
class PersonService {
@Autowired PersonRepository personRepository;
List<Person> findPeople(Person probe) {
return personRepository.findAll(Example.of(probe));
}
}
----
====
Redis Repositories support with their secondary indexes a subset of Spring Data's Query by Example features.
In particular, only exact, case-sensitive and non-null values are used to construct a query.
Secondary indexes use set-based operations (Set intersection, Set union) to determine matching keys. Adding a property to the query that is not indexed returns no result as no index exists. Query by Example support inspects indexing configuration to only include properties in the query that are covered by an index. This is to prevent accidental inclusion of not indexed properties.
Case-insensitive queries and unsupported ``StringMatcher``s are rejected at runtime.
*Supported Query by Example options*
* Case-sensitive, exact matching of simple and nested properties
* Any/All match modes
* Value transformation of the criteria value
* Exclusion of `null` values from the criteria
*Not supported by Query by Example*
* Case-insensitive matching
* Regex, prefix/contains/suffix String-matching
* Querying of Associations, Collection, and Map-like properties
* Inclusion of `null` values from the criteria
* `findAll` with sorting

View File

@@ -468,6 +468,8 @@ In the above example the lon/lat values are stored using `GEOADD` using the obje
NOTE: It is **not** possible to combine `near`/`within` with other criteria.
include::../{spring-data-commons-include}/query-by-example.adoc[leveloffset=+1]
include::query-by-example.adoc[leveloffset=+1]
[[redis.repositories.expirations]]
== Time To Live

View File

@@ -35,6 +35,8 @@ import org.springframework.util.ClassUtils;
*/
public class RedisKeyValueTemplate extends KeyValueTemplate {
private final RedisKeyValueAdapter adapter;
/**
* Create new {@link RedisKeyValueTemplate}.
*
@@ -43,6 +45,15 @@ public class RedisKeyValueTemplate extends KeyValueTemplate {
*/
public RedisKeyValueTemplate(RedisKeyValueAdapter adapter, RedisMappingContext mappingContext) {
super(adapter, mappingContext);
this.adapter = adapter;
}
/**
* @return the {@link RedisKeyValueAdapter}.
* @since 2.1
*/
public RedisKeyValueAdapter getAdapter() {
return adapter;
}
/*
@@ -54,6 +65,7 @@ public class RedisKeyValueTemplate extends KeyValueTemplate {
return (RedisMappingContext) super.getMappingContext();
}
/**
* Retrieve entities by resolving their {@literal id}s and converting them into required type. <br />
* The callback provides either a single {@literal id} or an {@link Iterable} of {@literal id}s, used for retrieving

View File

@@ -159,20 +159,23 @@ class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationC
@Override
public long count(RedisOperationChain criteria, String keyspace) {
if (criteria == null) {
if (criteria == null || criteria.isEmpty()) {
return this.getAdapter().count(keyspace);
}
return this.getAdapter().execute(connection -> {
String key = keyspace + ":";
byte[][] keys = new byte[criteria.getSismember().size()][];
int i = 0;
for (Object o : criteria.getSismember()) {
keys[i] = getAdapter().getConverter().getConversionService().convert(key + o, byte[].class);
long result = 0;
if (!criteria.getOrSismember().isEmpty()) {
result += connection.sUnion(keys(keyspace + ":", criteria.getOrSismember())).size();
}
return (long) connection.sInter(keys).size();
if (!criteria.getSismember().isEmpty()) {
result += connection.sInter(keys(keyspace + ":", criteria.getSismember())).size();
}
return result;
});
}

View File

@@ -18,17 +18,8 @@ package org.springframework.data.redis.core.convert;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -990,6 +981,16 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
return this.mappingContext;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.convert.RedisConverter#getIndexResolver()
*/
@Nullable
@Override
public IndexResolver getIndexResolver() {
return this.indexResolver;
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityConverter#getConversionService()

View File

@@ -19,11 +19,13 @@ import org.springframework.data.convert.EntityConverter;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
import org.springframework.data.redis.core.mapping.RedisPersistentProperty;
import org.springframework.lang.Nullable;
/**
* Redis specific {@link EntityConverter}.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.7
*/
public interface RedisConverter
@@ -35,4 +37,11 @@ public interface RedisConverter
*/
@Override
RedisMappingContext getMappingContext();
/**
* @return the configured {@link IndexResolver}, may be {@literal null}.
* @since 2.1
*/
@Nullable
IndexResolver getIndexResolver();
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2018 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
*
* http://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.redis.repository.query;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher.MatchMode;
import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.redis.core.convert.IndexResolver;
import org.springframework.data.redis.core.convert.IndexedData;
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
import org.springframework.data.redis.core.mapping.RedisPersistentProperty;
import org.springframework.data.support.ExampleMatcherAccessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Mapper for Query-by-Example examples to an actual query.
* <p/>
* This mapper creates a {@link RedisOperationChain} for a given {@link Example} considering exact matches,
* {@link PropertyValueTransformer value transformations} and {@link MatchMode} for indexed simple and nested type
* properties. {@link java.util.Map} and {@link java.util.Collection} properties are not considered.
* <p/>
* Example matching is limited to case-sensitive and exact matches only.
*
* @author Mark Paluch
* @since 2.1
*/
public class ExampleQueryMapper {
private final Set<StringMatcher> SUPPORTED_MATCHERS = EnumSet.of(StringMatcher.DEFAULT, StringMatcher.EXACT);
private final MappingContext<RedisPersistentEntity<?>, RedisPersistentProperty> mappingContext;
private final IndexResolver indexResolver;
/**
* Creates a new {@link ExampleQueryMapper} given {@link MappingContext} and {@link IndexResolver}.
*
* @param mappingContext must not be {@literal null}.
* @param indexResolver must not be {@literal null}.
*/
public ExampleQueryMapper(MappingContext<RedisPersistentEntity<?>, RedisPersistentProperty> mappingContext,
IndexResolver indexResolver) {
Assert.notNull(mappingContext, "MappingContext must not be null!");
Assert.notNull(indexResolver, "IndexResolver must not be null!");
this.mappingContext = mappingContext;
this.indexResolver = indexResolver;
}
/**
* Retrieve a mapped {@link RedisOperationChain} to query secondary indexes given {@link Example}.
*
* @param example must not be {@literal null}.
* @return the mapped {@link RedisOperationChain}.
*/
public RedisOperationChain getMappedExample(Example<?> example) {
RedisOperationChain chain = new RedisOperationChain();
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor(example.getMatcher());
applyPropertySpecs("", example.getProbe(), mappingContext.getRequiredPersistentEntity(example.getProbeType()),
matcherAccessor, example.getMatcher().getMatchMode(), chain);
return chain;
}
private void applyPropertySpecs(String path, @Nullable Object probe, RedisPersistentEntity<?> persistentEntity,
ExampleMatcherAccessor exampleSpecAccessor, MatchMode matchMode, RedisOperationChain chain) {
if (probe == null) {
return;
}
PersistentPropertyAccessor propertyAccessor = persistentEntity.getPropertyAccessor(probe);
Set<IndexedData> indexedData = getIndexedData(path, probe, persistentEntity);
Set<String> indexNames = indexedData.stream().map(IndexedData::getIndexName).distinct().collect(Collectors.toSet());
persistentEntity.forEach(property -> {
if (property.isIdProperty()) {
return;
}
String propertyPath = StringUtils.hasText(path) ? path + "." + property.getName() : property.getName();
if (exampleSpecAccessor.isIgnoredPath(propertyPath) || property.isCollectionLike() || property.isMap()) {
return;
}
applyPropertySpec(propertyPath, indexNames::contains, exampleSpecAccessor, propertyAccessor, property, matchMode,
chain);
});
}
private void applyPropertySpec(String path, Predicate<String> hasIndex, ExampleMatcherAccessor exampleSpecAccessor,
PersistentPropertyAccessor propertyAccessor, RedisPersistentProperty property, MatchMode matchMode,
RedisOperationChain chain) {
StringMatcher stringMatcher = exampleSpecAccessor.getDefaultStringMatcher();
boolean ignoreCase = exampleSpecAccessor.isIgnoreCaseEnabled();
Object value = propertyAccessor.getProperty(property);
if (exampleSpecAccessor.hasPropertySpecifiers()) {
stringMatcher = exampleSpecAccessor.getStringMatcherForPath(path);
ignoreCase = exampleSpecAccessor.isIgnoreCaseForPath(path);
}
if (ignoreCase) {
throw new InvalidDataAccessApiUsageException("Redis Query-by-Example supports only case-sensitive matching.");
}
if (!SUPPORTED_MATCHERS.contains(stringMatcher)) {
throw new InvalidDataAccessApiUsageException(
String.format("Redis Query-by-Example does not support string matcher %s. Supported matchers are: %s.",
stringMatcher, SUPPORTED_MATCHERS));
}
if (exampleSpecAccessor.hasPropertySpecifier(path)) {
PropertyValueTransformer valueTransformer = exampleSpecAccessor.getValueTransformerForPath(path);
value = valueTransformer.apply(Optional.ofNullable(value)).orElse(null);
}
if (value == null) {
return;
}
if (property.isEntity()) {
applyPropertySpecs(path, value, mappingContext.getRequiredPersistentEntity(property), exampleSpecAccessor,
matchMode, chain);
} else {
if (matchMode == MatchMode.ALL) {
if (hasIndex.test(path)) {
chain.sismember(path, value);
}
} else {
chain.orSismember(path, value);
}
}
}
private Set<IndexedData> getIndexedData(String path, Object probe, RedisPersistentEntity<?> persistentEntity) {
String keySpace = persistentEntity.getKeySpace();
return keySpace == null ? Collections.emptySet()
: indexResolver.resolveIndexesFor(persistentEntity.getKeySpace(), path, persistentEntity.getTypeInformation(),
probe);
}
}

View File

@@ -33,6 +33,7 @@ import org.springframework.util.Assert;
* Simple set of operations required to run queries against Redis.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.7
*/
public class RedisOperationChain {
@@ -42,6 +43,10 @@ public class RedisOperationChain {
private @Nullable NearPath near;
public boolean isEmpty() {
return near == null && sismember.isEmpty() && orSismember.isEmpty();
}
public void sismember(String path, Object value) {
sismember(new PathAndValue(path, value));
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright 2018 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
*
* http://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.redis.repository.support;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.data.redis.core.RedisKeyValueTemplate;
import org.springframework.data.redis.core.convert.IndexResolver;
import org.springframework.data.redis.core.convert.PathIndexResolver;
import org.springframework.data.redis.core.convert.RedisConverter;
import org.springframework.data.redis.repository.query.ExampleQueryMapper;
import org.springframework.data.redis.repository.query.RedisOperationChain;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.util.Assert;
/**
* Repository fragment implementing Redis {@link QueryByExampleExecutor Query-by-Example} operations.
* <p/>
* This executor uses {@link ExampleQueryMapper} to map {@link Example}s into {@link KeyValueQuery} to execute its query
* methods.
*
* @author Mark Paluch
* @since 2.1
*/
@SuppressWarnings("unchecked")
public class QueryByExampleRedisExecutor<T> implements QueryByExampleExecutor<T> {
private final EntityInformation<T, ?> entityInformation;
private final RedisKeyValueTemplate keyValueTemplate;
private final ExampleQueryMapper mapper;
/**
* Create a new {@link QueryByExampleRedisExecutor} given {@link EntityInformation} and {@link RedisKeyValueTemplate}.
* This constructor uses the configured {@link IndexResolver} from the converter.
*
* @param entityInformation must not be {@literal null}.
* @param keyValueTemplate must not be {@literal null}.
*/
public QueryByExampleRedisExecutor(EntityInformation<T, ?> entityInformation,
RedisKeyValueTemplate keyValueTemplate) {
Assert.notNull(entityInformation, "EntityInformation must not be null!");
Assert.notNull(keyValueTemplate, "RedisKeyValueTemplate must not be null!");
this.entityInformation = entityInformation;
this.keyValueTemplate = keyValueTemplate;
RedisConverter converter = keyValueTemplate.getAdapter().getConverter();
IndexResolver indexResolver = converter.getIndexResolver();
this.mapper = new ExampleQueryMapper(keyValueTemplate.getMappingContext(),
indexResolver != null ? indexResolver : new PathIndexResolver(converter.getMappingContext()));
}
/**
* Create a new {@link QueryByExampleRedisExecutor} given {@link EntityInformation} and {@link RedisKeyValueTemplate}.
*
* @param entityInformation must not be {@literal null}.
* @param keyValueTemplate must not be {@literal null}.
*/
public QueryByExampleRedisExecutor(EntityInformation<T, ?> entityInformation, RedisKeyValueTemplate keyValueTemplate,
IndexResolver indexResolver) {
Assert.notNull(entityInformation, "EntityInformation must not be null!");
Assert.notNull(keyValueTemplate, "RedisKeyValueTemplate must not be null!");
Assert.notNull(indexResolver, "IndexResolver must not be null!");
this.entityInformation = entityInformation;
this.keyValueTemplate = keyValueTemplate;
this.mapper = new ExampleQueryMapper(keyValueTemplate.getMappingContext(),
new PathIndexResolver(keyValueTemplate.getMappingContext()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findOne(org.springframework.data.domain.Example)
*/
@Override
public <S extends T> Optional<S> findOne(Example<S> example) {
RedisOperationChain operationChain = getQuery(example);
KeyValueQuery<RedisOperationChain> query = new KeyValueQuery<>(operationChain);
Iterable<T> result = keyValueTemplate.find(query.limit(1), entityInformation.getJavaType());
Iterator<T> iterator = result.iterator();
return iterator.hasNext() ? Optional.of((S) iterator.next()) : Optional.empty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
*/
@Override
public <S extends T> Iterable<S> findAll(Example<S> example) {
RedisOperationChain operationChain = getQuery(example);
return (Iterable<S>) keyValueTemplate.find(new KeyValueQuery<>(operationChain), entityInformation.getJavaType());
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
*/
@Override
public <S extends T> Iterable<S> findAll(Example<S> example, Sort sort) {
throw new UnsupportedOperationException("Ordering is not supported");
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable)
*/
@Override
public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
Assert.notNull(pageable, "Pageable must not be null!");
RedisOperationChain operationChain = getQuery(example);
KeyValueQuery<RedisOperationChain> query = new KeyValueQuery<>(operationChain);
Iterable<T> result = keyValueTemplate.find(
query.orderBy(pageable.getSort()).skip(pageable.getOffset()).limit(pageable.getPageSize()),
entityInformation.getJavaType());
long count = operationChain.isEmpty() ? keyValueTemplate.count(entityInformation.getJavaType())
: keyValueTemplate.count(query, entityInformation.getJavaType());
List<S> list = new ArrayList<>();
for (T t : result) {
list.add((S) t);
}
return new PageImpl<>(list, pageable, count);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#count(org.springframework.data.domain.Example)
*/
@Override
public <S extends T> long count(Example<S> example) {
RedisOperationChain operationChain = getQuery(example);
return keyValueTemplate.count(new KeyValueQuery<>(operationChain), entityInformation.getJavaType());
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryByExampleExecutor#exists(org.springframework.data.domain.Example)
*/
@Override
public <S extends T> boolean exists(Example<S> example) {
return count(example) > 0;
}
private <S extends T> RedisOperationChain getQuery(Example<S> example) {
Assert.notNull(example, "Example must not be null!");
return mapper.getMappedExample(example);
}
}

View File

@@ -18,11 +18,16 @@ package org.springframework.data.redis.repository.support;
import org.springframework.data.keyvalue.core.KeyValueOperations;
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
import org.springframework.data.redis.repository.core.MappingRedisEntityInformation;
import org.springframework.data.redis.repository.query.RedisQueryCreator;
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.query.QueryByExampleExecutor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -70,6 +75,28 @@ public class RedisRepositoryFactory extends KeyValueRepositoryFactory {
this.operations = keyValueOperations;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryFragments(org.springframework.data.repository.core.RepositoryMetadata)
*/
@Override
protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) {
RepositoryFragments fragments = RepositoryFragments.empty();
if (QueryByExampleExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) {
RedisMappingContext mappingContext = (RedisMappingContext) this.operations.getMappingContext();
RedisPersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType());
MappingRedisEntityInformation<?, ?> entityInformation = new MappingRedisEntityInformation<>(persistentEntity);
fragments = fragments.append(RepositoryFragment.implemented(QueryByExampleExecutor.class,
getTargetRepositoryViaReflection(QueryByExampleRedisExecutor.class, entityInformation, operations)));
}
return fragments;
}
/*
* (non-Javadoc)
* @see org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory#getEntityInformation(java.lang.Class)

View File

@@ -18,19 +18,20 @@ package org.springframework.data.redis.repository;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.io.Serializable;
import lombok.Data;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.Data;
import org.hamcrest.core.IsNull;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@@ -47,6 +48,7 @@ import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
/**
* Base for testing Redis repository support in different configurations.
@@ -288,6 +290,22 @@ public abstract class RedisRepositoryIntegrationTestBase {
}
}
@Test // DATAREDIS-605
public void shouldFindByExample() {
Person eddard = new Person("eddard", "stark");
Person tyrion = new Person("tyrion", "lannister");
Person robb = new Person("robb", "stark");
Person jon = new Person("jon", "snow");
Person arya = new Person("arya", "stark");
repo.saveAll(Arrays.asList(eddard, tyrion, robb, jon, arya));
List<Person> result = repo.findAll(Example.of(new Person(null, "stark")));
assertThat(result, hasSize(3));
}
@Test // DATAREDIS-533
public void nearQueryShouldReturnResultsCorrectly() {
@@ -347,7 +365,8 @@ public abstract class RedisRepositoryIntegrationTestBase {
assertThat(result, not(hasItems(p1)));
}
public static interface PersonRepository extends PagingAndSortingRepository<Person, String> {
public static interface PersonRepository
extends PagingAndSortingRepository<Person, String>, QueryByExampleExecutor<Person> {
List<Person> findByFirstname(String firstname);
@@ -368,6 +387,9 @@ public abstract class RedisRepositoryIntegrationTestBase {
Page<Person> findBy(Pageable page);
List<Person> findByHometownLocationNear(Point point, Distance distance);
@Override
<S extends Person> List<S> findAll(Example<S> example);
}
public static interface CityRepository extends CrudRepository<City, String> {

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2018 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
*
* http://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.redis.repository.query;
import static org.assertj.core.api.Assertions.*;
import lombok.Data;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
import org.springframework.data.redis.core.convert.PathIndexResolver;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.redis.repository.query.RedisOperationChain.PathAndValue;
/**
* Unit tests for {@link ExampleQueryMapper}.
*
* @author Mark Paluch
*/
public class ExampleQueryMapperUnitTests {
RedisMappingContext mappingContext = new RedisMappingContext();
ExampleQueryMapper mapper = new ExampleQueryMapper(mappingContext, new PathIndexResolver(mappingContext));
@Test // DATAREDIS-605
public void shouldRejectCaseInsensitiveMatching() {
assertThatThrownBy(() -> {
mapper.getMappedExample(Example.of(new Person(), ExampleMatcher.matching().withIgnoreCase()));
}).isInstanceOf(InvalidDataAccessApiUsageException.class);
}
@Test // DATAREDIS-605
public void shouldRejectUnsupportedStringMatchers() {
List<StringMatcher> unsupported = Arrays.asList(StringMatcher.STARTING, StringMatcher.REGEX,
StringMatcher.CONTAINING, StringMatcher.ENDING);
for (StringMatcher stringMatcher : unsupported) {
assertThatThrownBy(() -> {
mapper.getMappedExample(
Example.of(new Person(), ExampleMatcher.matching().withStringMatcher(StringMatcher.STARTING)));
}) //
.hasMessageContaining("does not support") //
.describedAs("Unsupported matcher " + stringMatcher) //
.isInstanceOf(InvalidDataAccessApiUsageException.class);
}
}
@Test // DATAREDIS-605
public void shouldMapSimpleExample() {
Person person = new Person();
person.setFirstname("Walter");
person.setGender(Gender.MALE);
person.setAge(50);
RedisOperationChain operationChain = mapper.getMappedExample(Example.of(person));
assertThat(operationChain.getOrSismember()).isEmpty();
assertThat(operationChain.getSismember()).contains(new PathAndValue("firstname", "Walter"),
new PathAndValue("gender", Gender.MALE), new PathAndValue("age", 50));
}
@Test // DATAREDIS-605
public void shouldIgnoreFieldsWithoutIndexWithAllMatch() {
Person person = new Person();
person.setLastname("Foo");
RedisOperationChain operationChain = mapper.getMappedExample(Example.of(person));
assertThat(operationChain.getOrSismember()).isEmpty();
assertThat(operationChain.getSismember()).isEmpty();
}
@Test // DATAREDIS-605
public void shouldIncludeFieldsWithoutIndexWithAnyMatch() {
Person person = new Person();
person.setLastname("Foo");
RedisOperationChain operationChain = mapper.getMappedExample(Example.of(person, ExampleMatcher.matchingAny()));
assertThat(operationChain.getOrSismember()).containsOnly(new PathAndValue("lastname", "Foo"));
assertThat(operationChain.getSismember()).isEmpty();
}
@Test // DATAREDIS-605
public void shouldIgnorePaths() {
Person person = new Person();
person.setFirstname("Walter");
person.setGender(Gender.MALE);
person.setAge(50);
RedisOperationChain operationChain = mapper
.getMappedExample(Example.of(person, ExampleMatcher.matching().withIgnorePaths("gender", "age")));
assertThat(operationChain.getOrSismember()).isEmpty();
assertThat(operationChain.getSismember()).containsOnly(new PathAndValue("firstname", "Walter"));
}
@Test // DATAREDIS-605
public void shouldMapNestedExample() {
Person person = new Person();
Species species = new Species();
species.name = "Homo Coquus Caeruleus Methiticus";
person.setSpecies(species);
RedisOperationChain operationChain = mapper.getMappedExample(Example.of(person));
assertThat(operationChain.getOrSismember()).isEmpty();
assertThat(operationChain.getSismember())
.containsOnly(new PathAndValue("species.name", "Homo Coquus Caeruleus Methiticus"));
}
@Test // DATAREDIS-605
public void shouldIgnoreMapsAndCollections() {
Person person = new Person();
person.setNicknames(Arrays.asList("Heisenberg"));
person.setPhysicalAttributes(Collections.singletonMap("healthy", "no"));
RedisOperationChain operationChain = mapper.getMappedExample(Example.of(person));
assertThat(operationChain.getOrSismember()).isEmpty();
assertThat(operationChain.getSismember()).isEmpty();
}
@Test // DATAREDIS-605
public void shouldMapMatchingAny() {
Person person = new Person();
person.setFirstname("Walter");
person.setGender(Gender.MALE);
person.setAge(50);
RedisOperationChain operationChain = mapper.getMappedExample(Example.of(person, ExampleMatcher.matchingAny()));
assertThat(operationChain.getSismember()).isEmpty();
assertThat(operationChain.getOrSismember()).contains(new PathAndValue("firstname", "Walter"),
new PathAndValue("gender", Gender.MALE), new PathAndValue("age", 50));
}
@Test // DATAREDIS-605
public void shouldApplyPropertyTransformation() {
Person person = new Person();
person.setFirstname("Walter");
Example<Person> example = Example.of(person,
ExampleMatcher.matching().withTransformer("firstname", v -> v.map(s -> s.toString().toUpperCase())));
RedisOperationChain operationChain = mapper.getMappedExample(example);
assertThat(operationChain.getSismember()).contains(new PathAndValue("firstname", "WALTER"));
}
@Data
public static class Person {
@Id String id;
@Indexed String firstname;
String lastname;
@Indexed Gender gender;
List<String> nicknames;
@Indexed Integer age;
Map<String, String> physicalAttributes;
@Reference Person relative;
Species species;
}
public enum Gender {
MALE, FEMALE {
@Override
public String toString() {
return "Superwoman";
}
}
}
public static class Species {
@Indexed String name;
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright 2018 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
*
* http://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.redis.repository.support;
import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.Optional;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.RedisKeyValueAdapter;
import org.springframework.data.redis.core.RedisKeyValueTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
import org.springframework.data.redis.repository.core.MappingRedisEntityInformation;
/**
* Integration tests for {@link QueryByExampleRedisExecutor}.
*
* @author Mark Paluch
*/
public class QueryByExampleRedisExecutorTests {
JedisConnectionFactory connectionFactory;
RedisMappingContext mappingContext = new RedisMappingContext();
RedisKeyValueTemplate kvTemplate;
Person walt, hank, gus;
@Before
public void before() {
connectionFactory = new JedisConnectionFactory();
connectionFactory.afterPropertiesSet();
RedisTemplate<byte[], byte[]> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
kvTemplate = new RedisKeyValueTemplate(new RedisKeyValueAdapter(template, mappingContext), mappingContext);
SimpleKeyValueRepository<Person, String> repository = new SimpleKeyValueRepository<>(
getEntityInformation(Person.class), new KeyValueTemplate(new RedisKeyValueAdapter(template)));
repository.deleteAll();
walt = new Person("Walter", "White");
walt.setHometown(new City("Albuquerqe"));
hank = new Person("Hank", "Schrader");
hank.setHometown(new City("Albuquerqe"));
gus = new Person("Gus", "Fring");
gus.setHometown(new City("Albuquerqe"));
repository.saveAll(Arrays.asList(walt, hank, gus));
}
@After
public void tearDown() {
connectionFactory.destroy();
}
@Test // DATAREDIS-605
public void shouldFindOneByExample() {
QueryByExampleRedisExecutor<Person> executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class),
kvTemplate);
Optional<Person> result = executor.findOne(Example.of(walt));
assertThat(result).contains(walt);
}
@Test // DATAREDIS-605
public void shouldNotFindOneByExample() {
QueryByExampleRedisExecutor<Person> executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class),
kvTemplate);
Optional<Person> result = executor.findOne(Example.of(new Person("Skyler", "White")));
assertThat(result).isEmpty();
}
@Test // DATAREDIS-605
public void shouldFindAllByExample() {
QueryByExampleRedisExecutor<Person> executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class),
kvTemplate);
Person person = new Person();
person.setHometown(walt.getHometown());
Iterable<Person> result = executor.findAll(Example.of(person));
assertThat(result).contains(walt, gus, hank);
}
@Test // DATAREDIS-605
public void shouldNotSupportFindAllOrdered() {
QueryByExampleRedisExecutor<Person> executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class),
kvTemplate);
Person person = new Person();
person.setHometown(walt.getHometown());
assertThatThrownBy(() -> executor.findAll(Example.of(person), Sort.by("foo")))
.isInstanceOf(UnsupportedOperationException.class);
}
@Test // DATAREDIS-605
public void shouldFindAllPagedByExample() {
QueryByExampleRedisExecutor<Person> executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class),
kvTemplate);
Person person = new Person();
person.setHometown(walt.getHometown());
Page<Person> result = executor.findAll(Example.of(person), PageRequest.of(0, 2));
assertThat(result).hasSize(2);
assertThat(result.getTotalElements()).isEqualTo(3);
}
@Test // DATAREDIS-605
public void shouldCountCorrectly() {
QueryByExampleRedisExecutor<Person> executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class),
kvTemplate);
Person person = new Person();
person.setHometown(walt.getHometown());
assertThat(executor.count(Example.of(person))).isEqualTo(3);
assertThat(executor.count(Example.of(walt))).isEqualTo(1);
assertThat(executor.count(Example.of(new Person()))).isEqualTo(3);
assertThat(executor.count(Example.of(new Person("Foo", "Bar")))).isZero();
}
@Test // DATAREDIS-605
public void shouldReportExistenceCorrectly() {
QueryByExampleRedisExecutor<Person> executor = new QueryByExampleRedisExecutor<>(getEntityInformation(Person.class),
kvTemplate);
Person person = new Person();
person.setHometown(walt.getHometown());
assertThat(executor.exists(Example.of(person))).isTrue();
assertThat(executor.exists(Example.of(walt))).isTrue();
assertThat(executor.exists(Example.of(new Person()))).isTrue();
assertThat(executor.exists(Example.of(new Person("Foo", "Bar")))).isFalse();
}
@SuppressWarnings("unchecked")
private <T> MappingRedisEntityInformation<T, String> getEntityInformation(Class<T> entityClass) {
return new MappingRedisEntityInformation<>(
(RedisPersistentEntity) mappingContext.getRequiredPersistentEntity(entityClass));
}
@RedisHash("persons")
@Data
static class Person {
@Id String id;
@Indexed String firstname;
String lastname;
City hometown;
public Person() {}
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class City {
@Indexed String name;
}
}