Skip to content

Commit 20a7d3f

Browse files
Introduce HSETEX command to the Spring Data Redis framework
1 parent cb97e58 commit 20a7d3f

23 files changed

+1163
-7
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.commons.logging.Log;
2525
import org.apache.commons.logging.LogFactory;
2626
import org.jetbrains.annotations.NotNull;
27+
import org.jspecify.annotations.NonNull;
2728
import org.jspecify.annotations.NullUnmarked;
2829
import org.jspecify.annotations.Nullable;
2930
import org.springframework.core.convert.converter.Converter;
@@ -1620,6 +1621,11 @@ public List<String> hGetEx(String key, Expiration expiration, String... fields)
16201621
return convertAndReturn(delegate.hGetEx(serialize(key), expiration, serializeMulti(fields)), byteListToStringList);
16211622
}
16221623

1624+
@Override
1625+
public Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, Expiration expiration) {
1626+
return convertAndReturn(delegate.hSetEx(serialize(key), serialize(hashes), condition, expiration), Converters.identityConverter());
1627+
}
1628+
16231629
@Override
16241630
public Long incr(String key) {
16251631
return incr(serialize(key));
@@ -2603,6 +2609,11 @@ public List<byte[]> hGetEx(@NotNull byte[] key, Expiration expiration, @NotNull
26032609
return convertAndReturn(delegate.hGetEx(key, expiration, fields), Converters.identityConverter());
26042610
}
26052611

2612+
@Override
2613+
public Boolean hSetEx(@NotNull byte[] key, @NonNull Map<byte[], byte[]> hashes, HashFieldSetOption condition, Expiration expiration) {
2614+
return convertAndReturn(delegate.hSetEx(key, hashes, condition, expiration), Converters.identityConverter());
2615+
}
2616+
26062617
public @Nullable List<Long> applyExpiration(String key,
26072618
org.springframework.data.redis.core.types.Expiration expiration,
26082619
ExpirationOptions options, String... fields) {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,6 +1608,13 @@ default List<byte[]> hGetEx(byte[] key, Expiration expiration, byte[]... fields)
16081608
return hashCommands().hGetEx(key, expiration, fields);
16091609
}
16101610

1611+
/** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */
1612+
@Override
1613+
@Deprecated
1614+
default Boolean hSetEx(byte[] key, Map<byte[], byte[]> hashes, HashFieldSetOption condition, Expiration expiration) {
1615+
return hashCommands().hSetEx(key, hashes, condition, expiration);
1616+
}
1617+
16111618
/** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */
16121619
@Override
16131620
@Deprecated

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,4 +1410,101 @@ default Mono<List<ByteBuffer>> hGetEx(ByteBuffer key, Expiration expiration, Lis
14101410
* @see <a href="https://redis.io/commands/hgetex">Redis Documentation: HGETEX</a>
14111411
*/
14121412
Flux<MultiValueResponse<HGetExCommand, ByteBuffer>> hGetEx(Publisher<HGetExCommand> commands);
1413+
1414+
/**
1415+
* {@literal HSETEX} {@link Command}.
1416+
*
1417+
* @author Viktoriya Kutsarova
1418+
* @see <a href="https://redis.io/commands/hsetex">Redis Documentation: HSETEX</a>
1419+
*/
1420+
class HSetExCommand extends KeyCommand {
1421+
1422+
private final Map<ByteBuffer, ByteBuffer> fieldValueMap;
1423+
private final RedisHashCommands.HashFieldSetOption condition;
1424+
private final Expiration expiration;
1425+
1426+
private HSetExCommand(@Nullable ByteBuffer key, Map<ByteBuffer, ByteBuffer> fieldValueMap,
1427+
RedisHashCommands.HashFieldSetOption condition, Expiration expiration) {
1428+
super(key);
1429+
this.fieldValueMap = fieldValueMap;
1430+
this.condition = condition;
1431+
this.expiration = expiration;
1432+
}
1433+
1434+
/**
1435+
* Creates a new {@link HSetExCommand} for setting field-value pairs with condition and expiration.
1436+
*
1437+
* @param fieldValueMap the field-value pairs to set; must not be {@literal null}.
1438+
* @param condition the condition for setting fields; must not be {@literal null}.
1439+
* @param expiration the expiration to apply; must not be {@literal null}.
1440+
* @return new instance of {@link HSetExCommand}.
1441+
*/
1442+
public static HSetExCommand setWithConditionAndExpiration(Map<ByteBuffer, ByteBuffer> fieldValueMap,
1443+
RedisHashCommands.HashFieldSetOption condition, Expiration expiration) {
1444+
return new HSetExCommand(null, fieldValueMap, condition, expiration);
1445+
}
1446+
1447+
/**
1448+
* Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties.
1449+
*
1450+
* @param key must not be {@literal null}.
1451+
* @return a new {@link HSetExCommand} with {@literal key} applied.
1452+
*/
1453+
public HSetExCommand from(ByteBuffer key) {
1454+
Assert.notNull(key, "Key must not be null");
1455+
return new HSetExCommand(key, fieldValueMap, condition, expiration);
1456+
}
1457+
1458+
/**
1459+
* @return the field-value map.
1460+
*/
1461+
public Map<ByteBuffer, ByteBuffer> getFieldValueMap() {
1462+
return fieldValueMap;
1463+
}
1464+
1465+
/**
1466+
* @return the condition for setting fields.
1467+
*/
1468+
public RedisHashCommands.HashFieldSetOption getCondition() {
1469+
return condition;
1470+
}
1471+
1472+
/**
1473+
* @return the expiration to apply.
1474+
*/
1475+
public Expiration getExpiration() {
1476+
return expiration;
1477+
}
1478+
}
1479+
1480+
/**
1481+
* Set field-value pairs in hash at {@literal key} with condition and expiration.
1482+
*
1483+
* @param key must not be {@literal null}.
1484+
* @param fieldValueMap the field-value pairs to set; must not be {@literal null}.
1485+
* @param condition the condition for setting fields; must not be {@literal null}.
1486+
* @param expiration the expiration to apply; must not be {@literal null}.
1487+
* @return never {@literal null}.
1488+
* @see <a href="https://redis.io/commands/hsetex">Redis Documentation: HSETEX</a>
1489+
*/
1490+
default Mono<Boolean> hSetEx(ByteBuffer key, Map<ByteBuffer, ByteBuffer> fieldValueMap,
1491+
RedisHashCommands.HashFieldSetOption condition, Expiration expiration) {
1492+
1493+
Assert.notNull(key, "Key must not be null");
1494+
Assert.notNull(fieldValueMap, "Field-value map must not be null");
1495+
Assert.notNull(condition, "Condition must not be null");
1496+
Assert.notNull(expiration, "Expiration must not be null");
1497+
1498+
return hSetEx(Mono.just(HSetExCommand.setWithConditionAndExpiration(fieldValueMap, condition, expiration).from(key)))
1499+
.next().map(CommandResponse::getOutput);
1500+
}
1501+
1502+
/**
1503+
* Set field-value pairs in hash at {@literal key} with condition and expiration.
1504+
*
1505+
* @param commands must not be {@literal null}.
1506+
* @return never {@literal null}.
1507+
* @see <a href="https://redis.io/commands/hsetex">Redis Documentation: HSETEX</a>
1508+
*/
1509+
Flux<BooleanResponse<HSetExCommand>> hSetEx(Publisher<HSetExCommand> commands);
14131510
}

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,60 @@ default List<Long> hExpireAt(byte @NonNull [] key, long unixTime, byte @NonNull
568568
List<byte[]> hGetEx(byte @NonNull [] key, Expiration expiration,
569569
byte @NonNull [] @NonNull... fields);
570570

571+
/**
572+
* Set field-value pairs in hash at {@literal key} with optional condition and expiration.
573+
*
574+
* @param key must not be {@literal null}.
575+
* @param hashes the field-value pairs to set; must not be {@literal null}.
576+
* @param hashFieldSetOption the optional condition for setting fields.
577+
* @param expiration the optional expiration to apply.
578+
* @return never {@literal null}.
579+
* @see <a href="https://redis.io/commands/hsetex">Redis Documentation: HSETEX</a>
580+
*/
581+
Boolean hSetEx(byte @NonNull [] key, @NonNull Map<byte[], byte[]> hashes, HashFieldSetOption hashFieldSetOption,
582+
Expiration expiration);
583+
584+
/**
585+
* {@code HSETEX} command arguments for {@code FNX}, {@code FXX}.
586+
*
587+
* @author Viktoriya Kutsarova
588+
*/
589+
enum HashFieldSetOption {
590+
591+
/**
592+
* Do not set any additional command argument.
593+
*/
594+
UPSERT,
595+
596+
/**
597+
* {@code FNX}
598+
*/
599+
IF_NONE_EXIST,
600+
601+
/**
602+
* {@code FXX}
603+
*/
604+
IF_ALL_EXIST;
605+
606+
/**
607+
* Do not set any additional command argument.
608+
*/
609+
public static HashFieldSetOption upsert() {
610+
return UPSERT;
611+
}
612+
613+
/**
614+
* {@code FNX}
615+
*/
616+
public static HashFieldSetOption ifNoneExist() {
617+
return IF_NONE_EXIST;
618+
}
619+
620+
/**
621+
* {@code FXX}
622+
*/
623+
public static HashFieldSetOption ifAllExist() {
624+
return IF_ALL_EXIST;
625+
}
626+
}
571627
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2576,6 +2576,20 @@ List<Long> hpExpireAt(@NonNull String key, long unixTimeInMillis, ExpirationOpti
25762576
*/
25772577
List<String> hGetEx(@NonNull String key, Expiration expiration, @NonNull String @NonNull... fields);
25782578

2579+
/**
2580+
* Set field-value pairs in hash at {@literal key} with optional condition and expiration.
2581+
*
2582+
* @param key must not be {@literal null}.
2583+
* @param hashes the field-value pairs to set; must not be {@literal null}.
2584+
* @param condition the optional condition for setting fields.
2585+
* @param expiration the optional expiration to apply.
2586+
* @return never {@literal null}.
2587+
* @see <a href="https://redis.io/commands/hsetex">Redis Documentation: HSETEX</a>
2588+
* @see RedisHashCommands#hSetEx(byte[], Map, HashFieldSetOption, Expiration)
2589+
*/
2590+
Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition,
2591+
Expiration expiration);
2592+
25792593
// -------------------------------------------------------------------------
25802594
// Methods dealing with HyperLogLog
25812595
// -------------------------------------------------------------------------

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,19 @@ public List<byte[]> hGetEx(byte[] key, Expiration expiration, byte[]... fields)
441441
}
442442
}
443443

