Skip to content

Commit cdf9dc3

Browse files
marcingrzejszczakmp911de
authored andcommitted
Adds support for Redis SET … GET command.
Closes #2853 Original pull request: #3017
1 parent ea4d728 commit cdf9dc3

17 files changed

+280
-0
lines changed

src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java

+5
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,11 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
770770
return convertAndReturn(delegate.set(key, value, expiration, option), Converters.identityConverter());
771771
}
772772

773+
@Override
774+
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
775+
return convertAndReturn(delegate.setGet(key, value, expiration, option), Converters.identityConverter());
776+
}
777+
773778
@Override
774779
public Boolean setBit(byte[] key, long offset, boolean value) {
775780
return convertAndReturn(delegate.setBit(key, offset, value), Converters.identityConverter());

src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java

+7
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ default Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption o
326326
return stringCommands().set(key, value, expiration, option);
327327
}
328328

329+
/** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */
330+
@Override
331+
@Deprecated
332+
default byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
333+
return stringCommands().setGet(key, value, expiration, option);
334+
}
335+
329336
/** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */
330337
@Override
331338
@Deprecated

src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java

+36
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
*
4848
* @author Christoph Strobl
4949
* @author Mark Paluch
50+
* @author Marcin Grzejszczak
5051
* @since 2.0
5152
*/
5253
public interface ReactiveStringCommands {
@@ -193,6 +194,41 @@ default Mono<Boolean> set(ByteBuffer key, ByteBuffer value, Expiration expiratio
193194
*/
194195
Flux<BooleanResponse<SetCommand>> set(Publisher<SetCommand> commands);
195196

197+
/**
198+
* Set {@literal value} for {@literal key} with {@literal expiration} and {@literal options}. Return the old
199+
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
200+
* stored at key is not a string.
201+
*
202+
* @param key must not be {@literal null}.
203+
* @param value must not be {@literal null}.
204+
* @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} for no expiration time or
205+
* {@link Expiration#keepTtl()} to keep the existing.
206+
* @param option must not be {@literal null}.
207+
* @return
208+
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
209+
* @since 3.4
210+
*/
211+
@Nullable
212+
default Mono<ByteBuffer> setGet(ByteBuffer key, ByteBuffer value, Expiration expiration, SetOption option) {
213+
214+
Assert.notNull(key, "Key must not be null");
215+
Assert.notNull(value, "Value must not be null");
216+
217+
return setGet(Mono.just(SetCommand.set(key).value(value).withSetOption(option).expiring(expiration))).next()
218+
.map(CommandResponse::getOutput);
219+
}
220+
221+
/**
222+
* Set each and every item separately by invoking {@link SetCommand}. Return the old
223+
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
224+
* stored at key is not a string.
225+
*
226+
* @param commands must not be {@literal null}.
227+
* @return {@link Flux} of {@link ByteBufferResponse} holding the {@link SetCommand} along with the command result.
228+
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
229+
*/
230+
Flux<ByteBufferResponse<SetCommand>> setGet(Publisher<SetCommand> commands);
231+
196232
/**
197233
* Get single element stored at {@literal key}.
198234
*

src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java

+16
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,22 @@ enum BitOperation {
122122
@Nullable
123123
Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option);
124124

125+
/**
126+
* Set {@code value} for {@code key}. Return the old string stored at key, or nil if key did not exist.
127+
* An error is returned and SET aborted if the value stored at key is not a string.
128+
*
129+
* @param key must not be {@literal null}.
130+
* @param value must not be {@literal null}.
131+
* @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} to not set any ttl or
132+
* {@link Expiration#keepTtl()} to keep the existing expiration.
133+
* @param option must not be {@literal null}. Use {@link SetOption#upsert()} to add non existing.
134+
* @return {@literal null} when used in pipeline / transaction.
135+
* @since 3.4
136+
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
137+
*/
138+
@Nullable
139+
byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option);
140+
125141
/**
126142
* Set {@code value} for {@code key}, only if {@code key} does not exist.
127143
*

src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java

+18
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,24 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
150150
}
151151
}
152152

153+
@Override
154+
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
155+
156+
Assert.notNull(key, "Key must not be null");
157+
Assert.notNull(value, "Value must not be null");
158+
Assert.notNull(expiration, "Expiration must not be null");
159+
Assert.notNull(option, "Option must not be null");
160+
161+
SetParams setParams = JedisConverters.toSetCommandExPxArgument(expiration,
162+
JedisConverters.toSetCommandNxXxArgument(option));
163+
164+
try {
165+
return connection.getCluster().setGet(key, value, setParams);
166+
} catch (Exception ex) {
167+
throw convertJedisAccessException(ex);
168+
}
169+
}
170+
153171
@Override
154172
public Boolean setNX(byte[] key, byte[] value) {
155173

src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java

+14
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
116116
.getOrElse(Converters.stringToBooleanConverter(), () -> false);
117117
}
118118

119+
@Override
120+
@Nullable
121+
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
122+
Assert.notNull(key, "Key must not be null");
123+
Assert.notNull(value, "Value must not be null");
124+
Assert.notNull(expiration, "Expiration must not be null");
125+
Assert.notNull(option, "Option must not be null");
126+
127+
SetParams params = JedisConverters.toSetCommandExPxArgument(expiration,
128+
JedisConverters.toSetCommandNxXxArgument(option));
129+
130+
return connection.invoke().just(Jedis::setGet, PipelineBinaryCommands::setGet, key, value, params);
131+
}
132+
119133
@Override
120134
public Boolean setNX(byte[] key, byte[] value) {
121135

src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java

+13
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,19 @@ public Flux<BooleanResponse<SetCommand>> set(Publisher<SetCommand> commands) {
105105
}));
106106
}
107107

108+
@Override
109+
public Flux<ByteBufferResponse<SetCommand>> setGet(Publisher<SetCommand> commands) {
110+
return this.connection.execute(reactiveCommands -> Flux.from(commands).concatMap((command) -> {
111+
112+
Assert.notNull(command.getKey(), "Key must not be null");
113+
Assert.notNull(command.getValue(), "Value must not be null");
114+
115+
return reactiveCommands.setGet(command.getKey(), command.getValue())
116+
.map(v -> new ByteBufferResponse<>(command, v))
117+
.defaultIfEmpty(new AbsentByteBufferResponse<>(command));
118+
}));
119+
}
120+
108121
@Override
109122
public Flux<ByteBufferResponse<SetCommand>> getSet(Publisher<SetCommand> commands) {
110123

src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java

+12
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
115115
.orElse(LettuceConverters.stringToBooleanConverter(), false);
116116
}
117117

118+
@Override
119+
@Nullable
120+
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
121+
Assert.notNull(key, "Key must not be null");
122+
Assert.notNull(value, "Value must not be null");
123+
Assert.notNull(expiration, "Expiration must not be null");
124+
Assert.notNull(option, "Option must not be null");
125+
126+
return connection.invoke()
127+
.just(RedisStringAsyncCommands::setGet, key, value, LettuceConverters.toSetArgs(expiration, option));
128+
}
129+
118130
@Override
119131
public Boolean setNX(byte[] key, byte[] value) {
120132

src/main/java/org/springframework/data/redis/core/BoundValueOperations.java

+13
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ public interface BoundValueOperations<K, V> extends BoundKeyOperations<K> {
4949
*/
5050
void set(V value, long timeout, TimeUnit unit);
5151

52+
/**
53+
* Set the {@code value} and expiration {@code timeout} for the bound key. Return the old
54+
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
55+
* stored at key is not a string.
56+
*
57+
* @param value must not be {@literal null}.
58+
* @param timeout
59+
* @param unit must not be {@literal null}.
60+
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
61+
* @since 3.4
62+
*/
63+
V setGet(V value, long timeout, TimeUnit unit);
64+
5265
/**
5366
* Set the {@code value} and expiration {@code timeout} for the bound key.
5467
*

src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java

+12
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ public Mono<Boolean> set(K key, V value, Duration timeout) {
7777
stringCommands.set(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT));
7878
}
7979

80+
@Override
81+
public Mono<V> setGet(K key, V value, Duration timeout) {
82+
83+
Assert.notNull(key, "Key must not be null");
84+
Assert.notNull(value, "Value must not be null");
85+
Assert.notNull(timeout, "Duration must not be null");
86+
87+
return createMono(stringCommands ->
88+
stringCommands.setGet(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT))
89+
.map(this::readRequiredValue);
90+
}
91+
8092
@Override
8193
public Mono<Boolean> setIfAbsent(K key, V value) {
8294

src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java

+21
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,27 @@ public void set(K key, V value, long timeout, TimeUnit unit) {
208208
execute(connection -> connection.set(rawKey, rawValue, Expiration.from(timeout, unit), SetOption.upsert()));
209209
}
210210

211+
@Override
212+
public V setGet(K key, V value, long timeout, TimeUnit unit) {
213+
return doSetGet(key, value, Expiration.from(timeout, unit));
214+
}
215+
216+
@Override
217+
public V setGet(K key, V value, Duration duration) {
218+
return doSetGet(key, value, Expiration.from(duration));
219+
}
220+
221+
private V doSetGet(K key, V value, Expiration duration) {
222+
byte[] rawValue = rawValue(value);
223+
return execute( new ValueDeserializingRedisCallback(key) {
224+
225+
@Override
226+
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
227+
return connection.stringCommands().setGet(rawKey, rawValue, duration, SetOption.UPSERT);
228+
}
229+
});
230+
}
231+
211232
@Override
212233
public Boolean setIfAbsent(K key, V value) {
213234

src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java

+12
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ public interface ReactiveValueOperations<K, V> {
5858
*/
5959
Mono<Boolean> set(K key, V value, Duration timeout);
6060

61+
/**
62+
* Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old
63+
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
64+
* stored at key is not a string.
65+
*
66+
* @param key must not be {@literal null}.
67+
* @param value
68+
* @param timeout must not be {@literal null}.
69+
* @see <a href="https://redis.io/commands/setex">Redis Documentation: SETEX</a>
70+
*/
71+
Mono<V> setGet(K key, V value, Duration timeout);
72+
6173
/**
6274
* Set {@code key} to hold the string {@code value} if {@code key} is absent.
6375
*

src/main/java/org/springframework/data/redis/core/ValueOperations.java

+28
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
* @author Christoph Strobl
3333
* @author Mark Paluch
3434
* @author Jiahe Cai
35+
* @author Marcin Grzejszczak
3536
*/
3637
public interface ValueOperations<K, V> {
3738

@@ -44,6 +45,33 @@ public interface ValueOperations<K, V> {
4445
*/
4546
void set(K key, V value);
4647

48+
/**
49+
* Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old
50+
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
51+
* stored at key is not a string.
52+
*
53+
* @param key must not be {@literal null}.
54+
* @param value must not be {@literal null}.
55+
* @param timeout the key expiration timeout.
56+
* @param unit must not be {@literal null}.
57+
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
58+
* @since 3.4
59+
*/
60+
V setGet(K key, V value, long timeout, TimeUnit unit);
61+
62+
/**
63+
* Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old
64+
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
65+
* stored at key is not a string.
66+
*
67+
* @param key must not be {@literal null}.
68+
* @param value must not be {@literal null}.
69+
* @param duration expiration duration
70+
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
71+
* @since 3.4
72+
*/
73+
V setGet(K key, V value, Duration duration);
74+
4775
/**
4876
* Set the {@code value} and expiration {@code timeout} for {@code key}.
4977
*

src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java

+5
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,11 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
10931093
return delegate.set(key, value, expiration, options);
10941094
}
10951095

1096+
@Override
1097+
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
1098+
return delegate.setGet(key, value, expiration, option);
1099+
}
1100+
10961101
@Override
10971102
public List<Long> bitField(byte[] key, BitFieldSubCommands subCommands) {
10981103
return delegate.bitField(key, subCommands);

src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsIntegrationTests.java

+25
Original file line numberDiff line numberDiff line change
@@ -556,4 +556,29 @@ void setKeepTTL() {
556556
assertThat(nativeBinaryCommands.ttl(KEY_1_BBUFFER)).isCloseTo(expireSeconds, Offset.offset(5L));
557557
assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2);
558558
}
559+
560+
@ParameterizedRedisTest // GH-2853
561+
void setGetMono() {
562+
nativeCommands.set(KEY_1, VALUE_1);
563+
564+
connection.stringCommands().setGet(KEY_1_BBUFFER, VALUE_2_BBUFFER, Expiration.keepTtl(), SetOption.upsert())
565+
.as(StepVerifier::create) //
566+
.expectNext(VALUE_1_BBUFFER) //
567+
.verifyComplete();
568+
569+
assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2);
570+
}
571+
572+
@ParameterizedRedisTest // GH-2853
573+
void setGetFlux() {
574+
nativeCommands.set(KEY_1, VALUE_1);
575+
576+
connection.stringCommands().setGet(Mono.just(SetCommand.set(KEY_1_BBUFFER).value(VALUE_2_BBUFFER).expiring(Expiration.keepTtl()).withSetOption( SetOption.upsert())))
577+
.map(CommandResponse::getOutput)
578+
.as(StepVerifier::create) //
579+
.expectNext(VALUE_1_BBUFFER) //
580+
.verifyComplete();
581+
582+
assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2);
583+
}
559584
}

src/test/java/org/springframework/data/redis/core/DefaultValueOperationsIntegrationTests.java

+26
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,32 @@ void testSetWithExpirationWithTimeUnitMilliseconds() {
316316
await().atMost(Duration.ofMillis(500L)).until(() -> !redisTemplate.hasKey(key));
317317
}
318318

319+
@ParameterizedRedisTest
320+
void testSetGetWithExpiration() {
321+
322+
K key = keyFactory.instance();
323+
V value1 = valueFactory.instance();
324+
V value2 = valueFactory.instance();
325+
326+
valueOps.set(key, value1);
327+
328+
assertThat(valueOps.setGet(key, value2, 1, TimeUnit.SECONDS)).isEqualTo(value1);
329+
assertThat(valueOps.get(key)).isEqualTo(value2);
330+
}
331+
332+
@ParameterizedRedisTest
333+
void testSetGetWithExpirationDuration() {
334+
335+
K key = keyFactory.instance();
336+
V value1 = valueFactory.instance();
337+
V value2 = valueFactory.instance();
338+
339+
valueOps.set(key, value1);
340+
341+
assertThat(valueOps.setGet(key, value2, Duration.ofMillis(1000))).isEqualTo(value1);
342+
assertThat(valueOps.get(key)).isEqualTo(value2);
343+
}
344+
319345
@ParameterizedRedisTest
320346
void testAppend() {
321347

0 commit comments

Comments
 (0)