diff --git a/pom.xml b/pom.xml index e6d868411f..44d2fc5de6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 1.8.0.BUILD-SNAPSHOT + 1.8.0.DATAREDIS-523-SNAPSHOT Spring Data Redis diff --git a/src/main/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc index 97529e0ca2..3ef6b219cb 100644 --- a/src/main/asciidoc/reference/redis-repositories.adoc +++ b/src/main/asciidoc/reference/redis-repositories.adoc @@ -453,6 +453,7 @@ public class TimeToLiveOnMethod { ---- ==== +NOTE: Annotating a property explicitly with `@TimeToLive` will read back the actual `TTL` or `PTTL` value from Redis. -1 indicates that the object has no expire associated. The repository implementation ensures subscription to http://redis.io/topics/notifications[Redis keyspace notifications] via `RedisMessageListenerContainer`. 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 5a1e221867..4029f8de3d 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; @@ -39,6 +40,7 @@ import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; import org.springframework.data.keyvalue.core.KeyValueAdapter; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; @@ -58,6 +60,7 @@ import org.springframework.data.redis.util.ByteUtils; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; /** @@ -295,7 +298,7 @@ public Map doInRedis(RedisConnection connection) throws DataAcce data.setId(stringId); data.setKeyspace(stringKeyspace); - return converter.read(type, data); + return readBackTimeToLiveIfSet(binId, converter.read(type, data)); } /* @@ -346,29 +349,18 @@ public List getAllOf(final Serializable keyspace) { final byte[] binKeyspace = toBytes(keyspace); - List> raw = redisOps.execute(new RedisCallback>>() { + Set ids = redisOps.execute(new RedisCallback>() { @Override - public List> doInRedis(RedisConnection connection) throws DataAccessException { - - final List> rawData = new ArrayList>(); - - Set members = connection.sMembers(binKeyspace); - - for (byte[] id : members) { - rawData.add(connection - .hGetAll(createKey(asString(keyspace), getConverter().getConversionService().convert(id, String.class)))); - } - - return rawData; + public Set doInRedis(RedisConnection connection) throws DataAccessException { + return connection.sMembers(binKeyspace); } }); - List result = new ArrayList(raw.size()); - for (Map rawData : raw) { - result.add(converter.read(Object.class, new RedisData(rawData))); + List result = new ArrayList(); + for (byte[] key : ids) { + result.add(get(key, keyspace)); } - return result; } @@ -579,6 +571,49 @@ public byte[] toBytes(Object source) { return converter.getConversionService().convert(source, byte[].class); } + /** + * Read back and set {@link TimeToLive} for the property. + * + * @param key + * @param target + * @return + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private T readBackTimeToLiveIfSet(final byte[] key, T target) { + + if (target == null || key == null) { + return target; + } + + RedisPersistentEntity entity = this.converter.getMappingContext().getPersistentEntity(target.getClass()); + if (entity.hasExplictTimeToLiveProperty()) { + + PersistentProperty ttlProperty = entity.getExplicitTimeToLiveProperty(); + + final TimeToLive ttl = ttlProperty.findAnnotation(TimeToLive.class); + + Long timeout = redisOps.execute(new RedisCallback() { + + @Override + public Long doInRedis(RedisConnection connection) throws DataAccessException { + + if (ObjectUtils.nullSafeEquals(TimeUnit.SECONDS, ttl.unit())) { + return connection.ttl(key); + } + + return connection.pTtl(key, ttl.unit()); + } + }); + + if (timeout != null || !ttlProperty.getType().isPrimitive()) { + entity.getPropertyAccessor(target).setProperty(ttlProperty, + converter.getConversionService().convert(timeout, ttlProperty.getType())); + } + } + + return target; + } + /** * Configure usage of {@link KeyExpirationEventMessageListener}. * diff --git a/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java b/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java index b2579f32b3..1507fed615 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/BasicRedisPersistentEntity.java @@ -20,6 +20,7 @@ import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.redis.core.TimeToLive; import org.springframework.data.redis.core.TimeToLiveAccessor; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -59,6 +60,24 @@ public TimeToLiveAccessor getTimeToLiveAccessor() { return this.timeToLiveAccessor; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.mapping.RedisPersistentEntity#hasExplictTimeToLiveProperty() + */ + @Override + public boolean hasExplictTimeToLiveProperty() { + return getExplicitTimeToLiveProperty() != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.mapping.RedisPersistentEntity#getExplicitTimeToLiveProperty() + */ + @Override + public RedisPersistentProperty getExplicitTimeToLiveProperty() { + return (RedisPersistentProperty) this.getPersistentProperty(TimeToLive.class); + } + /* * (non-Javadoc) * @see org.springframework.data.mapping.model.BasicPersistentEntity#returnPropertyIfBetterIdPropertyCandidateOrNull(org.springframework.data.mapping.PersistentProperty) diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java index f4c333fd26..886a7ba177 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java @@ -282,9 +282,9 @@ public Long getTimeToLive(final Object source) { } else if (ttlProperty != null) { RedisPersistentEntity entity = mappingContext.getPersistentEntity(type); - Long timeout = (Long) entity.getPropertyAccessor(source).getProperty(ttlProperty); + Number timeout = (Number) entity.getPropertyAccessor(source).getProperty(ttlProperty); if (timeout != null) { - return TimeUnit.SECONDS.convert(timeout, unit); + return TimeUnit.SECONDS.convert(timeout.longValue(), unit); } } else { diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java index 14c6470a4d..1997c0df27 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisPersistentEntity.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. @@ -17,6 +17,7 @@ import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.redis.core.TimeToLiveAccessor; /** @@ -35,4 +36,18 @@ public interface RedisPersistentEntity extends KeyValuePersistentEntity { */ TimeToLiveAccessor getTimeToLiveAccessor(); + /** + * @return {@literal true} when a property is annotated with {@link org.springframework.data.redis.core.TimeToLive}. + * @since 1.8 + */ + boolean hasExplictTimeToLiveProperty(); + + /** + * Get the {@link PersistentProperty} that is annotated with {@link org.springframework.data.redis.core.TimeToLive}. + * + * @return can be {@null}. + * @since 1.8 + */ + PersistentProperty> getExplicitTimeToLiveProperty(); + } diff --git a/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java b/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java index 35aa506b4c..897de3facd 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisKeyValueTemplateTests.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. @@ -16,6 +16,7 @@ package org.springframework.data.redis.core; import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.number.IsCloseTo.*; import static org.junit.Assert.*; import java.util.ArrayList; @@ -43,10 +44,14 @@ import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.util.ObjectUtils; +import lombok.Data; import lombok.EqualsAndHashCode; /** + * Integration tests for {@link RedisKeyValueTemplate}. + * * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(Parameterized.class) public class RedisKeyValueTemplateTests { @@ -865,6 +870,91 @@ public Void doInRedis(RedisConnection connection) throws DataAccessException { }); } + /** + * @see DATAREDIS-523 + */ + @Test + public void shouldReadBackExplicitTimeToLive() throws InterruptedException { + + WithTtl source = new WithTtl(); + source.id = "ttl-1"; + source.ttl = 5L; + source.value = "5 seconds"; + + template.insert(source); + + Thread.sleep(1100); + + WithTtl target = template.findById(source.id, WithTtl.class); + assertThat(target.ttl, is(notNullValue())); + assertThat(target.ttl.doubleValue(), is(closeTo(3D, 1D))); + } + + /** + * @see DATAREDIS-523 + */ + @Test + public void shouldReadBackExplicitTimeToLiveToPrimitiveField() throws InterruptedException { + + WithPrimitiveTtl source = new WithPrimitiveTtl(); + source.id = "ttl-1"; + source.ttl = 5; + source.value = "5 seconds"; + + template.insert(source); + + Thread.sleep(1100); + + WithPrimitiveTtl target = template.findById(source.id, WithPrimitiveTtl.class); + assertThat((double) target.ttl, is(closeTo(3D, 1D))); + } + + /** + * @see DATAREDIS-523 + */ + @Test + public void shouldReadBackExplicitTimeToLiveWhenFetchingList() throws InterruptedException { + + WithTtl source = new WithTtl(); + source.id = "ttl-1"; + source.ttl = 5L; + source.value = "5 seconds"; + + template.insert(source); + + Thread.sleep(1100); + + WithTtl target = template.findAll(WithTtl.class).iterator().next(); + + assertThat(target.ttl, is(notNullValue())); + assertThat(target.ttl.doubleValue(), is(closeTo(3D, 1D))); + } + + /** + * @see DATAREDIS-523 + */ + @Test + public void shouldReadBackExplicitTimeToLiveAndSetItToMinusOnelIfPersisted() throws InterruptedException { + + WithTtl source = new WithTtl(); + source.id = "ttl-1"; + source.ttl = 5L; + source.value = "5 seconds"; + + template.insert(source); + + nativeTemplate.execute(new RedisCallback() { + + @Override + public Boolean doInRedis(RedisConnection connection) throws DataAccessException { + return connection.persist((WithTtl.class.getName() + ":ttl-1").getBytes()); + } + }); + + WithTtl target = template.findById(source.id, WithTtl.class); + assertThat(target.ttl, is(-1L)); + } + @EqualsAndHashCode @RedisHash("template-test-type-mapping") static class VariousTypes { @@ -973,4 +1063,20 @@ public String toString() { } } + + @Data + static class WithTtl { + + @Id String id; + String value; + @TimeToLive Long ttl; + } + + @Data + static class WithPrimitiveTtl { + + @Id String id; + String value; + @TimeToLive int ttl; + } }