Статьи

Создание приложения с использованием Spring Data с Redis в качестве хранилища данных — часть 1

В первой части серии о Redis я говорил о том, что такое хранилище данных Redis, типы данных, поддерживаемые Redis, и как вы можете начать работу с Redis. В этой статье я расскажу о том, как разработчик Java может программно получать доступ к Redis и выполнять операции с использованием Spring Data. Spring Data — это новый проект по созданию приложений на основе Spring , в которых используются новые технологии доступа к данным, такие как нереляционные базы данных, структуры сокращения карт и облачные сервисы данных. В Spring Data есть множество подпроектов, специфичных для конкретного хранилища данных. Мы только взглянем на подпроект spring-data-keyvalue и поговорим только о поддержке Redis key-value store. Хранилище spring-data-keyvalue также поддерживает другое хранилище значений ключей, называемое Riak.но я ограничу эту статью только Redis.

Проект SDKV (spring-data-keyvalue) обеспечивает абстракцию существующих клиентских библиотек Redis, таких как Jedis, JRedis. Это позволяет очень легко работать с хранилищем данных ключей Redis, исключая код панели, необходимый для взаимодействия с Redis. SDKV предоставляет обобщенный шаблонный класс с именем RedisTemplate, который очень похож на JDBCTemplate или HibernateTemplate для взаимодействия с Redis. Это освобождает разработчиков от изучения низкоуровневого API.

Предпосылки

Прежде чем начать работать, убедитесь, что у вас есть следующее

  1. Redis (пользователь Windows может загрузить Redis из  репозитория dmajkic git )
  2. Java V6 JDK
  3. Apache Maven V2.0.9 или выше
  4. SpringSource Tool Suite
  5. Гит

Если у вас возникли проблемы с установкой сервера Redis, обратитесь к моей предыдущей статье .

Создать исходный код spring-data-keyvalue

В этой статье будет использоваться текущая версия разработки (1.0.0.M2) проекта spring-data-keyvalue. Чтобы получить последний исходный код, вы должны оформить проект spring-data-keyvalue с помощью Git, введя следующую команду

git clone git://github.com/SpringSource/spring-data-keyvalue.git

Эта команда создаст папку с именем spring-data-keyvalue, в которой будет весь исходный код. Мы должны создать этот исходный код, чтобы артефакты были доступны в вашем хранилище maven. Перед сборкой проекта вы должны запустить Redis с помощью команды redis-server. После запуска Redis, пожалуйста, выполните mvn clean install поверх проекта. Это создаст проект и поместит необходимые артефакты в ваш локальный репозиторий maven.

Создать шаблон проекта с использованием STS

Нам нужно создать шаблонный проект Spring, чтобы мы могли использовать его для нашего простого приложения. Для этого откройте STS и перейдите в Файл -> Создать -> Проект шаблона Spring -> Простой проект Spring Spring -> нажмите Да при появлении запроса. Пожалуйста, укажите название проекта и имя пакета верхнего уровня. Я использую имя проекта в качестве словаря и имя пакета как «com.redis.dictionary». Это создаст пример проекта с именем словарь в вашем рабочем пространстве STS.

Изменить pom.xml

Проект, который мы создали выше, не имеет зависимостей, связанных с проектом SDKV. Для этого замените файл pom.xml проекта шаблона, который мы только что создали, на файл pom.xml, показанный ниже.

