diff --git a/src/main/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc index f0fc1c128..9cca5cbdb 100644 --- a/src/main/asciidoc/reference/redis-repositories.adoc +++ b/src/main/asciidoc/reference/redis-repositories.adoc @@ -383,8 +383,8 @@ public class ApplicationConfig { public static class MyIndexConfiguration extends IndexConfiguration { @Override - protected Iterable initialConfiguration() { - return Collections.singleton(new RedisIndexSetting("persons", "firstname")); + protected Iterable initialConfiguration() { + return Collections.singleton(new SimpleIndexDefinition("persons", "firstname")); } } } @@ -411,8 +411,8 @@ public class ApplicationConfig { public static class MyIndexConfiguration extends IndexConfiguration { @Override - protected Iterable initialConfiguration() { - return Collections.singleton(new RedisIndexSetting("persons", "firstname")); + protected Iterable 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 <> 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. diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java index 002c7f30b..ddb345e7c 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java @@ -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 typeMapper; + private final Comparator listKeyComparator = new NullSafeComparator( + 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 keys = new ArrayList(partial.keySet()); + keys.sort(listKeyComparator); + Collection 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 keys = source.extractAllKeysFor(path); + List keys = new ArrayList(source.extractAllKeysFor(path)); + keys.sort(listKeyComparator); Collection target = CollectionFactory.createCollection(collectionType, valueType, keys.size()); @@ -810,4 +822,90 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { } } + private enum NaturalOrderingKeyComparator implements Comparator { + + 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 { + + 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); + } + } + } + } diff --git a/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java index 4647efd98..3b655dc6b 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java @@ -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 map = new LinkedHashMap(); + 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 map = new LinkedHashMap(); 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 map = new LinkedHashMap(); + 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")); diff --git a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java new file mode 100644 index 000000000..e9d7c22df --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryClusterIntegrationTests.java @@ -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 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 template = new RedisTemplate(); + template.setConnectionFactory(connectionFactory); + + return template; + } + } +} diff --git a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java new file mode 100644 index 000000000..0af807318 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java @@ -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 page1 = repo.findPersonByLastname("stark", new PageRequest(0, 5)); + + assertThat(page1.getNumberOfElements(), is(5)); + assertThat(page1.getTotalElements(), is(6L)); + + Page 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 eddardAndJon = repo.findByFirstnameOrLastname("eddard", "snow"); + + assertThat(eddardAndJon, hasSize(2)); + assertThat(eddardAndJon, containsInAnyOrder(eddard, jon)); + } + + public static interface PersonRepository extends CrudRepository { + + List findByFirstname(String firstname); + + List findByLastname(String lastname); + + Page findPersonByLastname(String lastname, Pageable page); + + List findByFirstnameAndLastname(String firstname, String lastname); + + List 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 initialConfiguration() { + return Collections. 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 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; + } + } + +} diff --git a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java index 866190dc3..961ecea59 100644 --- a/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTests.java @@ -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 page1 = repo.findPersonByLastname("stark", new PageRequest(0, 5)); - - assertThat(page1.getNumberOfElements(), is(5)); - assertThat(page1.getTotalElements(), is(6L)); - - Page 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 eddardAndJon = repo.findByFirstnameOrLastname("eddard", "snow"); - - assertThat(eddardAndJon, hasSize(2)); - assertThat(eddardAndJon, containsInAnyOrder(eddard, jon)); - } - - public static interface PersonRepository extends CrudRepository { - - List findByFirstname(String firstname); - - List findByLastname(String lastname); - - Page findPersonByLastname(String lastname, Pageable page); - - List findByFirstnameAndLastname(String firstname, String lastname); - - List 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 initialConfiguration() { - return Collections. 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 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; - } - } - }