444+
@Override
445+
public Boolean hSetEx(byte[] key, Map<byte[], byte[]> hashes, HashFieldSetOption condition, Expiration expiration) {
446+
447+
Assert.notNull(key, "Key must not be null");
448+
Assert.notNull(hashes, "Fields must not be null");
449+
450+
try {
451+
return JedisConverters.toBoolean(connection.getCluster().hsetex(key, JedisConverters.toHSetExParams(condition, expiration), hashes));
452+
} catch (Exception ex) {
453+
throw convertJedisAccessException(ex);
454+
}
455+
}
456+
444457
@Nullable
445458
@Override
446459
public Long hStrLen(byte[] key, byte[] field) {

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

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.redis.connection.jedis;
1717

18+
import org.springframework.data.redis.connection.*;
1819
import redis.clients.jedis.GeoCoordinate;
1920
import redis.clients.jedis.HostAndPort;
2021
import redis.clients.jedis.Protocol;
@@ -47,26 +48,19 @@
4748
import org.springframework.data.geo.Metric;
4849
import org.springframework.data.geo.Metrics;
4950
import org.springframework.data.geo.Point;
50-
import org.springframework.data.redis.connection.BitFieldSubCommands;
5151
import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy;
5252
import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldSet;
5353
import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldSubCommand;
54-
import org.springframework.data.redis.connection.RedisGeoCommands;
5554
import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit;
5655
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
5756
import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs;
5857
import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.Flag;
5958
import org.springframework.data.redis.connection.RedisListCommands.Position;
60-
import org.springframework.data.redis.connection.RedisNode;
61-
import org.springframework.data.redis.connection.RedisServer;
62-
import org.springframework.data.redis.connection.RedisServerCommands;
6359
import org.springframework.data.redis.connection.RedisStringCommands.BitOperation;
6460
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
6561
import org.springframework.data.redis.connection.RedisZSetCommands.ZAddArgs;
66-
import org.springframework.data.redis.connection.SortParameters;
6762
import org.springframework.data.redis.connection.SortParameters.Order;
6863
import org.springframework.data.redis.connection.SortParameters.Range;
69-
import org.springframework.data.redis.connection.ValueEncoding;
7064
import org.springframework.data.redis.connection.convert.Converters;
7165
import org.springframework.data.redis.connection.convert.ListConverter;
7266
import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter;
@@ -392,6 +386,67 @@ static GetExParams toGetExParams(Expiration expiration, GetExParams params) {
392386
: params.ex(expiration.getConverted(TimeUnit.SECONDS));
393387
}
394388

389+
/**
390+
* Converts a given {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to the according
391+
* {@code HSETEX} command argument.
392+
* <dl>
393+
* <dt>{@link RedisHashCommands.HashFieldSetOption#ifNoneExist()}</dt>
394+
* <dd>{@code FNX}</dd>
395+
* <dt>{@link RedisHashCommands.HashFieldSetOption#ifAllExist()}</dt>
396+
* <dd>{@code FXX}</dd>
397+
* <dt>{@link RedisHashCommands.HashFieldSetOption#upsert()}</dt>
398+
* <dd>no condition flag</dd>
399+
* </dl>
400+
* <dl>
401+
* <dt>{@link TimeUnit#MILLISECONDS}</dt>
402+
* <dd>{@code PX|PXAT}</dd>
403+
* <dt>{@link TimeUnit#SECONDS}</dt>
404+
* <dd>{@code EX|EXAT}</dd>
405+
* </dl>
406+
*
407+
* @param condition can be {@literal null}.
408+
* @param expiration can be {@literal null}.
409+
* @since 4.0
410+
*/
411+
static HSetExParams toHSetExParams(RedisHashCommands.@Nullable HashFieldSetOption condition, @Nullable Expiration expiration) {
412+
return toHSetExParams(condition, expiration, new HSetExParams());
413+
}
414+
415+
static HSetExParams toHSetExParams(RedisHashCommands.@Nullable HashFieldSetOption condition, @Nullable Expiration expiration, HSetExParams params) {
416+
417+
if (condition == null && expiration == null) {
418+
return params;
419+
}
420+
421+
if (condition != null) {
422+
if (condition.equals(RedisHashCommands.HashFieldSetOption.ifNoneExist())) {
423+
params.fnx();
424+
} else if (condition.equals(RedisHashCommands.HashFieldSetOption.ifAllExist())) {
425+
params.fxx();
426+
}
427+
}
428+
429+
if (expiration == null) {
430+
return params;
431+
}
432+
433+
if (expiration.isKeepTtl()) {
434+
return params.keepTtl();
435+
}
436+
437+
if (expiration.isPersistent()) {
438+
return params;
439+
}
440+
441+
if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) {
442+
return expiration.isUnixTimestamp() ? params.pxAt(expiration.getExpirationTime())
443+
: params.px(expiration.getExpirationTime());
444+
}
445+
446+
return expiration.isUnixTimestamp() ? params.exAt(expiration.getConverted(TimeUnit.SECONDS))
447+
: params.ex(expiration.getConverted(TimeUnit.SECONDS));
448+
}
449+
395450
/**
396451
* Converts a given {@link Expiration} to the according {@code HGETEX} command argument depending on
397452
* {@link Expiration#isUnixTimestamp()}.
@@ -411,6 +466,10 @@ static HGetExParams toHGetExParams(Expiration expiration) {
411466

412467
static HGetExParams toHGetExParams(Expiration expiration, HGetExParams params) {
413468

469+
if (expiration == null) {
470+
return params;
471+
}
472+
414473
if (expiration.isPersistent()) {
415474
return params.persist();
416475
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,18 @@ public List<byte[]> hGetEx(byte @NonNull [] key, Expiration expiration, byte @No
351351
return connection.invoke().just(Jedis::hgetex, PipelineBinaryCommands::hgetex, key, JedisConverters.toHGetExParams(expiration), fields);
352352
}
353353

354+
@Override
355+
public Boolean hSetEx(byte @NonNull [] key, @NonNull Map<byte[], byte[]> hashes, HashFieldSetOption condition,
356+
Expiration expiration) {
357+
358+
Assert.notNull(key, "Key must not be null");
359+
Assert.notNull(hashes, "Hashes must not be null");
360+
361+
return connection.invoke().from(Jedis::hsetex, PipelineBinaryCommands::hsetex, key,
362+
JedisConverters.toHSetExParams(condition, expiration), hashes)
363+
.get(Converters::toBoolean);
364+
}
365+
354366
@Nullable
355367
@Override
356368
public Long hStrLen(byte[] key, byte[] field) {

0 commit comments

Comments
 (0)