();
+ template.setConnectionFactory(connectionFactory);
+
+ return template;
+ }
+
+}
diff --git a/redis/repository/src/main/java/example/springdata/redis/repositories/Gender.java b/redis/repository/src/main/java/example/springdata/redis/repositories/Gender.java
new file mode 100644
index 00000000..7686d31a
--- /dev/null
+++ b/redis/repository/src/main/java/example/springdata/redis/repositories/Gender.java
@@ -0,0 +1,23 @@
+/*
+ * 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 example.springdata.redis.domain;
+
+/**
+ * @author Christoph Strobl
+ */
+enum Gender {
+ FEMALE, MALE
+}
diff --git a/redis/repository/src/main/java/example/springdata/redis/repositories/Person.java b/redis/repository/src/main/java/example/springdata/redis/repositories/Person.java
new file mode 100644
index 00000000..256e99da
--- /dev/null
+++ b/redis/repository/src/main/java/example/springdata/redis/repositories/Person.java
@@ -0,0 +1,117 @@
+/*
+ * 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 example.springdata.redis.domain;
+
+import java.lang.reflect.Field;
+import java.util.List;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Reference;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+/**
+ * {@link Person} object stored inside a Redis {@literal HASH}.
+ *
+ * Sample (key = persons:9b0ed8ee-14be-46ec-b5fa-79570aadb91d):
+ *
+ *
+ *
+ * _class := example.springdata.redis.domain.Person
+ * id := 9b0ed8ee-14be-46ec-b5fa-79570aadb91d
+ * firstname := eddard
+ * lastname := stark
+ * gender := MALE
+ * address.city := winterfell
+ * address.country := the north
+ * children.[0] := persons:41436096-aabe-42fa-bd5a-9a517fbf0260
+ * children.[1] := persons:1973d8e7-fbd4-4f93-abab-a2e3a00b3f53
+ * children.[2] := persons:440b24c6-ede2-495a-b765-2d8b8d6e3995
+ * children.[3] := persons:85f0c1d1-cef6-40a4-b969-758ebb68dd7b
+ * children.[4] := persons:73cb36e8-add9-4ec0-b5dd-d820e04f44f0
+ * children.[5] := persons:9c2461aa-2ef2-469f-83a2-bd216df8357f
+ *
+ *
+ *
+ * @author Christoph Strobl
+ */
+@Data
+@EqualsAndHashCode(exclude = { "children" })
+@RedisHash("persons")
+class Person {
+
+ /**
+ * The {@literal id} and {@link RedisHash#toString()} build up the {@literal key} for the Redis {@literal HASH}.
+ *
+ *
+ *
+ * {@link RedisHash#value()} + ":" + {@link Person#id}
+ * //eg. persons:9b0ed8ee-14be-46ec-b5fa-79570aadb91d
+ *
+ *
+ *
+ * Note: empty {@literal id} fields are automatically assigned during save operation.
+ */
+ private @Id String id;
+
+ /**
+ * Using {@link Indexed} marks the property as for indexing which uses Redis {@literal SET} to keep track of
+ * {@literal ids} for objects with matching values.
+ *
+ *
+ *
+ * {@link RedisHash#value()} + ":" + {@link Field#getName()} +":" + {@link Field#get(Object)}
+ * //eg. persons:firstname:eddard
+ *
+ *
+ */
+ private @Indexed String firstname;
+ private @Indexed String lastname;
+
+ private Gender gender;
+
+ /**
+ * Since {@link Indexed} is used on {@link Address#getCity()} index structures for {@code persons:address:city} are be
+ * maintained.
+ */
+ private Address address;
+
+ /**
+ * Using {@link Reference} allows to link to existing objects via their {@literal key}. The values stored in the
+ * objects {@literal HASH} looks like:
+ *
+ *
+ *
+ * children.[0] := persons:41436096-aabe-42fa-bd5a-9a517fbf0260
+ * children.[1] := persons:1973d8e7-fbd4-4f93-abab-a2e3a00b3f53
+ * children.[2] := persons:440b24c6-ede2-495a-b765-2d8b8d6e3995
+ *
+ *
+ */
+ private @Reference List children;
+
+ public Person() {}
+
+ public Person(String firstname, String lastname, Gender gender) {
+ this.firstname = firstname;
+ this.lastname = lastname;
+ this.gender = gender;
+ }
+
+}
diff --git a/redis/repository/src/main/java/example/springdata/redis/repositories/PersonRepository.java b/redis/repository/src/main/java/example/springdata/redis/repositories/PersonRepository.java
new file mode 100644
index 00000000..a9c4c128
--- /dev/null
+++ b/redis/repository/src/main/java/example/springdata/redis/repositories/PersonRepository.java
@@ -0,0 +1,39 @@
+/*
+ * 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 example.springdata.redis.domain;
+
+import java.util.List;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * @author Christoph Strobl
+ */
+interface PersonRepository extends CrudRepository {
+
+ List findByLastname(String lastname);
+
+ Page findPersonByLastname(String lastname, Pageable page);
+
+ List findByFirstnameAndLastname(String firstname, String lastname);
+
+ List findByFirstnameOrLastname(String firstname, String lastname);
+
+ List findByAddress_City(String city);
+
+}
diff --git a/redis/repository/src/test/java/example/springdata/redis/repositories/PersonRepositoryTests.java b/redis/repository/src/test/java/example/springdata/redis/repositories/PersonRepositoryTests.java
new file mode 100644
index 00000000..23bc6884
--- /dev/null
+++ b/redis/repository/src/test/java/example/springdata/redis/repositories/PersonRepositoryTests.java
@@ -0,0 +1,216 @@
+/*
+ * 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 example.springdata.redis.domain;
+
+import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*;
+import static org.hamcrest.core.Is.*;
+import static org.hamcrest.core.IsCollectionContaining.*;
+import static org.hamcrest.core.IsNot.*;
+import static org.junit.Assert.*;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.data.redis.core.index.Indexed;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import example.springdata.redis.AppConfig;
+import example.springdata.redis.test.util.EmbeddedRedisServer;
+import example.springdata.redis.test.util.RequiresRedisServer;
+
+/**
+ * @author Christoph Strobl
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = AppConfig.class)
+public class PersonRepositoryTests {
+
+ /**
+ * We need to have a Redis server instance available.
+ * 1) Start/Stop an embedded instance or reuse an already running local installation
+ * 2) Ignore tests if startup failed and no server running locally.
+ */
+ public static @ClassRule RuleChain rules = RuleChain.outerRule(
+ EmbeddedRedisServer.runningAt(6379).suppressExceptions()).around(RequiresRedisServer.onLocalhost());
+
+ /** {@link Charset} for String conversion **/
+ private static final Charset CHARSET = Charset.forName("UTF-8");
+
+ @Autowired RedisOperations ops;
+ @Autowired PersonRepository repo;
+
+ /*
+ * Set of test users
+ */
+ Person eddard = new Person("eddard", "stark", Gender.MALE);
+ Person robb = new Person("robb", "stark", Gender.MALE);
+ Person sansa = new Person("sansa", "stark", Gender.FEMALE);
+ Person arya = new Person("arya", "stark", Gender.FEMALE);
+ Person bran = new Person("bran", "stark", Gender.MALE);
+ Person rickon = new Person("rickon", "stark", Gender.MALE);
+ Person jon = new Person("jon", "snow", Gender.MALE);
+
+ @Before
+ @After
+ public void setUp() {
+
+ ops.execute((RedisConnection connection) -> {
+ connection.flushDb();
+ return "OK";
+ });
+ }
+
+ /**
+ * Save a single entity and verify that a key for the given keyspace/prefix exists.
+ * Print out the hash structure within Redis.
+ */
+ @Test
+ public void saveSingleEntity() {
+
+ repo.save(eddard);
+
+ assertThat(ops.execute((RedisConnection connection) -> connection.exists(new String("persons:" + eddard.getId())
+ .getBytes(CHARSET))), is(true));
+ }
+
+ /**
+ * Find entity by a single {@link Indexed} property value.
+ */
+ @Test
+ public void findBySingleProperty() {
+
+ flushTestUsers();
+
+ List starks = repo.findByLastname(eddard.getLastname());
+
+ assertThat(starks, containsInAnyOrder(eddard, robb, sansa, arya, bran, rickon));
+ assertThat(starks, not(hasItem(jon)));
+ }
+
+ /**
+ * Find entities by multiple {@link Indexed} properties using {@literal AND}.
+ */
+ @Test
+ public void findByMultipleProperties() {
+
+ flushTestUsers();
+
+ List aryaStark = repo.findByFirstnameAndLastname(arya.getFirstname(), arya.getLastname());
+
+ assertThat(aryaStark, hasItem(arya));
+ assertThat(aryaStark, not(hasItems(eddard, robb, sansa, bran, rickon, jon)));
+ }
+
+ /**
+ * Find entities by multiple {@link Indexed} properties using {@literal OR}.
+ */
+ @Test
+ public void findByMultiplePropertiesUsingOr() {
+
+ flushTestUsers();
+
+ List aryaAndJon = repo.findByFirstnameOrLastname(arya.getFirstname(), jon.getLastname());
+
+ assertThat(aryaAndJon, containsInAnyOrder(arya, jon));
+ assertThat(aryaAndJon, not(hasItems(eddard, robb, sansa, bran, rickon)));
+ }
+
+ /**
+ * Find entities in range defined by {@link Pageable}.
+ */
+ @Test
+ public void findByReturingPage() {
+
+ flushTestUsers();
+
+ Page page1 = repo.findPersonByLastname(eddard.getLastname(), new PageRequest(0, 5));
+
+ assertThat(page1.getNumberOfElements(), is(5));
+ assertThat(page1.getTotalElements(), is(6L));
+
+ Page page2 = repo.findPersonByLastname(eddard.getLastname(), new PageRequest(1, 5));
+
+ assertThat(page2.getNumberOfElements(), is(1));
+ assertThat(page2.getTotalElements(), is(6L));
+ }
+
+ /**
+ * Find entity by a single {@link Indexed} property on an embedded entity.
+ */
+ @Test
+ public void findByEmbeddedProperty() {
+
+ Address winterfell = new Address();
+ winterfell.setCountry("the north");
+ winterfell.setCity("winterfell");
+
+ eddard.setAddress(winterfell);
+
+ flushTestUsers();
+
+ List eddardStark = repo.findByAddress_City(winterfell.getCity());
+
+ assertThat(eddardStark, hasItem(eddard));
+ assertThat(eddardStark, not(hasItems(robb, sansa, arya, bran, rickon, jon)));
+ }
+
+ /**
+ * Store references to other entites without embedding all data.
+ * Print out the hash structure within Redis.
+ */
+ @Test
+ public void useReferencesToStoreDataToOtherObjects() {
+
+ flushTestUsers();
+
+ eddard.setChildren(Arrays.asList(jon, robb, sansa, arya, bran, rickon));
+
+ repo.save(eddard);
+
+ Person laoded = repo.findOne(eddard.getId());
+ assertThat(laoded.getChildren(), hasItems(jon, robb, sansa, arya, bran, rickon));
+
+ /*
+ * Deceased:
+ *
+ * - Robb was killed by Roose Bolton during the Red Wedding.
+ * - Jon was stabbed by brothers or the Night's Watch.
+ */
+ repo.delete(Arrays.asList(robb, jon));
+
+ laoded = repo.findOne(eddard.getId());
+ assertThat(laoded.getChildren(), hasItems(sansa, arya, bran, rickon));
+ assertThat(laoded.getChildren(), not(hasItems(robb, jon)));
+ }
+
+ private void flushTestUsers() {
+ repo.save(Arrays.asList(eddard, robb, sansa, arya, bran, rickon, jon));
+ }
+}
diff --git a/redis/repository/src/test/resources/logback.xml b/redis/repository/src/test/resources/logback.xml
new file mode 100644
index 00000000..61c86dd9
--- /dev/null
+++ b/redis/repository/src/test/resources/logback.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/redis/util/pom.xml b/redis/util/pom.xml
index 7d72cf68..89b17291 100644
--- a/redis/util/pom.xml
+++ b/redis/util/pom.xml
@@ -21,6 +21,12 @@
org.springframework.boot
spring-boot-starter-data-redis
+
+ com.github.kstyrc
+ embedded-redis
+ 0.6
+ true
+