<?xml version="1.0" encoding="UTF-8"?>xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.shekhar</groupId><artifactId>redis</artifactId><name>redis-dictionary</name><packaging>war</packaging><version>1.0.0-BUILD-SNAPSHOT</version><properties><java-version>1.6</java-version><org.springframework-version>3.0.5.RELEASE</org.springframework-version><org.springframework.roo-version>1.0.2.RELEASE</org.springframework.roo-version><org.aspectj-version>1.6.9</org.aspectj-version><redis.version>1.0.0.M2-SNAPSHOT</redis.version></properties><dependencies><!-- Spring --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${org.springframework-version}</version><exclusions><!-- Exclude Commons Logging in favor of <span class="hiddenSpellError" pre="of ">SLF4j</span> --><exclusion><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId></exclusion></exclusions></dependency><!-- <span class="hiddenSpellError" pre="">AspectJ</span> --><dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId><version>${org.aspectj-version}</version></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.15</version><exclusions><exclusion><groupId>javax.mail</groupId><artifactId>mail</artifactId></exclusion><exclusion><groupId>javax.jms</groupId><artifactId>jms</artifactId></exclusion><exclusion><groupId>com.sun.jdmk</groupId><artifactId>jmxtools</artifactId></exclusion><exclusion><groupId>com.sun.jmx</groupId><artifactId>jmxri</artifactId></exclusion></exclusions><scope>runtime</scope></dependency><!-- @Inject --><dependency><groupId>javax.inject</groupId><artifactId>javax.inject</artifactId><version>1</version></dependency><!-- Test --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.8.1</version><scope>test</scope></dependency><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>${redis.version}</version></dependency><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-keyvalue-core</artifactId><version>${redis.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>${org.springframework-version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>${org.springframework-version}</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-io</artifactId><version>1.3.2</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>${org.springframework-version}</version><scope>test</scope><exclusions><exclusion><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId></exclusion></exclusions></dependency></dependencies><repositories><repository><id>spring-maven-milestone</id>Springframework Maven Repository<url>http://maven.springframework.org/milestone</url></repository><repository><id>spring-maven-snapshot</id><snapshots><enabled>true</enabled></snapshots>Springframework Maven SNAPSHOT Repository<url>http://maven.springframework.org/snapshot</url></repository></repositories><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>${java-version}</source><target>${java-version}</target></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><executions><execution><id>install</id><phase>install</phase><goals><goal>sources</goal></goals></execution></executions></plugin></plugins></build></project>

 

Creating a Dictionary Application

Now that we have completed all the preliminary stuff required for working with Redis and Spring data. Let’s build a small application which will help us understand how we can use SDKV API with Redis.

The application that we are going to build is a simple dictionary application which will let us perform CRUD operations on the Redis datastore. A dictionary is a collection of words where each word can have multiple meanings. The dictionary application can be very easily modeled to Redis List datatype with word as the key of list and meanings as its value. Instead of List you can also use Set if you want meanings should be unique. For example, we can have a word «astonishing» as a key and «astounding», «staggering» as its values. Let’s create a simple word meaning list using redis-cli.  Please start Redis using redis-server command and client using redis-cli command.

redis> RPUSH astonishing astounding(integer) 1redis> RPUSH astonishing staggering(integer) 2redis> LRANGE astonishing 0 -11) "astounding"2) "staggering"

In the above redis commands snippet, we created a list named astonishing and we pushed meanings astounding and staggering in the end of astonishing list. To get all the meanings for astonishing we using LRANGE command.

Now that we have seen how it works using redis-cli lets create a class called DictionaryDao which will let us perform CRUD operations on Redis using SDKV API. As I have talked above that the core class in SDKV project is RedisTemplate we will inject RedisTemplate in our class.

import org.springframework.data.keyvalue.redis.core.RedisTemplate;public class DictionaryDao {private RedisTemplate<String, String> template;public DictionaryDao(RedisTemplate template) {this.template = template;}public Long addWordWithItsMeaningToDictionary(String word, String meaning) {Long index = template.opsForList().rightPush(word, meaning);return index;}}

RedisTemplate provides key type operations like ValueOperations,ListOperations, SetOperations, HashOperations, ZSetOperations. In the code shown above I have used ListOperations to store a new word in the Redis datastore. As we are using rightPush operation meanings will get added to end of the list. This method will return the index to which the element is added in the list. Lets write JUnit test case for this method

@Testpublic void shouldAddWordWithItsMeaningToDictionary() {JedisConnectionFactory factory = new JedisConnectionFactory();factory.setUsePool(true);factory.setPort(6379);factory.setHostName("localhost");factory.afterPropertiesSet();RedisTemplate<String, String> template = new RedisTemplate<String, String>(factory);DictionaryDao dao = new DictionaryDao(template);Long index = dao.addWordWithItsMeaningToDictionary("lollop","To move forward with a bounding, drooping motion.");assertThat(index, is(notNullValue()));assertThat(index, is(equalTo(1L)));}

