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:
committed by
Christoph Strobl
parent
2be6a3b3f7
commit
a3f876f926
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user