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() {