diff --git a/src/main/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc index 9cca5cbdb..f190b11b2 100644 --- a/src/main/asciidoc/reference/redis-repositories.adoc +++ b/src/main/asciidoc/reference/redis-repositories.adoc @@ -26,7 +26,7 @@ public class Person { ==== We have a pretty simple domain object here. Note that it has a property named `id` annotated with `org.springframework.data.annotation.Id` and a `@RedisHash` annotation on its type. -Those two are responsible for creating the actual key used to persist the hash. +Those two are responsible for creating the actual key used to persist the hash. NOTE: Properties annotated with `@Id` as well as those named `id` are considered as the identifier properties. Those with the annotation are favored over others. @@ -56,7 +56,7 @@ public class ApplicationConfig { public RedisConnectionFactory connectionFactory() { return new JedisConnectionFactory(); } - + @Bean public RedisTemplate redisTemplate() { @@ -75,17 +75,17 @@ Given the setup above we can go on and inject `PersonRepository` into our compon ---- @Autowired PersonRepository repo; -public void basicCrudOperations() { +public void basicCrudOperations() { Person rand = new Person("rand", "al'thor"); rand.setAddress(new Address("emond's field", "andor")); - + repo.save(rand); <1> - + repo.findOne(rand.getId()); <2> - + repo.count(); <3> - + repo.delete(rand); <4> } ---- @@ -113,7 +113,7 @@ address.country = andor ---- <1> The `_class` attribute is included on root level as well as on any nested interface or abstract types. <2> Simple property values are mapped by path. -<3> Properties of complex types are mapped by their dot path. +<3> Properties of complex types are mapped by their dot path. ==== [cols="1,2,3", options="header"] @@ -163,7 +163,7 @@ Mapping behavior can be customized by registering the according `Converter` in ` .Sample byte[] Converters ==== [source,java] ----- +---- @WritingConverter public class AddressToBytesConverter implements Converter { @@ -183,15 +183,15 @@ public class AddressToBytesConverter implements Converter { @ReadingConverter public class BytesToAddressConverter implements Converter { - + private final Jackson2JsonRedisSerializer
serializer; - + public BytesToAddressConverter() { - + serializer = new Jackson2JsonRedisSerializer
(Address.class); serializer.setObjectMapper(new ObjectMapper()); } - + @Override public Address convert(byte[] value) { return serializer.deserialize(value); @@ -203,10 +203,10 @@ public class BytesToAddressConverter implements Converter { Using the above byte[] `Converter` produces eg. ==== [source,text] ----- +---- _class = org.example.Person id = e2c7dcee-b8cd-4424-883e-736ce564363e -firstname = rand +firstname = rand lastname = al’thor address = { city : "emond's field", country : "andor" } ---- @@ -216,7 +216,7 @@ address = { city : "emond's field", country : "andor" } .Sample Map Converters ==== [source,java] ----- +---- @WritingConverter public class AddressToMapConverter implements Converter> { @@ -228,7 +228,7 @@ public class AddressToMapConverter implements Converter> { - + @Override public Address convert(Map source) { return new Address(new String(source.get("ciudad"))); @@ -241,10 +241,10 @@ Using the above Map `Converter` produces eg. ==== [source,text] ----- +---- _class = org.example.Person id = e2c7dcee-b8cd-4424-883e-736ce564363e -firstname = rand +firstname = rand lastname = al’thor ciudad = "emond's field" ---- @@ -266,7 +266,7 @@ By default the prefix is set to `getClass().getName()`. This default can be alte public class ApplicationConfig { //... RedisConnectionFactory and RedisTemplate Bean definitions omitted - + public static class MyKeyspaceConfiguration extends KeyspaceConfiguration { @Override @@ -287,14 +287,14 @@ public class ApplicationConfig { public class ApplicationConfig { //... RedisConnectionFactory and RedisTemplate Bean definitions omitted - + @Bean public RedisMappingContext keyValueMappingContext() { return new RedisMappingContext( new MappingConfiguration( - new MyKeyspaceConfiguration(), new IndexConfiguration())); + new MyKeyspaceConfiguration(), new IndexConfiguration())); } - + public static class MyKeyspaceConfiguration extends KeyspaceConfiguration { @Override @@ -310,7 +310,7 @@ public class ApplicationConfig { == Secondary Indexes http://redis.io/topics/indexes[Secondary indexes] are used to enable lookup operations based on native Redis structures. Values are written to the according indexes on every save and are removed when objects are deleted or <>. -Given the sample `Person` entity we can create an index for _firstname_ by annotating the property with `@Indexed`. +Given the sample `Person` entity we can create an index for _firstname_ by annotating the property with `@Indexed`. .Annotation driven indexing ==== @@ -355,7 +355,7 @@ Further more the programmatic setup allows to define indexes on map keys and lis public class Person { // ... other properties omitted - + Map attributes; <1> Map relatives; <2> List
addresses; <3> @@ -379,7 +379,7 @@ Same as with _keyspaces_ it is possible to configure indexes without the need of public class ApplicationConfig { //... RedisConnectionFactory and RedisTemplate Bean definitions omitted - + public static class MyIndexConfiguration extends IndexConfiguration { @Override @@ -400,14 +400,14 @@ public class ApplicationConfig { public class ApplicationConfig { //... RedisConnectionFactory and RedisTemplate Bean definitions omitted - + @Bean public RedisMappingContext keyValueMappingContext() { return new RedisMappingContext( new MappingConfiguration( - new KeyspaceConfiguration(), new MyIndexConfiguration())); + new KeyspaceConfiguration(), new MyIndexConfiguration())); } - + public static class MyIndexConfiguration extends IndexConfiguration { @Override @@ -427,7 +427,7 @@ The expiration time in seconds can be set via `@RedisHash(timeToLive=...)` as we More flexible expiration times can be set by using the `@TimeToLive` annotation on either a numeric property or method. However do not apply `@TimeToLive` on both a method and a property within the same class. -.Expirations +.Expirations ==== [source,java] ---- @@ -435,16 +435,16 @@ public class TimeToLiveOnProperty { @Id private String id; - + @TimeToLive private Long expiration; } public class TimeToLiveOnMethod { - @Id + @Id private String id; - + @TimeToLive public long getTimeToLive() { return new Random().nextLong(); @@ -456,13 +456,19 @@ public class TimeToLiveOnMethod { The repository implementation ensures subscription to http://redis.io/topics/notifications[Redis keyspace notifications] via `RedisMessageListenerContainer`. -When the expiration is set to a positive value the according `EXPIRE` command is executed. +When the expiration is set to a positive value the according `EXPIRE` command is executed. Additionally to persisting the original, a _phantom_ copy is persisted in Redis and set to expire 5 minutes after the original one. This is done to enable the Repository support to publish `RedisKeyExpiredEvent` holding the expired value via Springs `ApplicationEventPublisher` whenever a key expires even though the original values have already been gone. Expiry events will be received on all connected applications using Spring Data Redis repositories. -The `RedisKeyExpiredEvent` will hold a copy of the actually expired domain object as well as the key. +By default, the key expiry listener is disabled when initializing the application. The startup mode can be adjusted in `@EnableRedisRepositories` or `RedisKeyValueAdapter` to start the listener with the application or upon the first insert of an entity with a TTL. See `EnableKeyspaceEvents` for possible values. -NOTE: The keyspace notification message listener will alter `notify-keyspace-events` settings in Redis if those are not already set. Existing settings will not be overridden, so it is left to the user to set those up correctly when not leaving them empty. +The `RedisKeyExpiredEvent` will hold a copy of the actually expired domain object as well as the key. + +NOTE: Delaying or disabling the expiry event listener startup impacts `RedisKeyExpiredEvent` publishing. +A disabled event listener will not publish expiry events. A delayed startup can cause loss of events because the delayed +listener initialization. + +NOTE: The keyspace notification message listener will alter `notify-keyspace-events` settings in Redis if those are not already set. Existing settings will not be overridden, so it is left to the user to set those up correctly when not leaving them empty. Please note that `CONFIG` is disabled on AWS ElastiCache and enabling the listener leads to an error. NOTE: Redis Pub/Sub messages are not persistent. If a key expires while the application is down the expiry event will not be processed which may lead to secondary indexes containing still references to the expired object. @@ -474,29 +480,29 @@ On loading from Redis, references are resolved automatically and mapped back int .Sample Property Reference ==== [source,text] ----- +---- _class = org.example.Person id = e2c7dcee-b8cd-4424-883e-736ce564363e -firstname = rand +firstname = rand lastname = al’thor mother = persons:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56 <1> ---- <1> Reference stores the whole key (`keyspace:id`) of the referenced object. ==== -WARNING: Referenced Objects are not subject of persisting changes when saving the referencing object. Please make sure to persist changes on referenced objects separately, since only the reference will be stored. +WARNING: Referenced Objects are not subject of persisting changes when saving the referencing object. Please make sure to persist changes on referenced objects separately, since only the reference will be stored. Indexes set on properties of referenced types will not be resolved. [[redis.repositories.queries]] == Queries and Query Methods -Query methods allow automatic derivation of simple finder queries from the method name. +Query methods allow automatic derivation of simple finder queries from the method name. .Sample Repository finder Method ==== [source,java] ---- public interface PersonRepository extends CrudRepository { - + List findByFirstname(String firstname); } ---- @@ -552,7 +558,7 @@ Still some considerations have to be done as the default key distribution will s | |=============== ==== - + 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. diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java index 8d2c5cf4a..13ddf4896 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -98,11 +98,11 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter private RedisOperations redisOps; private RedisConverter converter; private RedisMessageListenerContainer messageListenerContainer; - private AtomicReference expirationListener = new AtomicReference( + private final AtomicReference expirationListener = new AtomicReference( null); private ApplicationEventPublisher eventPublisher; - private EnableKeyspaceEvents enableKeyspaceEvents = EnableKeyspaceEvents.ON_STARTUP; + private EnableKeyspaceEvents enableKeyspaceEvents = EnableKeyspaceEvents.OFF; /** * Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default @@ -144,10 +144,9 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter mappingConverter.setCustomConversions(customConversions == null ? new CustomConversions() : customConversions); mappingConverter.afterPropertiesSet(); - converter = mappingConverter; + this.converter = mappingConverter; this.redisOps = redisOps; - - intiMessageListenerContainer(); + initMessageListenerContainer(); } /** @@ -162,10 +161,9 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter Assert.notNull(redisOps, "RedisOperations must not be null!"); - converter = redisConverter; + this.converter = redisConverter; this.redisOps = redisOps; - - intiMessageListenerContainer(); + initMessageListenerContainer(); } /** @@ -493,7 +491,9 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter this.expirationListener.get().destroy(); } - this.messageListenerContainer.destroy(); + if(this.messageListenerContainer != null){ + this.messageListenerContainer.destroy(); + } } /* @@ -535,12 +535,12 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter this.eventPublisher = applicationContext; } - private void intiMessageListenerContainer() { + private void initMessageListenerContainer() { this.messageListenerContainer = new RedisMessageListenerContainer(); - messageListenerContainer.setConnectionFactory(((RedisTemplate) redisOps).getConnectionFactory()); - messageListenerContainer.afterPropertiesSet(); - messageListenerContainer.start(); + this.messageListenerContainer.setConnectionFactory(((RedisTemplate) redisOps).getConnectionFactory()); + this.messageListenerContainer.afterPropertiesSet(); + this.messageListenerContainer.start(); } private void initKeyExpirationListener() { diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java index 128048c43..311361a90 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java @@ -41,8 +41,9 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key; /** * Annotation to activate Redis repositories. If no base package is configured through either {@link #value()}, * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class. - * + * * @author Christoph Strobl + * @author Mark Paluch * @since 1.7 */ @Target(ElementType.TYPE) @@ -88,14 +89,14 @@ public @interface EnableRedisRepositories { * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning * for {@code PersonRepositoryImpl}. - * + * * @return */ String repositoryImplementationPostfix() default "Impl"; /** * Configures the location of where to find the Spring Data named queries properties file. - * + * * @return */ String namedQueriesLocation() default ""; @@ -103,7 +104,7 @@ public @interface EnableRedisRepositories { /** * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to * {@link Key#CREATE_IF_NOT_FOUND}. - * + * * @return */ Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; @@ -111,21 +112,21 @@ public @interface EnableRedisRepositories { /** * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to * {@link RedisRepositoryFactoryBean}. - * + * * @return */ Class repositoryFactoryBeanClass() default RedisRepositoryFactoryBean.class; /** * Configure the repository base class to be used to create repository proxies for this particular configuration. - * + * * @return */ Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; /** * Configures the name of the {@link KeyValueOperations} bean to be used with the repositories detected. - * + * * @return */ String keyValueTemplateRef() default "redisKeyValueTemplate"; @@ -138,21 +139,21 @@ public @interface EnableRedisRepositories { /** * Configures the bean name of the {@link RedisOperations} to be used. Defaulted to {@literal redisTemplate}. - * + * * @return */ String redisTemplateRef() default "redisTemplate"; /** * Set up index patterns using simple configuration class. - * + * * @return */ Class indexConfiguration() default IndexConfiguration.class; /** * Set up keyspaces for specific types. - * + * * @return */ Class keyspaceConfiguration() default KeyspaceConfiguration.class; @@ -163,6 +164,6 @@ public @interface EnableRedisRepositories { * @return * @since 1.8 */ - EnableKeyspaceEvents enableKeyspaceEvents() default EnableKeyspaceEvents.ON_DEMAND; + EnableKeyspaceEvents enableKeyspaceEvents() default EnableKeyspaceEvents.OFF; } diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java index f6ed24137..fd6225f30 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java @@ -43,7 +43,7 @@ import org.springframework.util.StringUtils; /** * {@link RepositoryConfigurationExtension} for Redis. - * + * * @author Christoph Strobl * @since 1.7 */ @@ -72,7 +72,7 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon return "redis"; } - /* + /* * (non-Javadoc) * @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getDefaultKeyValueTemplateRef() */ @@ -123,9 +123,12 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon redisKeyValueAdapterDefinition.setConstructorArgumentValues(constructorArgumentValuesForRedisKeyValueAdapter); - DirectFieldAccessor dfa = new DirectFieldAccessor(configurationSource); - AnnotationAttributes aa = (AnnotationAttributes) dfa.getPropertyValue("attributes"); - redisKeyValueAdapterDefinition.setAttribute("enableKeyspaceEvents", aa.getEnum("enableKeyspaceEvents")); + DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(configurationSource); + AnnotationAttributes attributes = (AnnotationAttributes) fieldAccessor.getPropertyValue("attributes"); + + MutablePropertyValues redisKeyValueAdapterProps = new MutablePropertyValues(); + redisKeyValueAdapterProps.add("enableKeyspaceEvents", attributes.getEnum("enableKeyspaceEvents")); + redisKeyValueAdapterDefinition.setPropertyValues(redisKeyValueAdapterProps); registerIfNotAlreadyRegistered(redisKeyValueAdapterDefinition, registry, REDIS_ADAPTER_BEAN_NAME, configurationSource); diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java index 19e13e3c0..a91920af9 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueAdapterUnitTests.java @@ -185,7 +185,6 @@ public class RedisKeyValueAdapterUnitTests { adapter.destroy(); adapter = new RedisKeyValueAdapter(template, context); - adapter.setEnableKeyspaceEvents(EnableKeyspaceEvents.OFF); adapter.afterPropertiesSet(); KeyExpirationEventMessageListener listener = ((AtomicReference) getField(adapter, diff --git a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java index 0f3fc2fd1..640b94eb6 100644 --- a/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtensionUnitTests.java @@ -15,11 +15,11 @@ */ package org.springframework.data.redis.repository.configuration; +import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import java.util.Collection; -import static org.hamcrest.core.IsEqual.*; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -39,6 +39,8 @@ import org.springframework.data.repository.config.RepositoryConfiguration; import org.springframework.data.repository.config.RepositoryConfigurationSource; /** + * Unit tests for {@link RedisRepositoryConfigurationExtension}. + * * @author Christoph Strobl */ public class RedisRepositoryConfigurationExtensionUnitTests { @@ -86,18 +88,25 @@ public class RedisRepositoryConfigurationExtensionUnitTests { * @see DATAREDIS-491 */ @Test - public void picksUpEnableKeyspaceEventsCorrectly() { + public void picksUpEnableKeyspaceEventsOnStartupCorrectly() { + + metadata = new StandardAnnotationMetadata(Config.class, true); + BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); + + assertThat(getEnableKeyspaceEvents(beanDefintionRegistry), + equalTo((Object) EnableKeyspaceEvents.ON_STARTUP)); + } + + /** + * @see DATAREDIS-491 + */ + @Test + public void picksUpEnableKeyspaceEventsDefaultCorrectly() { metadata = new StandardAnnotationMetadata(ConfigWithKeyspaceEventsDisabled.class, true); - configurationSource = new AnnotationRepositoryConfigurationSource(metadata, EnableRedisRepositories.class, loader, - environment); + BeanDefinitionRegistry beanDefintionRegistry = getBeanDefinitionRegistry(); - RedisRepositoryConfigurationExtension extension = new RedisRepositoryConfigurationExtension(); - - BeanDefinitionRegistry beanDefintionRegistry = new SimpleBeanDefinitionRegistry(); - extension.registerBeansForRoot(beanDefintionRegistry, configurationSource); - - assertThat(beanDefintionRegistry.getBeanDefinition("redisKeyValueAdapter").getAttribute("enableKeyspaceEvents"), + assertThat(getEnableKeyspaceEvents(beanDefintionRegistry), equalTo((Object) EnableKeyspaceEvents.OFF)); } @@ -126,12 +135,30 @@ public class RedisRepositoryConfigurationExtensionUnitTests { .concat(configs.toString())); } - @EnableRedisRepositories(considerNestedRepositories = true) + private BeanDefinitionRegistry getBeanDefinitionRegistry() { + + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableRedisRepositories.class, loader, environment); + + RedisRepositoryConfigurationExtension extension = new RedisRepositoryConfigurationExtension(); + + BeanDefinitionRegistry beanDefintionRegistry = new SimpleBeanDefinitionRegistry(); + extension.registerBeansForRoot(beanDefintionRegistry, configurationSource); + + return beanDefintionRegistry; + } + + private Object getEnableKeyspaceEvents(BeanDefinitionRegistry beanDefintionRegistry) { + return beanDefintionRegistry.getBeanDefinition("redisKeyValueAdapter").getPropertyValues() + .getPropertyValue("enableKeyspaceEvents").getValue(); + } + + @EnableRedisRepositories(considerNestedRepositories = true, enableKeyspaceEvents = EnableKeyspaceEvents.ON_STARTUP) static class Config { } - @EnableRedisRepositories(considerNestedRepositories = true, enableKeyspaceEvents = EnableKeyspaceEvents.OFF) + @EnableRedisRepositories(considerNestedRepositories = true) static class ConfigWithKeyspaceEventsDisabled { }