In the test shown above, we first created the JedisConnectionFactory because RedisTemplate requires a connection factory which it will use for connecting to Redis. There is also another connection factory called JRedisConnectionFactory which can also be used for connecting with Redis. When you run this test, this test will pass and word will get stored in Redis. But when you will run this test again, this test will fail because Redis will add the same meaning again and index returned will be 2. So, we have to clean the Redis datastore after each run. To clean up the Redis datastore we have to use either flushAll() or flushDb server commands. The difference between flushAll() and flushDb is that flushAll will remove all keys from all databases where as flushDb() will remove keys from only current database. So, we can modify our test as shown below

import static org.hamcrest.CoreMatchers.equalTo;import static org.hamcrest.CoreMatchers.is;import static org.hamcrest.CoreMatchers.notNullValue;import static org.junit.Assert.assertThat;import org.junit.After;import org.junit.Before;import org.junit.Test;import org.springframework.data.keyvalue.redis.connection.jedis.JedisConnectionFactory;import org.springframework.data.keyvalue.redis.core.RedisTemplate;public class DictionaryDaoIntegrationTest {private RedisTemplate<String, String> template;private DictionaryDao dao;@Beforepublic void setUp() throws Exception {this.template = getRedisTemplate();this.template.afterPropertiesSet();dao = new DictionaryDao(template);}protected JedisConnectionFactory getConnectionFactory() {JedisConnectionFactory factory = new JedisConnectionFactory();factory.setUsePool(true);factory.setPort(6379);factory.setHostName("localhost");factory.afterPropertiesSet();return factory;}protected RedisTemplate<String, String> getRedisTemplate() {return new RedisTemplate(getConnectionFactory());}@Afterpublic void tearDown() throws Exception {template.getConnectionFactory().getConnection().flushAll();template.getConnectionFactory().getConnection().close();}@Testpublic void shouldAddWordWithItsMeaningToDictionary() {Long index = dao.addWordWithItsMeaningToDictionary("lollop","To move forward with a bounding, drooping motion.");assertThat(index, is(notNullValue()));assertThat(index, is(equalTo(1L)));}@Testpublic void shouldAddMeaningToAWordIfItExists() {Long index = dao.addWordWithItsMeaningToDictionary("lollop","To move forward with a bounding, drooping motion.");assertThat(index, is(notNullValue()));assertThat(index, is(equalTo(1L)));index = dao.addWordWithItsMeaningToDictionary("lollop","To hang loosely; droop; dangle.");assertThat(index, is(equalTo(2L)));}}

 Now that we have the functionality to store the word in Redis datastore it makes sense to have the functionality to get all the meanings for a word. This can be handled very easily using List’s range operation.The range() method takes three arguments — name of the key, start and end of range. In order to get all the meanings for a word we can use start as 0 and end as -1.

public List getAllTheMeaningsForAWord(String word) {List<String> meanings = template.opsForList().range(word, 0, -1);return meanings;}

The last feature that I would like to add in this article is deleting an existing word. This can be done using delete operation of RedisTemplate class. The delete operation takes a collection of keys.

public void removeWords(String... words) {template.delete(Arrays.asList(words));}

Lets write JUnit test cases for the Read and Delete operations that we have added above.

@Testpublic void shouldGetAllTheMeaningForAWord() {setupOneWord();List allMeanings = dao.getAllTheMeaningsForAWord("lollop");assertThat(allMeanings.size(), is(equalTo(2)));assertThat(allMeanings,hasItems("To move forward with a bounding, drooping motion.","To hang loosely; droop; dangle."));}@Testpublic void shouldDeleteAWordFromDictionary() throws Exception {setupOneWord();dao.removeWords("lollop");List allMeanings = dao.getAllTheMeaningsForAWord("lollop");assertThat(allMeanings.size(), is(equalTo(0)));}private void setupOneWord() {dao.addWordWithItsMeaningToDictionary("lollop","To move forward with a bounding, drooping motion.");dao.addWordWithItsMeaningToDictionary("lollop","To hang loosely; droop; dangle.");}

Conclusion to First Part

In this article, we have just started with Spring Data Redis support. We have just looked at some of the List operations provided by SDKV project. In the future parts, I will talk about other datatypes, pub-sub support, using MULTI-EXEC block. You can get the source code for this series on my github repository.