DATAREDIS-425 - Preserve item order in list, update documentation.

We now preserve the item order when converting list elements. This does not mean that we place elements at the exact position retrieved from the store but rather maintain their order based on the index value.

Additionally applied some documentation polishing and cluster tests.

Original Pull Request: #156
This commit is contained in:
Christoph Strobl
2016-03-11 09:43:26 +01:00
parent 9eb309c495
commit 8f129b16f9
6 changed files with 627 additions and 357 deletions

View File

@@ -383,8 +383,8 @@ public class ApplicationConfig {
public static class MyIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<RedisIndexSetting> initialConfiguration() {
return Collections.singleton(new RedisIndexSetting("persons", "firstname"));
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.singleton(new SimpleIndexDefinition("persons", "firstname"));
}
}
}
@@ -411,8 +411,8 @@ public class ApplicationConfig {
public static class MyIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<RedisIndexSetting> initialConfiguration() {
return Collections.singleton(new RedisIndexSetting("persons", "firstname"));
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.singleton(new SimpleIndexDefinition("persons", "firstname"));
}
}
}
@@ -537,7 +537,38 @@ Here's an overview of the keywords supported for Redis and what a method contain
|===============
====
[[redis.misc.cdi-integration]]
[[redis.repositories.cluster]]
== Redis Repositories running on Cluster
Using the Redis repository support in a clustered Redis environment is fine. Please see the <<cluster, Redis Cluster>> section for `ConnectionFactory` configuration details.
Still some considerations have to be done as the default key distribution will spread entities and secondary indexes through out the whole cluster and its slots.
[options = "header, autowidth"]
|===============
|key|type|slot|node
|persons:e2c7dcee-b8cd-4424-883e-736ce564363e|id for hash|15171|127.0.0.1:7381
|persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56|id for hash|7373|127.0.0.1:7380
|persons:firstname:rand|index|1700|127.0.0.1:7379
|
|===============
====
Some commands like `SINTER` and `SUNION` can only be processed on the Server side when all involved keys map to the same slot. Otherwise computation has to be done on client side.
Therefore it be useful to pin keyspaces to a single slot which allows to make use of Redis serverside computation right away.
[options = "header, autowidth"]
|===============
|key|type|slot|node
|{persons}:e2c7dcee-b8cd-4424-883e-736ce564363e|id for hash|2399|127.0.0.1:7379
|{persons}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56|id for hash|2399|127.0.0.1:7379
|{persons}:firstname:rand|index|2399|127.0.0.1:7379
|
|===============
====
TIP: Define and pin keyspaces via `@RedisHash("{yourkeyspace}") to specific slots when using Redis cluster.
[[redis.repositories.cdi-integration]]
== CDI integration
Instances of the repository interfaces are usually created by a container, which Spring is the most natural choice when working with Spring Data. There's sophisticated support to easily set up Spring to create bean instances. Spring Data Redis ships with a custom CDI extension that allows using the repository abstraction in CDI environments. The extension is part of the JAR so all you need to do to activate it is dropping the Spring Data Redis JAR into your classpath.

View File

@@ -15,8 +15,11 @@
*/
package org.springframework.data.redis.core.convert;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@@ -53,6 +56,7 @@ import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.comparator.NullSafeComparator;
/**
* {@link RedisConverter} implementation creating flat binary map structure out of a given domain type. Considers
@@ -103,6 +107,9 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
private final GenericConversionService conversionService;
private final EntityInstantiators entityInstantiators;
private final TypeMapper<RedisData> typeMapper;
private final Comparator<String> listKeyComparator = new NullSafeComparator<String>(
NaturalOrderingKeyComparator.INSTANCE, true);
private ReferenceResolver referenceResolver;
private IndexResolver indexResolver;
private CustomConversions customConversions;
@@ -533,11 +540,15 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
Bucket partial = source.getBucket().extract(path + ".[");
List<String> keys = new ArrayList<String>(partial.keySet());
keys.sort(listKeyComparator);
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, partial.size());
for (byte[] value : partial.values()) {
target.add(fromBytes(value, valueType));
for (String key : keys) {
target.add(fromBytes(partial.get(key), valueType));
}
return target;
}
@@ -551,7 +562,8 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
private Collection<?> readCollectionOfComplexTypes(String path, Class<?> collectionType, Class<?> valueType,
Bucket source) {
Set<String> keys = source.extractAllKeysFor(path);
List<String> keys = new ArrayList<String>(source.extractAllKeysFor(path));
keys.sort(listKeyComparator);
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, keys.size());
@@ -810,4 +822,90 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean {
}
}
private enum NaturalOrderingKeyComparator implements Comparator<String> {
INSTANCE;
/*
* (non-Javadoc)
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(String s1, String s2) {
int s1offset = 0;
int s2offset = 0;
while (s1offset < s1.length() && s2offset < s2.length()) {
Part thisPart = extractPart(s1, s1offset);
Part thatPart = extractPart(s2, s2offset);
int result = thisPart.compareTo(thatPart);
if (result != 0) {
return result;
}
s1offset += thisPart.length();
s2offset += thatPart.length();
}
return 0;
}
private Part extractPart(String source, int offset) {
StringBuilder builder = new StringBuilder();
char c = source.charAt(offset);
builder.append(c);
boolean isDigit = Character.isDigit(c);
for (int i = offset + 1; i < source.length(); i++) {
c = source.charAt(i);
if ((isDigit && !Character.isDigit(c)) || (!isDigit && Character.isDigit(c))) {
break;
}
builder.append(c);
}
return new Part(builder.toString(), isDigit);
}
private static class Part implements Comparable<Part> {
private final String rawValue;
private final Long longValue;
Part(String value, boolean isDigit) {
this.rawValue = value;
this.longValue = isDigit ? Long.valueOf(value) : null;
}
boolean isNumeric() {
return longValue != null;
}
int length() {
return rawValue.length();
}
/*
* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(Part that) {
if (this.isNumeric() && that.isNumeric()) {
return this.longValue.compareTo(that.longValue);
}
return this.rawValue.compareTo(that.rawValue);
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2016 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.
@@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.core.convert;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.hamcrest.core.IsInstanceOf.*;
@@ -308,7 +309,22 @@ public class MappingRedisConverterUnitTests {
map.put("nicknames.[1]", "lews therin");
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
assertThat(converter.read(Person.class, rdo).nicknames, hasItems("dragon reborn", "lews therin"));
assertThat(converter.read(Person.class, rdo).nicknames, contains("dragon reborn", "lews therin"));
}
/**
* @see DATAREDIS-425
*/
@Test
public void readConvertsUnorderedListOfSimplePropertiesCorrectly() {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("nicknames.[9]", "car'a'carn");
map.put("nicknames.[10]", "lews therin");
map.put("nicknames.[1]", "dragon reborn");
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
assertThat(converter.read(Person.class, rdo).nicknames, contains("dragon reborn", "car'a'carn", "lews therin"));
}
/**
@@ -338,6 +354,7 @@ public class MappingRedisConverterUnitTests {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("coworkers.[0].firstname", "mat");
map.put("coworkers.[0].nicknames.[0]", "prince of the ravens");
map.put("coworkers.[0].nicknames.[1]", "gambler");
map.put("coworkers.[1].firstname", "perrin");
map.put("coworkers.[1].address.city", "two rivers");
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
@@ -348,6 +365,34 @@ public class MappingRedisConverterUnitTests {
assertThat(target.coworkers.get(0).firstname, is("mat"));
assertThat(target.coworkers.get(0).nicknames, notNullValue());
assertThat(target.coworkers.get(0).nicknames.get(0), is("prince of the ravens"));
assertThat(target.coworkers.get(0).nicknames.get(1), is("gambler"));
assertThat(target.coworkers.get(1).firstname, is("perrin"));
assertThat(target.coworkers.get(1).address.city, is("two rivers"));
}
/**
* @see DATAREDIS-425
*/
@Test
public void readUnorderedListOfComplexPropertyCorrectly() {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("coworkers.[10].firstname", "perrin");
map.put("coworkers.[10].address.city", "two rivers");
map.put("coworkers.[1].firstname", "mat");
map.put("coworkers.[1].nicknames.[1]", "gambler");
map.put("coworkers.[1].nicknames.[0]", "prince of the ravens");
RedisData rdo = new RedisData(Bucket.newBucketFromStringMap(map));
Person target = converter.read(Person.class, rdo);
assertThat(target.coworkers, notNullValue());
assertThat(target.coworkers.get(0).firstname, is("mat"));
assertThat(target.coworkers.get(0).nicknames, notNullValue());
assertThat(target.coworkers.get(0).nicknames.get(0), is("prince of the ravens"));
assertThat(target.coworkers.get(0).nicknames.get(1), is("gambler"));
assertThat(target.coworkers.get(1).firstname, is("perrin"));
assertThat(target.coworkers.get(1).address.city, is("two rivers"));

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2016 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;
import static org.springframework.data.redis.connection.ClusterTestVariables.*;
import java.util.Arrays;
import java.util.List;
import org.junit.ClassRule;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.test.util.RedisClusterRule;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class RedisRepositoryClusterIntegrationTests extends RedisRepositoryIntegrationTestBase {
static final List<String> CLUSTER_NODES = Arrays.asList(CLUSTER_NODE_1.asString(), CLUSTER_NODE_2.asString(),
CLUSTER_NODE_3.asString());
/**
* ONLY RUN WHEN CLUSTER AVAILABLE
*/
public static @ClassRule RedisClusterRule clusterRule = new RedisClusterRule();
@Configuration
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
keyspaceConfiguration = MyKeyspaceConfiguration.class,
includeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*PersonRepository") })
static class Config {
@Bean
RedisTemplate<?, ?> redisTemplate() {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(CLUSTER_NODES);
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(clusterConfig);
connectionFactory.afterPropertiesSet();
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(connectionFactory);
return template;
}
}
}

View File

@@ -0,0 +1,369 @@
/*
* Copyright 2016 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;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.junit.Assert.*;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
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.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.core.index.IndexConfiguration;
import org.springframework.data.redis.core.index.IndexDefinition;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
import org.springframework.data.repository.CrudRepository;
/**
* Base for testing Redis repository support in different configurations.
*
* @author Christoph Strobl
*/
public abstract class RedisRepositoryIntegrationTestBase {
@Autowired PersonRepository repo;
@Autowired KeyValueTemplate kvTemplate;
@Before
public void setUp() {
// flush keyspaces
kvTemplate.delete(Person.class);
kvTemplate.delete(City.class);
}
/**
* @see DATAREDIS-425
*/
@Test
public void simpleFindShouldReturnEntitiesCorrectly() {
Person rand = new Person();
rand.firstname = "rand";
rand.lastname = "al'thor";
Person egwene = new Person();
egwene.firstname = "egwene";
repo.save(Arrays.asList(rand, egwene));
assertThat(repo.count(), is(2L));
assertThat(repo.findOne(rand.id), is(rand));
assertThat(repo.findOne(egwene.id), is(egwene));
assertThat(repo.findByFirstname("rand").size(), is(1));
assertThat(repo.findByFirstname("rand"), hasItem(rand));
assertThat(repo.findByFirstname("egwene").size(), is(1));
assertThat(repo.findByFirstname("egwene"), hasItem(egwene));
assertThat(repo.findByLastname("al'thor"), hasItem(rand));
}
/**
* @see DATAREDIS-425
*/
@Test
public void simpleFindByMultipleProperties() {
Person egwene = new Person();
egwene.firstname = "egwene";
egwene.lastname = "al'vere";
Person marin = new Person();
marin.firstname = "marin";
marin.lastname = "al'vere";
repo.save(Arrays.asList(egwene, marin));
assertThat(repo.findByLastname("al'vere").size(), is(2));
assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").size(), is(1));
assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").get(0), is(egwene));
}
/**
* @see DATAREDIS-425
*/
@Test
public void findReturnsReferenceDataCorrectly() {
// Prepare referenced data entry
City tarValon = new City();
tarValon.id = "1";
tarValon.name = "tar valon";
kvTemplate.insert(tarValon);
// Prepare domain entity
Person moiraine = new Person();
moiraine.firstname = "moiraine";
moiraine.city = tarValon; // reference data
// save domain entity
repo.save(moiraine);
// find and assert current location set correctly
Person loaded = repo.findOne(moiraine.getId());
assertThat(loaded.city, is(tarValon));
// remove reference location data
kvTemplate.delete("1", City.class);
// find and assert the location is gone
Person reLoaded = repo.findOne(moiraine.getId());
assertThat(reLoaded.city, IsNull.nullValue());
}
/**
* @see DATAREDIS-425
*/
@Test
public void findReturnsPageCorrectly() {
Person eddard = new Person("eddard", "stark");
Person robb = new Person("robb", "stark");
Person sansa = new Person("sansa", "stark");
Person arya = new Person("arya", "stark");
Person bran = new Person("bran", "stark");
Person rickon = new Person("rickon", "stark");
repo.save(Arrays.asList(eddard, robb, sansa, arya, bran, rickon));
Page<Person> page1 = repo.findPersonByLastname("stark", new PageRequest(0, 5));
assertThat(page1.getNumberOfElements(), is(5));
assertThat(page1.getTotalElements(), is(6L));
Page<Person> page2 = repo.findPersonByLastname("stark", page1.nextPageable());
assertThat(page2.getNumberOfElements(), is(1));
assertThat(page2.getTotalElements(), is(6L));
}
/**
* @see DATAREDIS-425
*/
@Test
public void findUsingOrReturnsResultCorrectly() {
Person eddard = new Person("eddard", "stark");
Person robb = new Person("robb", "stark");
Person jon = new Person("jon", "snow");
repo.save(Arrays.asList(eddard, robb, jon));
List<Person> eddardAndJon = repo.findByFirstnameOrLastname("eddard", "snow");
assertThat(eddardAndJon, hasSize(2));
assertThat(eddardAndJon, containsInAnyOrder(eddard, jon));
}
public static interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByFirstname(String firstname);
List<Person> findByLastname(String lastname);
Page<Person> findPersonByLastname(String lastname, Pageable page);
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
List<Person> findByFirstnameOrLastname(String firstname, String lastname);
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.<IndexDefinition> singleton(new SimpleIndexDefinition("persons", "lastname"));
}
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
return Collections.singleton(new KeyspaceSettings(City.class, "cities"));
}
}
@RedisHash("persons")
@SuppressWarnings("serial")
public static class Person implements Serializable {
@Id String id;
@Indexed String firstname;
String lastname;
@Reference City city;
public Person() {}
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getLastname() {
return lastname;
}
@Override
public String toString() {
return "Person [id=" + id + ", firstname=" + firstname + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((firstname == null) ? 0 : firstname.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
if (firstname == null) {
if (other.firstname != null) {
return false;
}
} else if (!firstname.equals(other.firstname)) {
return false;
}
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
}
public static class City {
@Id String id;
String name;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof City)) {
return false;
}
City other = (City) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2016 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.
@@ -15,42 +15,14 @@
*/
package org.springframework.data.redis.repository;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.junit.Assert.*;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.hamcrest.core.IsNull;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.keyvalue.core.KeyValueTemplate;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
import org.springframework.data.redis.core.index.IndexConfiguration;
import org.springframework.data.redis.core.index.IndexDefinition;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.repository.CrudRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -59,7 +31,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class RedisRepositoryIntegrationTests {
public class RedisRepositoryIntegrationTests extends RedisRepositoryIntegrationTestBase {
@Configuration
@EnableRedisRepositories(considerNestedRepositories = true, indexConfiguration = MyIndexConfiguration.class,
@@ -78,322 +50,5 @@ public class RedisRepositoryIntegrationTests {
return template;
}
}
@Autowired PersonRepository repo;
@Autowired KeyValueTemplate kvTemplate;
@Before
public void setUp() {
// flush keyspaces
kvTemplate.delete(Person.class);
kvTemplate.delete(City.class);
}
/**
* @see DATAREDIS-425
*/
@Test
public void simpleFindShouldReturnEntitiesCorrectly() {
Person rand = new Person();
rand.firstname = "rand";
rand.lastname = "al'thor";
Person egwene = new Person();
egwene.firstname = "egwene";
repo.save(Arrays.asList(rand, egwene));
assertThat(repo.count(), is(2L));
assertThat(repo.findOne(rand.id), is(rand));
assertThat(repo.findOne(egwene.id), is(egwene));
assertThat(repo.findByFirstname("rand").size(), is(1));
assertThat(repo.findByFirstname("rand"), hasItem(rand));
assertThat(repo.findByFirstname("egwene").size(), is(1));
assertThat(repo.findByFirstname("egwene"), hasItem(egwene));
assertThat(repo.findByLastname("al'thor"), hasItem(rand));
}
/**
* @see DATAREDIS-425
*/
@Test
public void simpleFindByMultipleProperties() {
Person egwene = new Person();
egwene.firstname = "egwene";
egwene.lastname = "al'vere";
Person marin = new Person();
marin.firstname = "marin";
marin.lastname = "al'vere";
repo.save(Arrays.asList(egwene, marin));
assertThat(repo.findByLastname("al'vere").size(), is(2));
assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").size(), is(1));
assertThat(repo.findByFirstnameAndLastname("egwene", "al'vere").get(0), is(egwene));
}
/**
* @see DATAREDIS-425
*/
@Test
public void findReturnsReferenceDataCorrectly() {
// Prepare referenced data entry
City tarValon = new City();
tarValon.id = "1";
tarValon.name = "tar valon";
kvTemplate.insert(tarValon);
// Prepare domain entity
Person moiraine = new Person();
moiraine.firstname = "moiraine";
moiraine.city = tarValon; // reference data
// save domain entity
repo.save(moiraine);
// find and assert current location set correctly
Person loaded = repo.findOne(moiraine.getId());
assertThat(loaded.city, is(tarValon));
// remove reference location data
kvTemplate.delete("1", City.class);
// find and assert the location is gone
Person reLoaded = repo.findOne(moiraine.getId());
assertThat(reLoaded.city, IsNull.nullValue());
}
/**
* @see DATAREDIS-425
*/
@Test
public void findReturnsPageCorrectly() {
Person eddard = new Person("eddard", "stark");
Person robb = new Person("robb", "stark");
Person sansa = new Person("sansa", "stark");
Person arya = new Person("arya", "stark");
Person bran = new Person("bran", "stark");
Person rickon = new Person("rickon", "stark");
repo.save(Arrays.asList(eddard, robb, sansa, arya, bran, rickon));
Page<Person> page1 = repo.findPersonByLastname("stark", new PageRequest(0, 5));
assertThat(page1.getNumberOfElements(), is(5));
assertThat(page1.getTotalElements(), is(6L));
Page<Person> page2 = repo.findPersonByLastname("stark", page1.nextPageable());
assertThat(page2.getNumberOfElements(), is(1));
assertThat(page2.getTotalElements(), is(6L));
}
/**
* @see DATAREDIS-425
*/
@Test
public void findUsingOrReturnsResultCorrectly() {
Person eddard = new Person("eddard", "stark");
Person robb = new Person("robb", "stark");
Person jon = new Person("jon", "snow");
repo.save(Arrays.asList(eddard, robb, jon));
List<Person> eddardAndJon = repo.findByFirstnameOrLastname("eddard", "snow");
assertThat(eddardAndJon, hasSize(2));
assertThat(eddardAndJon, containsInAnyOrder(eddard, jon));
}
public static interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByFirstname(String firstname);
List<Person> findByLastname(String lastname);
Page<Person> findPersonByLastname(String lastname, Pageable page);
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
List<Person> findByFirstnameOrLastname(String firstname, String lastname);
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<IndexDefinition> initialConfiguration() {
return Collections.<IndexDefinition> singleton(new SimpleIndexDefinition("persons", "lastname"));
}
}
/**
* Custom Redis {@link IndexConfiguration} forcing index of {@link Person#lastname}.
*
* @author Christoph Strobl
*/
static class MyKeyspaceConfiguration extends KeyspaceConfiguration {
@Override
protected Iterable<KeyspaceSettings> initialConfiguration() {
return Collections.singleton(new KeyspaceSettings(City.class, "cities"));
}
}
@RedisHash("persons")
@SuppressWarnings("serial")
public static class Person implements Serializable {
@Id String id;
@Indexed String firstname;
String lastname;
@Reference City city;
public Person() {}
public Person(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getLastname() {
return lastname;
}
@Override
public String toString() {
return "Person [id=" + id + ", firstname=" + firstname + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((firstname == null) ? 0 : firstname.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Person)) {
return false;
}
Person other = (Person) obj;
if (firstname == null) {
if (other.firstname != null) {
return false;
}
} else if (!firstname.equals(other.firstname)) {
return false;
}
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
}
public static class City {
@Id String id;
String name;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof City)) {
return false;
}
City other = (City) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
}
}