diff --git a/pom.xml b/pom.xml index 2252974424..2523d66767 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 3.4.0-SNAPSHOT + 3.4.0-GH-2853-SNAPSHOT Spring Data Redis Spring Data module for Redis diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 80cc128e55..ebe1aae9bd 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -770,6 +770,11 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op return convertAndReturn(delegate.set(key, value, expiration, option), Converters.identityConverter()); } + @Override + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + return convertAndReturn(delegate.setGet(key, value, expiration, option), Converters.identityConverter()); + } + @Override public Boolean setBit(byte[] key, long offset, boolean value) { return convertAndReturn(delegate.setBit(key, offset, value), Converters.identityConverter()); diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index aaeaafe18b..9aa5c1b0c9 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -326,6 +326,13 @@ default Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption o return stringCommands().set(key, value, expiration, option); } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ + @Override + @Deprecated + default byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + return stringCommands().setGet(key, value, expiration, option); + } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ @Override @Deprecated diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java index 493b055dae..20ddbfd092 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java @@ -47,6 +47,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Marcin Grzejszczak * @since 2.0 */ public interface ReactiveStringCommands { @@ -193,6 +194,41 @@ default Mono set(ByteBuffer key, ByteBuffer value, Expiration expiratio */ Flux> set(Publisher commands); + /** + * Set {@literal value} for {@literal key} with {@literal expiration} and {@literal options}. Return the old + * string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value + * stored at key is not a string. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} for no expiration time or + * {@link Expiration#keepTtl()} to keep the existing. + * @param option must not be {@literal null}. + * @return + * @see Redis Documentation: SET + * @since 3.4 + */ + @Nullable + default Mono setGet(ByteBuffer key, ByteBuffer value, Expiration expiration, SetOption option) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + + return setGet(Mono.just(SetCommand.set(key).value(value).withSetOption(option).expiring(expiration))).next() + .map(CommandResponse::getOutput); + } + + /** + * Set each and every item separately by invoking {@link SetCommand}. Return the old + * string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value + * stored at key is not a string. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link ByteBufferResponse} holding the {@link SetCommand} along with the command result. + * @see Redis Documentation: SET + */ + Flux> setGet(Publisher commands); + /** * Get single element stored at {@literal key}. * diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java index beac673984..663a3b9d34 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java @@ -122,6 +122,22 @@ enum BitOperation { @Nullable Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option); + /** + * Set {@code value} for {@code key}. Return the old string stored at key, or nil if key did not exist. + * An error is returned and SET aborted if the value stored at key is not a string. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} to not set any ttl or + * {@link Expiration#keepTtl()} to keep the existing expiration. + * @param option must not be {@literal null}. Use {@link SetOption#upsert()} to add non existing. + * @return {@literal null} when used in pipeline / transaction. + * @since 3.4 + * @see Redis Documentation: SET + */ + @Nullable + byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option); + /** * Set {@code value} for {@code key}, only if {@code key} does not exist. * diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java index 514ce81579..6b7213467b 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java @@ -150,6 +150,24 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op } } + @Override + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + Assert.notNull(expiration, "Expiration must not be null"); + Assert.notNull(option, "Option must not be null"); + + SetParams setParams = JedisConverters.toSetCommandExPxArgument(expiration, + JedisConverters.toSetCommandNxXxArgument(option)); + + try { + return connection.getCluster().setGet(key, value, setParams); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + @Override public Boolean setNX(byte[] key, byte[] value) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java index 889e87b102..fb65ad24ab 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java @@ -116,6 +116,20 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op .getOrElse(Converters.stringToBooleanConverter(), () -> false); } + @Override + @Nullable + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + Assert.notNull(expiration, "Expiration must not be null"); + Assert.notNull(option, "Option must not be null"); + + SetParams params = JedisConverters.toSetCommandExPxArgument(expiration, + JedisConverters.toSetCommandNxXxArgument(option)); + + return connection.invoke().just(Jedis::setGet, PipelineBinaryCommands::setGet, key, value, params); + } + @Override public Boolean setNX(byte[] key, byte[] value) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java index 7beb8a3bd2..fe96034b75 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java @@ -105,6 +105,19 @@ public Flux> set(Publisher commands) { })); } + @Override + public Flux> setGet(Publisher commands) { + return this.connection.execute(reactiveCommands -> Flux.from(commands).concatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getValue(), "Value must not be null"); + + return reactiveCommands.setGet(command.getKey(), command.getValue()) + .map(v -> new ByteBufferResponse<>(command, v)) + .defaultIfEmpty(new AbsentByteBufferResponse<>(command)); + })); + } + @Override public Flux> getSet(Publisher commands) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java index 1397fc9ce7..2716232323 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java @@ -115,6 +115,18 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op .orElse(LettuceConverters.stringToBooleanConverter(), false); } + @Override + @Nullable + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + Assert.notNull(expiration, "Expiration must not be null"); + Assert.notNull(option, "Option must not be null"); + + return connection.invoke() + .just(RedisStringAsyncCommands::setGet, key, value, LettuceConverters.toSetArgs(expiration, option)); + } + @Override public Boolean setNX(byte[] key, byte[] value) { diff --git a/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java b/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java index b11d7c6657..c9f8d7efd6 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundValueOperations.java @@ -49,6 +49,19 @@ public interface BoundValueOperations extends BoundKeyOperations { */ void set(V value, long timeout, TimeUnit unit); + /** + * Set the {@code value} and expiration {@code timeout} for the bound key. Return the old + * string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value + * stored at key is not a string. + * + * @param value must not be {@literal null}. + * @param timeout + * @param unit must not be {@literal null}. + * @see Redis Documentation: SET + * @since 3.4 + */ + V setGet(V value, long timeout, TimeUnit unit); + /** * Set the {@code value} and expiration {@code timeout} for the bound key. * diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java index 0f0ac35200..274ee6895b 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java @@ -77,6 +77,18 @@ public Mono set(K key, V value, Duration timeout) { stringCommands.set(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT)); } + @Override + public Mono setGet(K key, V value, Duration timeout) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(value, "Value must not be null"); + Assert.notNull(timeout, "Duration must not be null"); + + return createMono(stringCommands -> + stringCommands.setGet(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT)) + .map(this::readRequiredValue); + } + @Override public Mono setIfAbsent(K key, V value) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java index 9938d5a0ec..aacf342bf6 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java @@ -281,6 +281,27 @@ private boolean failsafeInvokePsetEx(RedisConnection connection) { }); } + @Override + public V setGet(K key, V value, long timeout, TimeUnit unit) { + return doSetGet(key, value, Expiration.from(timeout, unit)); + } + + @Override + public V setGet(K key, V value, Duration duration) { + return doSetGet(key, value, Expiration.from(duration)); + } + + private V doSetGet(K key, V value, Expiration duration) { + byte[] rawValue = rawValue(value); + return execute( new ValueDeserializingRedisCallback(key) { + + @Override + protected byte[] inRedis(byte[] rawKey, RedisConnection connection) { + return connection.stringCommands().setGet(rawKey, rawValue, duration, SetOption.UPSERT); + } + }); + } + @Override public Boolean setIfAbsent(K key, V value) { diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java index 2a0f44404c..2a9f1933cc 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java @@ -58,6 +58,18 @@ public interface ReactiveValueOperations { */ Mono set(K key, V value, Duration timeout); + /** + * Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old + * string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value + * stored at key is not a string. + * + * @param key must not be {@literal null}. + * @param value + * @param timeout must not be {@literal null}. + * @see Redis Documentation: SETEX + */ + Mono setGet(K key, V value, Duration timeout); + /** * Set {@code key} to hold the string {@code value} if {@code key} is absent. * diff --git a/src/main/java/org/springframework/data/redis/core/ValueOperations.java b/src/main/java/org/springframework/data/redis/core/ValueOperations.java index 546a83f8ff..229dddf81c 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperations.java @@ -32,6 +32,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Jiahe Cai + * @author Marcin Grzejszczak */ public interface ValueOperations { @@ -44,6 +45,33 @@ public interface ValueOperations { */ void set(K key, V value); + /** + * Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old + * string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value + * stored at key is not a string. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param timeout the key expiration timeout. + * @param unit must not be {@literal null}. + * @see Redis Documentation: SET + * @since 3.4 + */ + V setGet(K key, V value, long timeout, TimeUnit unit); + + /** + * Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old + * string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value + * stored at key is not a string. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param duration expiration duration + * @see Redis Documentation: SET + * @since 3.4 + */ + V setGet(K key, V value, Duration duration); + /** * Set the {@code value} and expiration {@code timeout} for {@code key}. * diff --git a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java index 2a10866752..b385a6f7da 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -1093,6 +1093,11 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op return delegate.set(key, value, expiration, options); } + @Override + public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) { + return delegate.setGet(key, value, expiration, option); + } + @Override public List bitField(byte[] key, BitFieldSubCommands subCommands) { return delegate.bitField(key, subCommands); diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java index 15548cfc22..d6348abfda 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java @@ -556,4 +556,29 @@ void setKeepTTL() { assertThat(nativeBinaryCommands.ttl(KEY_1_BBUFFER)).isCloseTo(expireSeconds, Offset.offset(5L)); assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2); } + + @ParameterizedRedisTest // GH-2853 + void setGetMono() { + nativeCommands.set(KEY_1, VALUE_1); + + connection.stringCommands().setGet(KEY_1_BBUFFER, VALUE_2_BBUFFER, Expiration.keepTtl(), SetOption.upsert()) + .as(StepVerifier::create) // + .expectNext(VALUE_1_BBUFFER) // + .verifyComplete(); + + assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2); + } + + @ParameterizedRedisTest // GH-2853 + void setGetFlux() { + nativeCommands.set(KEY_1, VALUE_1); + + connection.stringCommands().setGet(Mono.just(SetCommand.set(KEY_1_BBUFFER).value(VALUE_2_BBUFFER).expiring(Expiration.keepTtl()).withSetOption( SetOption.upsert()))) + .map(CommandResponse::getOutput) + .as(StepVerifier::create) // + .expectNext(VALUE_1_BBUFFER) // + .verifyComplete(); + + assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2); + } } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java index e539412d5a..85ab35f5e9 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java @@ -316,6 +316,32 @@ void testSetWithExpirationWithTimeUnitMilliseconds() { await().atMost(Duration.ofMillis(500L)).until(() -> !redisTemplate.hasKey(key)); } + @ParameterizedRedisTest + void testSetGetWithExpiration() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOps.set(key, value1); + + assertThat(valueOps.setGet(key, value2, 1, TimeUnit.SECONDS)).isEqualTo(value1); + assertThat(valueOps.get(key)).isEqualTo(value2); + } + + @ParameterizedRedisTest + void testSetGetWithExpirationDuration() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + valueOps.set(key, value1); + + assertThat(valueOps.setGet(key, value2, Duration.ofMillis(1000))).isEqualTo(value1); + assertThat(valueOps.get(key)).isEqualTo(value2); + } + @ParameterizedRedisTest void testAppend() { diff --git a/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java index bbc18c6f0e..7fc069eeb4 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java @@ -571,6 +571,23 @@ void testGetExpireMillis() { assertThat(ttl).isLessThan(25L); } + @ParameterizedRedisTest // GH-3017 + void testSetGetExpireMillis() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + redisTemplate.boundValueOps(key).set(value1); + + V oldValue = redisTemplate.boundValueOps(key).setGet(value2, 1, TimeUnit.DAYS); + redisTemplate.expire(key, 1, TimeUnit.DAYS); + Long ttl = redisTemplate.getExpire(key, TimeUnit.HOURS); + + assertThat(oldValue).isEqualTo(value1); + assertThat(ttl).isGreaterThanOrEqualTo(23L); + assertThat(ttl).isLessThan(25L); + } + @ParameterizedRedisTest // DATAREDIS-611 void testGetExpireDuration() {