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 6a7ad98f41..8d8249e6b4 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -23,7 +23,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; @@ -1617,20 +1616,20 @@ public List hVals(String key) { return convertAndReturn(delegate.hVals(serialize(key)), byteListToStringList); } - @Override - public List hGetDel(String key, String... fields) { - return convertAndReturn(delegate.hGetDel(serialize(key), serializeMulti(fields)), byteListToStringList); - } + @Override + public List hGetDel(String key, String... fields) { + return convertAndReturn(delegate.hGetDel(serialize(key), serializeMulti(fields)), byteListToStringList); + } - @Override - public List hGetEx(String key, Expiration expiration, String... fields) { - return convertAndReturn(delegate.hGetEx(serialize(key), expiration, serializeMulti(fields)), byteListToStringList); - } + @Override + public List hGetEx(String key, Expiration expiration, String... fields) { + return convertAndReturn(delegate.hGetEx(serialize(key), expiration, serializeMulti(fields)), byteListToStringList); + } - @Override - public Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, Expiration expiration) { - return convertAndReturn(delegate.hSetEx(serialize(key), serialize(hashes), condition, expiration), Converters.identityConverter()); - } + @Override + public Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, Expiration expiration) { + return convertAndReturn(delegate.hSetEx(serialize(key), serialize(hashes), condition, expiration), Converters.identityConverter()); + } @Override public Long incr(String key) { @@ -2610,20 +2609,21 @@ public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { return this.delegate.hTtl(key, timeUnit, fields); } - @Override - public List hGetDel(@NotNull byte[] key, @NotNull byte[]... fields) { - return convertAndReturn(delegate.hGetDel(key, fields), Converters.identityConverter()); - } + @Override + public List hGetDel(@NonNull byte[] key, @NonNull byte[]... fields) { + return convertAndReturn(delegate.hGetDel(key, fields), Converters.identityConverter()); + } - @Override - public List hGetEx(@NotNull byte[] key, Expiration expiration, @NotNull byte[]... fields) { - return convertAndReturn(delegate.hGetEx(key, expiration, fields), Converters.identityConverter()); - } + @Override + public List hGetEx(@NonNull byte[] key, @Nullable Expiration expiration, @NonNull byte[]... fields) { + return convertAndReturn(delegate.hGetEx(key, expiration, fields), Converters.identityConverter()); + } - @Override - public Boolean hSetEx(@NotNull byte[] key, @NonNull Map hashes, HashFieldSetOption condition, Expiration expiration) { - return convertAndReturn(delegate.hSetEx(key, hashes, condition, expiration), Converters.identityConverter()); - } + @Override + public Boolean hSetEx(@NonNull byte[] key, @NonNull Map hashes, @NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { + return convertAndReturn(delegate.hSetEx(key, hashes, condition, expiration), Converters.identityConverter()); + } public @Nullable List applyExpiration(String key, org.springframework.data.redis.core.types.Expiration expiration, 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 5a5e1daa57..ab0b6a78e3 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -23,6 +23,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Circle; @@ -1602,26 +1603,27 @@ default List hpTtl(byte[] key, byte[]... fields) { return hashCommands().hpTtl(key, fields); } - /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ - @Override - @Deprecated - default List hGetDel(byte[] key, byte[]... fields) { - return hashCommands().hGetDel(key, fields); - } - - /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ - @Override - @Deprecated - default List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { - return hashCommands().hGetEx(key, expiration, fields); - } - - /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ - @Override - @Deprecated - default Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { - return hashCommands().hSetEx(key, hashes, condition, expiration); - } + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hGetDel(byte[] key, byte[]... fields) { + return hashCommands().hGetDel(key, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hGetEx(byte[] key, @Nullable Expiration expiration, byte[]... fields) { + return hashCommands().hGetEx(key, expiration, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default Boolean hSetEx(byte[] key, Map hashes, @NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { + return hashCommands().hSetEx(key, hashes, condition, expiration); + } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java index 47898878f0..1ca9f031de 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -1257,254 +1258,267 @@ default Flux hpTtl(ByteBuffer key, List fields) { Flux> hpTtl(Publisher commands); - /** - * {@literal HGETDEL} {@link Command}. - * - * @author Viktoriya Kutsarova - * @see Redis Documentation: HGETDEL - */ - class HGetDelCommand extends HashFieldsCommand { - - private HGetDelCommand(@Nullable ByteBuffer key, List fields) { - super(key, fields); - } - - /** - * Creates a new {@link HGetDelCommand} given a {@link ByteBuffer field name}. - * - * @param field must not be {@literal null}. - * @return a new {@link HGetDelCommand} for a {@link ByteBuffer field name}. - */ - public static HGetDelCommand field(ByteBuffer field) { - - Assert.notNull(field, "Field must not be null"); - - return new HGetDelCommand(null, Collections.singletonList(field)); - } - - /** - * Creates a new {@link HGetDelCommand} given a {@link Collection} of field names. - * - * @param fields must not be {@literal null}. - * @return a new {@link HGetDelCommand} for a {@link Collection} of field names. - */ - public static HGetDelCommand fields(Collection fields) { - - Assert.notNull(fields, "Fields must not be null"); - - return new HGetDelCommand(null, new ArrayList<>(fields)); - } - - /** - * Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties. - * - * @param key must not be {@literal null}. - * @return a new {@link HGetDelCommand} with {@literal key} applied. - */ - public HGetDelCommand from(ByteBuffer key) { - - Assert.notNull(key, "Key must not be null"); - - return new HGetDelCommand(key, getFields()); - } - } - - - /** - * Get and delete the value of one or more {@literal fields} from hash at {@literal key}. Values are returned in the - * order of the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * When the last field is deleted, the key will also be deleted. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HGETDEL - */ - default Mono> hGetDel(ByteBuffer key, Collection fields) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - return hGetDel(Mono.just(HGetDelCommand.fields(fields).from(key))).next().map(MultiValueResponse::getOutput); - } - - /** - * Get and delete the value of one or more {@literal fields} from hash at {@literal key}. Values are returned in the - * order of the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * When the last field is deleted, the key will also be deleted. - * - * @param commands must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HGETDEL - */ - Flux> hGetDel(Publisher commands); - - class HGetExCommand extends HashFieldsCommand { - - private final Expiration expiration; - - private HGetExCommand(@Nullable ByteBuffer key, List fields, Expiration expiration) { - - super(key, fields); - - this.expiration = expiration; - } - - /** - * Creates a new {@link HGetExCommand}. - * - * @param fields the {@code fields} names to apply expiration to - * @param expiration the {@link Expiration} to apply to the given {@literal fields}. - * @return new instance of {@link HGetExCommand}. - */ - public static HGetExCommand expire(List fields, Expiration expiration) { - return new HGetExCommand(null, fields, expiration); - } - - /** - * @param key the {@literal key} from which to expire the {@literal fields} from. - * @return new instance of {@link HashExpireCommand}. - */ - public HGetExCommand from(ByteBuffer key) { - return new HGetExCommand(key, getFields(), expiration); - } - - /** - * Creates a new {@link HGetExCommand}. - * - * @param fields the {@code fields} names to apply expiration to - * @return new instance of {@link HGetExCommand}. - */ - public HGetExCommand fields(Collection fields) { - return new HGetExCommand(getKey(), new ArrayList<>(fields), expiration); - } - - public Expiration getExpiration() { - return expiration; - } - } - - /** - * Get the value of one or more {@literal fields} from hash at {@literal key} and optionally set expiration time or - * time-to-live (TTL) for given {@literal fields}. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HGETEX - */ - default Mono> hGetEx(ByteBuffer key, Expiration expiration, List fields) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - return hGetEx(Mono.just(HGetExCommand.expire(fields, expiration).from(key))).next().map(MultiValueResponse::getOutput); - } - - /** - * Get the value of one or more {@literal fields} from hash at {@literal key} and optionally set expiration time or - * time-to-live (TTL) for given {@literal fields}. - * - * @param commands must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HGETEX - */ - Flux> hGetEx(Publisher commands); - - /** - * {@literal HSETEX} {@link Command}. - * - * @author Viktoriya Kutsarova - * @see Redis Documentation: HSETEX - */ - class HSetExCommand extends KeyCommand { - - private final Map fieldValueMap; - private final RedisHashCommands.HashFieldSetOption condition; - private final Expiration expiration; - - private HSetExCommand(@Nullable ByteBuffer key, Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - super(key); - this.fieldValueMap = fieldValueMap; - this.condition = condition; - this.expiration = expiration; - } - - /** - * Creates a new {@link HSetExCommand} for setting field-value pairs with condition and expiration. - * - * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. - * @param condition the condition for setting fields; must not be {@literal null}. - * @param expiration the expiration to apply; must not be {@literal null}. - * @return new instance of {@link HSetExCommand}. - */ - public static HSetExCommand setWithConditionAndExpiration(Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - return new HSetExCommand(null, fieldValueMap, condition, expiration); - } - - /** - * Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties. - * - * @param key must not be {@literal null}. - * @return a new {@link HSetExCommand} with {@literal key} applied. - */ - public HSetExCommand from(ByteBuffer key) { - Assert.notNull(key, "Key must not be null"); - return new HSetExCommand(key, fieldValueMap, condition, expiration); - } - - /** - * @return the field-value map. - */ - public Map getFieldValueMap() { - return fieldValueMap; - } - - /** - * @return the condition for setting fields. - */ - public RedisHashCommands.HashFieldSetOption getCondition() { - return condition; - } - - /** - * @return the expiration to apply. - */ - public Expiration getExpiration() { - return expiration; - } - } - - /** - * Set field-value pairs in hash at {@literal key} with condition and expiration. - * - * @param key must not be {@literal null}. - * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. - * @param condition the condition for setting fields; must not be {@literal null}. - * @param expiration the expiration to apply; must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HSETEX - */ - default Mono hSetEx(ByteBuffer key, Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fieldValueMap, "Field-value map must not be null"); - Assert.notNull(condition, "Condition must not be null"); - Assert.notNull(expiration, "Expiration must not be null"); - - return hSetEx(Mono.just(HSetExCommand.setWithConditionAndExpiration(fieldValueMap, condition, expiration).from(key))) - .next().map(CommandResponse::getOutput); - } - - /** - * Set field-value pairs in hash at {@literal key} with condition and expiration. - * - * @param commands must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HSETEX - */ - Flux> hSetEx(Publisher commands); + /** + * {@literal HGETDEL} {@link Command}. + * + * @author Viktoriya Kutsarova + * @since 4.0 + * @see Redis Documentation: HGETDEL + */ + class HGetDelCommand extends HashFieldsCommand { + + private HGetDelCommand(@Nullable ByteBuffer key, List fields) { + super(key, fields); + } + + /** + * Creates a new {@link HGetDelCommand} given a {@link ByteBuffer field name}. + * + * @param field must not be {@literal null}. + * @return a new {@link HGetDelCommand} for a {@link ByteBuffer field name}. + */ + public static HGetDelCommand field(ByteBuffer field) { + + Assert.notNull(field, "Field must not be null"); + + return new HGetDelCommand(null, Collections.singletonList(field)); + } + + /** + * Creates a new {@link HGetDelCommand} given a {@link Collection} of field names. + * + * @param fields must not be {@literal null}. + * @return a new {@link HGetDelCommand} for a {@link Collection} of field names. + */ + public static HGetDelCommand fields(Collection fields) { + + Assert.notNull(fields, "Fields must not be null"); + + return new HGetDelCommand(null, new ArrayList<>(fields)); + } + + /** + * Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param key must not be {@literal null}. + * @return a new {@link HGetDelCommand} with {@literal key} applied. + */ + public HGetDelCommand from(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null"); + + return new HGetDelCommand(key, getFields()); + } + } + + + /** + * Get and delete the value of one or more {@literal fields} from hash at {@literal key}. Values are returned in the + * order of the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * When the last field is deleted, the key will also be deleted. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + * @see Redis Documentation: HGETDEL + */ + default Mono> hGetDel(ByteBuffer key, Collection fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + return hGetDel(Mono.just(HGetDelCommand.fields(fields).from(key))).next().map(MultiValueResponse::getOutput); + } + + /** + * Get and delete the value of one or more {@literal fields} from hash at {@literal key}. Values are returned in the + * order of the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * When the last field is deleted, the key will also be deleted. + * + * @param commands must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + * @see Redis Documentation: HGETDEL + */ + Flux> hGetDel(Publisher commands); + + /** + * {@literal HGETEX} {@link Command}. + * + * @author Viktoriya Kutsarova + * @see Redis Documentation: HGETEX + * @since 4.0 + */ + class HGetExCommand extends HashFieldsCommand { + + private final @Nullable Expiration expiration; + + private HGetExCommand(@Nullable ByteBuffer key, List fields, @Nullable Expiration expiration) { + + super(key, fields); + + this.expiration = expiration; + } + + /** + * Creates a new {@link HGetExCommand}. + * + * @param fields the {@code fields} names to apply expiration to + * @param expiration the optional {@link Expiration} to apply to the given {@literal fields}. + * @return new instance of {@link HGetExCommand}. + */ + public static HGetExCommand expire(List fields, @Nullable Expiration expiration) { + return new HGetExCommand(null, fields, expiration); + } + + /** + * @param key the {@literal key} from which to expire the {@literal fields} from. + * @return new instance of {@link HashExpireCommand}. + */ + public HGetExCommand from(ByteBuffer key) { + return new HGetExCommand(key, getFields(), expiration); + } + + /** + * Creates a new {@link HGetExCommand}. + * + * @param fields the {@code fields} names to apply expiration to + * @return new instance of {@link HGetExCommand}. + */ + public HGetExCommand fields(Collection fields) { + return new HGetExCommand(getKey(), new ArrayList<>(fields), expiration); + } + + public @Nullable Expiration getExpiration() { + return expiration; + } + } + + /** + * Get the value of one or more {@literal fields} from hash at {@literal key} and optionally set expiration time or + * time-to-live (TTL) for given {@literal fields}. + * + * @param key must not be {@literal null}. + * @param expiration the optional expiration to set. + * @param fields must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HGETEX + */ + default Mono> hGetEx(ByteBuffer key, @Nullable Expiration expiration, List fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + return hGetEx(Mono.just(HGetExCommand.expire(fields, expiration).from(key))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get the value of one or more {@literal fields} from hash at {@literal key} and optionally set expiration time or + * time-to-live (TTL) for given {@literal fields}. + * + * @param commands must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HGETEX + */ + Flux> hGetEx(Publisher commands); + + /** + * {@literal HSETEX} {@link Command}. + * + * @author Viktoriya Kutsarova + * @since 4.0 + * @see Redis Documentation: HSETEX + */ + class HSetExCommand extends KeyCommand { + + private final Map fieldValueMap; + private final RedisHashCommands.HashFieldSetOption condition; + private final @Nullable Expiration expiration; + + private HSetExCommand(@Nullable ByteBuffer key, Map fieldValueMap, + RedisHashCommands.HashFieldSetOption condition, @Nullable Expiration expiration) { + super(key); + this.fieldValueMap = fieldValueMap; + this.condition = condition; + this.expiration = expiration; + } + + /** + * Creates a new {@link HSetExCommand} for setting field-value pairs with condition and expiration. + * + * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. + * @param condition the condition for setting fields; must not be {@literal null}. + * @param expiration the optional expiration to apply. + * @return new instance of {@link HSetExCommand}. + */ + public static HSetExCommand setWithConditionAndExpiration(Map fieldValueMap, + RedisHashCommands.HashFieldSetOption condition, @Nullable Expiration expiration) { + return new HSetExCommand(null, fieldValueMap, condition, expiration); + } + + /** + * Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param key must not be {@literal null}. + * @return a new {@link HSetExCommand} with {@literal key} applied. + */ + public HSetExCommand from(ByteBuffer key) { + Assert.notNull(key, "Key must not be null"); + return new HSetExCommand(key, fieldValueMap, condition, expiration); + } + + /** + * @return the field-value map. + */ + public Map getFieldValueMap() { + return fieldValueMap; + } + + /** + * @return the condition for setting fields. + */ + public RedisHashCommands.HashFieldSetOption getCondition() { + return condition; + } + + /** + * @return the expiration to apply. + */ + public @Nullable Expiration getExpiration() { + return expiration; + } + } + + /** + * Set field-value pairs in hash at {@literal key} with condition and expiration. + * + * @param key must not be {@literal null}. + * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. + * @param condition the condition for setting fields; must not be {@literal null}. + * @param expiration the optional expiration to apply + * @return never {@literal null}. + * @see Redis Documentation: HSETEX + */ + default Mono hSetEx(ByteBuffer key, Map fieldValueMap, + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fieldValueMap, "Field-value map must not be null"); + Assert.notNull(condition, "Condition must not be null"); + + return hSetEx(Mono.just(HSetExCommand.setWithConditionAndExpiration(fieldValueMap, condition, expiration) + .from(key))) + .next().map(CommandResponse::getOutput); + } + + /** + * Set field-value pairs in hash at {@literal key} with condition and expiration. + * + * @param commands must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HSETEX + */ + Flux> hSetEx(Publisher commands); } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java index 80146c9fb0..46b97e3829 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -23,6 +23,8 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; @@ -544,86 +546,88 @@ default List hExpireAt(byte @NonNull [] key, long unixTime, byte @NonNull */ List<@NonNull Long> hpTtl(byte @NonNull [] key, byte @NonNull [] @NonNull... fields); - /** - * Get and delete the value of one or more {@code fields} from hash at {@code key}. Values are returned in the order of - * the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * When the last field is deleted, the key will also be deleted. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return list of values for deleted {@code fields} ({@literal null} for fields that does not exist) or an + /** + * Get and delete the value of one or more {@code fields} from hash at {@code key}. Values are returned in the order of + * the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * When the last field is deleted, the key will also be deleted. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return list of values for deleted {@code fields} ({@literal null} for fields that does not exist) or an * empty {@link List} if key does not exist or {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HGETDEL - */ - List hGetDel(byte @NonNull [] key, byte @NonNull [] @NonNull... fields); - - /** - * Get the value of one or more {@code fields} from hash at {@code key} and optionally set expiration time or - * time-to-live (TTL) for given {@code fields}. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. + * @see Redis Documentation: HGETDEL + */ + List hGetDel(byte @NonNull [] key, byte @NonNull [] @NonNull ... fields); + + /** + * Get the value of one or more {@code fields} from hash at {@code key} and optionally set expiration time or + * time-to-live (TTL) for given {@code fields}. + * + * @param key must not be {@literal null}. + * @param expiration the optional expiration to apply. + * @param fields must not be {@literal null}. * @return list of values for given {@code fields} or an empty {@link List} if key does not * exist or {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HGETEX - */ - List hGetEx(byte @NonNull [] key, Expiration expiration, - byte @NonNull [] @NonNull... fields); - - /** - * Set field-value pairs in hash at {@literal key} with optional condition and expiration. - * - * @param key must not be {@literal null}. - * @param hashes the field-value pairs to set; must not be {@literal null}. - * @param hashFieldSetOption the optional condition for setting fields. - * @param expiration the optional expiration to apply. - * @return never {@literal null}. - * @see Redis Documentation: HSETEX - */ - Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFieldSetOption hashFieldSetOption, - Expiration expiration); - - /** - * {@code HSETEX} command arguments for {@code FNX}, {@code FXX}. - * - * @author Viktoriya Kutsarova - */ - enum HashFieldSetOption { - - /** - * Do not set any additional command argument. - */ - UPSERT, - - /** - * {@code FNX} - */ - IF_NONE_EXIST, - - /** - * {@code FXX} - */ - IF_ALL_EXIST; - - /** - * Do not set any additional command argument. - */ - public static HashFieldSetOption upsert() { - return UPSERT; - } - - /** - * {@code FNX} - */ - public static HashFieldSetOption ifNoneExist() { - return IF_NONE_EXIST; - } - - /** - * {@code FXX} - */ - public static HashFieldSetOption ifAllExist() { - return IF_ALL_EXIST; - } - } + * @see Redis Documentation: HGETEX + */ + List hGetEx(byte @NonNull [] key, @Nullable Expiration expiration, + byte @NonNull [] @NonNull ... fields); + + /** + * Set field-value pairs in hash at {@literal key} with optional condition and expiration. + * + * @param key must not be {@literal null}. + * @param hashes the field-value pairs to set; must not be {@literal null}. + * @param hashFieldSetOption the condition for setting fields; must not be {@literal null}. + * @param expiration the optional expiration to apply. + * @return never {@literal null}. + * @see Redis Documentation: HSETEX + */ + Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, + @NonNull HashFieldSetOption hashFieldSetOption, @Nullable Expiration expiration); + + /** + * {@code HSETEX} command arguments for {@code FNX}, {@code FXX}. + * + * @author Viktoriya Kutsarova + * @since 4.0 + */ + enum HashFieldSetOption { + + /** + * Do not set any additional command argument. + */ + UPSERT, + + /** + * {@code FNX} + */ + IF_NONE_EXIST, + + /** + * {@code FXX} + */ + IF_ALL_EXIST; + + /** + * Do not set any additional command argument. + */ + public static HashFieldSetOption upsert() { + return UPSERT; + } + + /** + * {@code FNX} + */ + public static HashFieldSetOption ifNoneExist() { + return IF_NONE_EXIST; + } + + /** + * {@code FXX} + */ + public static HashFieldSetOption ifAllExist() { + return IF_ALL_EXIST; + } + } } diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index 3069f0c587..bf60a4f8cf 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -2564,43 +2564,43 @@ List hpExpireAt(@NonNull String key, long unixTimeInMillis, ExpirationOpti */ List hpTtl(@NonNull String key, @NonNull String @NonNull... fields); - /** - * Get and delete the value of one or more {@code fields} from hash at {@code key}. When the last field is deleted, - * the key will also be deleted. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return empty {@link List} if key does not exist. {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HMGET - * @see RedisHashCommands#hMGet(byte[], byte[]...) - */ - List hGetDel(@NonNull String key, @NonNull String @NonNull... fields); - - /** - * Get the value of one or more {@code fields} from hash at {@code key} and optionally set expiration time or - * time-to-live (TTL) for given {@code fields}. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return empty {@link List} if key does not exist. {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HGETEX - * @see RedisHashCommands#hGetEx(byte[], Expiration, byte[]...) - */ - List hGetEx(@NonNull String key, Expiration expiration, @NonNull String @NonNull... fields); - - /** - * Set field-value pairs in hash at {@literal key} with optional condition and expiration. - * - * @param key must not be {@literal null}. - * @param hashes the field-value pairs to set; must not be {@literal null}. - * @param condition the optional condition for setting fields. - * @param expiration the optional expiration to apply. - * @return never {@literal null}. - * @see Redis Documentation: HSETEX - * @see RedisHashCommands#hSetEx(byte[], Map, HashFieldSetOption, Expiration) - */ - Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, - Expiration expiration); + /** + * Get and delete the value of one or more {@code fields} from hash at {@code key}. When the last field is deleted, + * the key will also be deleted. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return empty {@link List} if key does not exist. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HMGET + * @see RedisHashCommands#hMGet(byte[], byte[]...) + */ + List hGetDel(@NonNull String key, @NonNull String @NonNull ... fields); + + /** + * Get the value of one or more {@code fields} from hash at {@code key} and optionally set expiration time or + * time-to-live (TTL) for given {@code fields}. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return empty {@link List} if key does not exist. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HGETEX + * @see RedisHashCommands#hGetEx(byte[], Expiration, byte[]...) + */ + List hGetEx(@NonNull String key, Expiration expiration, @NonNull String @NonNull ... fields); + + /** + * Set field-value pairs in hash at {@literal key} with optional condition and expiration. + * + * @param key must not be {@literal null}. + * @param hashes the field-value pairs to set; must not be {@literal null}. + * @param condition the optional condition for setting fields. + * @param expiration the optional expiration to apply. + * @return never {@literal null}. + * @see Redis Documentation: HSETEX + * @see RedisHashCommands#hSetEx(byte[], Map, HashFieldSetOption, Expiration) + */ + Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, + @NonNull HashFieldSetOption condition, @Nullable Expiration expiration); // ------------------------------------------------------------------------- // Methods dealing with HyperLogLog diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java index 582fb2a9cf..e37dab9cdc 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java @@ -15,7 +15,6 @@ */ package org.springframework.data.redis.connection.jedis; -import org.springframework.data.redis.core.types.Expiration; import redis.clients.jedis.args.ExpiryOption; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.resps.ScanResult; @@ -27,6 +26,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.ExpirationOptions; @@ -35,6 +35,7 @@ import org.springframework.data.redis.core.ScanCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.util.Assert; /** @@ -415,44 +416,47 @@ public List hpTtl(byte[] key, byte[]... fields) { } } - @Override - public List hGetDel(byte[] key, byte[]... fields) { + @Override + public List hGetDel(byte[] key, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - try { - return connection.getCluster().hgetdel(key, fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } + try { + return connection.getCluster().hgetdel(key, fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } - @Override - public List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { + @Override + public List hGetEx(byte[] key, @Nullable Expiration expiration, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - try { - return connection.getCluster().hgetex(key, JedisConverters.toHGetExParams(expiration), fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } + try { + return connection.getCluster().hgetex(key, JedisConverters.toHGetExParams(expiration), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } - @Override - public Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { + @Override + public Boolean hSetEx(byte[] key, Map hashes, @NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashes, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashes, "Fields must not be null"); + Assert.notNull(condition, "Condition must not be null"); - try { - return JedisConverters.toBoolean(connection.getCluster().hsetex(key, JedisConverters.toHSetExParams(condition, expiration), hashes)); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } + try { + return JedisConverters.toBoolean( + connection.getCluster().hsetex(key, JedisConverters.toHSetExParams(condition, expiration), hashes)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } @Nullable @Override diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index 1be5c57bab..249802553c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -45,8 +45,8 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Sort; import org.springframework.data.geo.Distance; @@ -401,105 +401,6 @@ static GetExParams toGetExParams(Expiration expiration, GetExParams params) { : params.ex(expiration.getConverted(TimeUnit.SECONDS)); } - /** - * Converts a given {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to the according - * {@code HSETEX} command argument. - *
- *
{@link RedisHashCommands.HashFieldSetOption#ifNoneExist()}
- *
{@code FNX}
- *
{@link RedisHashCommands.HashFieldSetOption#ifAllExist()}
- *
{@code FXX}
- *
{@link RedisHashCommands.HashFieldSetOption#upsert()}
- *
no condition flag
- *
- *
- *
{@link TimeUnit#MILLISECONDS}
- *
{@code PX|PXAT}
- *
{@link TimeUnit#SECONDS}
- *
{@code EX|EXAT}
- *
- * - * @param condition can be {@literal null}. - * @param expiration can be {@literal null}. - * @since 4.0 - */ - static HSetExParams toHSetExParams(RedisHashCommands.@Nullable HashFieldSetOption condition, @Nullable Expiration expiration) { - return toHSetExParams(condition, expiration, new HSetExParams()); - } - - static HSetExParams toHSetExParams(RedisHashCommands.@Nullable HashFieldSetOption condition, @Nullable Expiration expiration, HSetExParams params) { - - if (condition == null && expiration == null) { - return params; - } - - if (condition != null) { - if (condition.equals(RedisHashCommands.HashFieldSetOption.ifNoneExist())) { - params.fnx(); - } else if (condition.equals(RedisHashCommands.HashFieldSetOption.ifAllExist())) { - params.fxx(); - } - } - - if (expiration == null) { - return params; - } - - if (expiration.isKeepTtl()) { - return params.keepTtl(); - } - - if (expiration.isPersistent()) { - return params; - } - - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - return expiration.isUnixTimestamp() ? params.pxAt(expiration.getExpirationTime()) - : params.px(expiration.getExpirationTime()); - } - - return expiration.isUnixTimestamp() ? params.exAt(expiration.getConverted(TimeUnit.SECONDS)) - : params.ex(expiration.getConverted(TimeUnit.SECONDS)); - } - - /** - * Converts a given {@link Expiration} to the according {@code HGETEX} command argument depending on - * {@link Expiration#isUnixTimestamp()}. - *
- *
{@link TimeUnit#MILLISECONDS}
- *
{@code PX|PXAT}
- *
{@link TimeUnit#SECONDS}
- *
{@code EX|EXAT}
- *
- * - * @param expiration must not be {@literal null}. - * @since 4.0 - */ - static HGetExParams toHGetExParams(Expiration expiration) { - return toHGetExParams(expiration, new HGetExParams()); - } - - static HGetExParams toHGetExParams(Expiration expiration, HGetExParams params) { - - if (expiration == null) { - return params; - } - - if (expiration.isPersistent()) { - return params.persist(); - } - - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return params.pxAt(expiration.getExpirationTime()); - } - return params.px(expiration.getExpirationTime()); - } - - return expiration.isUnixTimestamp() ? params.exAt(expiration.getConverted(TimeUnit.SECONDS)) - : params.ex(expiration.getConverted(TimeUnit.SECONDS)); - } - /** * Converts a given {@link SetOption} to the according {@code SET} command argument.
*
@@ -543,6 +444,96 @@ public static SetParams toSetCommandNxXxArgument(SetOption option, SetParams par }; } + /** + * Converts a given {@link Expiration} to the according {@code HGETEX} command argument depending on + * {@link Expiration#isUnixTimestamp()}. + *
+ *
{@link TimeUnit#MILLISECONDS}
+ *
{@code PX|PXAT}
+ *
{@link TimeUnit#SECONDS}
+ *
{@code EX|EXAT}
+ *
+ * + * @param expiration can be {@literal null}. + * @since 4.0 + */ + static HGetExParams toHGetExParams(@Nullable Expiration expiration) { + + HGetExParams params = new HGetExParams(); + + if (expiration == null) { + return params; + } + + if (expiration.isPersistent()) { + return params.persist(); + } + + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + return expiration.isUnixTimestamp() ? + params.pxAt(expiration.getExpirationTime()) : + params.px(expiration.getExpirationTime()); + } + + return expiration.isUnixTimestamp() ? + params.exAt(expiration.getExpirationTimeInSeconds()) : + params.ex(expiration.getExpirationTimeInSeconds()); + } + + /** + * Converts a given {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to the according + * {@code HSETEX} command argument. + *
+ *
{@link RedisHashCommands.HashFieldSetOption#ifNoneExist()}
+ *
{@code FNX}
+ *
{@link RedisHashCommands.HashFieldSetOption#ifAllExist()}
+ *
{@code FXX}
+ *
{@link RedisHashCommands.HashFieldSetOption#upsert()}
+ *
no condition flag
+ *
+ *
+ *
{@link TimeUnit#MILLISECONDS}
+ *
{@code PX|PXAT}
+ *
{@link TimeUnit#SECONDS}
+ *
{@code EX|EXAT}
+ *
+ * + * @param condition must not be {@literal null}; use {@code UPSERT} to omit FNX/FXX. + * @param expiration can be {@literal null} to omit TTL. + * @return never {@literal null}. + * @since 4.0 + */ + static HSetExParams toHSetExParams(RedisHashCommands.@NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { + + HSetExParams params = new HSetExParams(); + + switch (condition) { + case IF_NONE_EXIST -> params.fnx(); + case IF_ALL_EXIST -> params.fxx(); + } + + if (expiration == null || expiration.isPersistent()) { + return params; + } + + if (expiration.isKeepTtl()) { + return params.keepTtl(); + } + + // PX | PXAT + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + return expiration.isUnixTimestamp() ? + params.pxAt(expiration.getExpirationTime()) : + params.px(expiration.getExpirationTime()); + } + + // EX | EXAT + return expiration.isUnixTimestamp() ? + params.exAt(expiration.getExpirationTimeInSeconds()) : + params.ex(expiration.getExpirationTimeInSeconds()); + } + private static byte[] boundaryToBytes(org.springframework.data.domain.Range.Bound boundary, byte[] inclPrefix, byte[] exclPrefix) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java index 11581a741d..449b3e4de1 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java @@ -333,35 +333,38 @@ protected void doClose() { return connection.invoke().just(Jedis::hpttl, PipelineBinaryCommands::hpttl, key, fields); } - @Override - public List hGetDel(byte @NonNull [] key, byte @NonNull [] @NonNull... fields) { + @Override + public List hGetDel(byte @NonNull [] key, byte @NonNull [] @NonNull ... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - return connection.invoke().just(Jedis::hgetdel, PipelineBinaryCommands::hgetdel, key, fields); - } + return connection.invoke().just(Jedis::hgetdel, PipelineBinaryCommands::hgetdel, key, fields); + } - @Override - public List hGetEx(byte @NonNull [] key, Expiration expiration, byte @NonNull [] @NonNull... fields) { + @Override + public List hGetEx(byte @NonNull [] key, @Nullable Expiration expiration, + byte @NonNull [] @NonNull ... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - return connection.invoke().just(Jedis::hgetex, PipelineBinaryCommands::hgetex, key, JedisConverters.toHGetExParams(expiration), fields); - } + return connection.invoke() + .just(Jedis::hgetex, PipelineBinaryCommands::hgetex, key, JedisConverters.toHGetExParams(expiration), fields); + } - @Override - public Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFieldSetOption condition, - Expiration expiration) { + @Override + public Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, + @NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashes, "Hashes must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashes, "Hashes must not be null"); + Assert.notNull(condition, "Condition must not be null"); - return connection.invoke().from(Jedis::hsetex, PipelineBinaryCommands::hsetex, key, - JedisConverters.toHSetExParams(condition, expiration), hashes) - .get(Converters::toBoolean); - } + return connection.invoke() + .from(Jedis::hsetex, PipelineBinaryCommands::hsetex, key, JedisConverters.toHSetExParams(condition, expiration), + hashes).get(Converters::toBoolean); + } @Nullable @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index bdeea2dd6f..2a367f45e7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -1243,10 +1243,10 @@ static class TypeHints { COMMAND_OUTPUT_TYPE_MAPPING.put(ZRANGEBYSCORE, ValueListOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(ZREVRANGE, ValueListOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(ZREVRANGEBYSCORE, ValueListOutput.class); - COMMAND_OUTPUT_TYPE_MAPPING.put(HGETDEL, ValueListOutput.class); - COMMAND_OUTPUT_TYPE_MAPPING.put(HGETEX, ValueListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HGETDEL, ValueListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HGETEX, ValueListOutput.class); - // BOOLEAN + // BOOLEAN COMMAND_OUTPUT_TYPE_MAPPING.put(EXISTS, BooleanOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(EXPIRE, BooleanOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(EXPIREAT, BooleanOutput.class); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 98c0969738..aee9852a18 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; @@ -622,93 +623,87 @@ static GetExArgs toGetExArgs(@Nullable Expiration expiration) { : args.ex(expiration.getConverted(TimeUnit.SECONDS)); } - /** - * Convert {@link Expiration} to {@link HGetExArgs}. - * - * @param expiration can be {@literal null}. - * @since 4.0 - */ - static HGetExArgs toHGetExArgs(@Nullable Expiration expiration) { - - HGetExArgs args = new HGetExArgs(); - - if (expiration == null) { - return args; - } - - if (expiration.isPersistent()) { - return args.persist(); - } - - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return args.pxAt(Instant.ofEpochSecond(expiration.getExpirationTime())); - } - return args.px(Duration.ofMillis(expiration.getExpirationTime())); - } - - return expiration.isUnixTimestamp() ? args.exAt(Instant.ofEpochSecond(expiration.getConverted(TimeUnit.SECONDS))) - : args.ex(Duration.ofSeconds(expiration.getConverted(TimeUnit.SECONDS))); - } - - /** - * Convert {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to {@link HSetExArgs} for the Redis {@code HSETEX} command. - * - *

Condition mapping:

- *
    - *
  • {@code IF_NONE_EXIST}  {@code FNX}
  • - *
  • {@code IF_ALL_EXIST}  {@code FXX}
  • - *
  • {@code UPSERT}  no condition flag
  • - *
- * - *

Expiration mapping:

- *
    - *
  • {@link Expiration#keepTtl()}  {@code KEEPTTL}
  • - *
  • Unix timestamp  {@code EXAT}/{@code PXAT} depending on time unit
  • - *
  • Relative expiration  {@code EX}/{@code PX} depending on time unit
  • - *
  • {@code null} expiration  no TTL argument
  • - *
- * - * @param condition must not be {@literal null}; use {@code UPSERT} to omit FNX/FXX. - * @param expiration can be {@literal null} to omit TTL. - * @return never {@literal null}. - * @since 4.0 - */ - static HSetExArgs toHSetExArgs(RedisHashCommands.HashFieldSetOption condition, @Nullable Expiration expiration) { - - HSetExArgs args = new HSetExArgs(); - - if (condition == null && expiration == null) { - return args; - } - - if (condition != null ) { - if (condition.equals(RedisHashCommands.HashFieldSetOption.ifNoneExist())) { - args.fnx(); - } - if (condition.equals(RedisHashCommands.HashFieldSetOption.ifAllExist())) { - args.fxx(); - } - } - - if (expiration == null) { - return args; - } - - if (expiration.isKeepTtl()) { - return args.keepttl(); - } - - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return args.pxAt(Instant.ofEpochSecond(expiration.getExpirationTime())); - } - return args.px(Duration.ofMillis(expiration.getExpirationTime())); - } - - return expiration.isUnixTimestamp() ? args.exAt(Instant.ofEpochSecond(expiration.getConverted(TimeUnit.SECONDS))) - : args.ex(Duration.ofSeconds(expiration.getConverted(TimeUnit.SECONDS))); - } + /** + * Convert {@link Expiration} to {@link HGetExArgs}. + * + * @param expiration can be {@literal null}. + * @since 4.0 + */ + static HGetExArgs toHGetExArgs(@Nullable Expiration expiration) { + + HGetExArgs args = new HGetExArgs(); + + if (expiration == null) { + return args; + } + + if (expiration.isPersistent()) { + return args.persist(); + } + + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + return expiration.isUnixTimestamp() ? + args.pxAt(Instant.ofEpochMilli(expiration.getExpirationTime())) : + args.px(Duration.ofMillis(expiration.getExpirationTime())); + } + + return expiration.isUnixTimestamp() ? + args.exAt(Instant.ofEpochSecond(expiration.getExpirationTimeInSeconds())) : + args.ex(Duration.ofSeconds(expiration.getExpirationTimeInSeconds())); + } + + /** + * Convert {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to {@link HSetExArgs} for the Redis + * {@code HSETEX} command. + *

Condition mapping:

+ *
    + *
  • {@code IF_NONE_EXIST} {@code FNX}
  • + *
  • {@code IF_ALL_EXIST} {@code FXX}
  • + *
  • {@code UPSERT} no condition flag
  • + *
+ *

Expiration mapping:

+ *
    + *
  • {@link Expiration#keepTtl()} {@code KEEPTTL}
  • + *
  • Unix timestamp {@code EXAT}/{@code PXAT} depending on time unit
  • + *
  • Relative expiration {@code EX}/{@code PX} depending on time unit
  • + *
  • {@code null} expiration no TTL argument
  • + *
+ * + * @param condition must not be {@literal null}; use {@code UPSERT} to omit FNX/FXX. + * @param expiration can be {@literal null} to omit TTL. + * @return never {@literal null}. + * @since 4.0 + */ + static HSetExArgs toHSetExArgs(RedisHashCommands.@NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { + + HSetExArgs args = new HSetExArgs(); + + switch (condition) { + case IF_NONE_EXIST -> args.fnx(); + case IF_ALL_EXIST -> args.fxx(); + } + + if (expiration == null || expiration.isPersistent()) { + return args; + } + + if (expiration.isKeepTtl()) { + return args.keepttl(); + } + + // PX | PXAT + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + return expiration.isUnixTimestamp() ? + args.pxAt(Instant.ofEpochMilli(expiration.getExpirationTime())) : + args.px(Duration.ofMillis(expiration.getExpirationTime())); + } + + // EX | EXAT + return expiration.isUnixTimestamp() ? + args.exAt(Instant.ofEpochSecond(expiration.getExpirationTimeInSeconds())) : + args.ex(Duration.ofSeconds(expiration.getExpirationTimeInSeconds())); + } @SuppressWarnings("NullAway") static Converter, Long> toTimeConverter(TimeUnit timeUnit) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java index 7b71e76dd4..25164bdc2e 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java @@ -266,38 +266,39 @@ public List hpTtl(byte @NonNull [] key, byte @NonNull [] @NonNull... field return connection.invoke().fromMany(RedisHashAsyncCommands::hpttl, key, fields).toList(); } - @Override - public List hGetDel(byte @NonNull [] key, byte @NonNull []... fields) { + @Override + public List hGetDel(byte @NonNull [] key, byte @NonNull []... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - return connection.invoke().fromMany(RedisHashAsyncCommands::hgetdel, key, fields) - .toList(source -> source.getValueOrElse(null)); - } + return connection.invoke().fromMany(RedisHashAsyncCommands::hgetdel, key, fields) + .toList(source -> source.getValueOrElse(null)); + } - @Override - public List hGetEx(byte @NonNull [] key, Expiration expiration, byte @NonNull []... fields) { + @Override + public List hGetEx(byte @NonNull [] key, Expiration expiration, byte @NonNull []... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - return connection.invoke().fromMany(RedisHashAsyncCommands::hgetex, key, - LettuceConverters.toHGetExArgs(expiration), fields) - .toList(source -> source.getValueOrElse(null)); - } + return connection.invoke() + .fromMany(RedisHashAsyncCommands::hgetex, key, LettuceConverters.toHGetExArgs(expiration), fields) + .toList(source -> source.getValueOrElse(null)); + } @Override - public Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFieldSetOption condition, - Expiration expiration) { + public Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, + @NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashes, "Hashes must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashes, "Hashes must not be null"); + Assert.notNull(condition, "Condition must not be null"); - return connection.invoke().from(RedisHashAsyncCommands::hsetex, key, - LettuceConverters.toHSetExArgs(condition, expiration), hashes) - .get(LettuceConverters.longToBooleanConverter()); - } + return connection.invoke() + .from(RedisHashAsyncCommands::hsetex, key, LettuceConverters.toHSetExArgs(condition, expiration), + hashes).get(LettuceConverters.longToBooleanConverter()); + } /** * @param key diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java index ac9f0491d0..712050ced7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java @@ -356,50 +356,52 @@ public Flux> hpTtl(Publisher> hGetDel(Publisher commands) { + @Override + public Flux> hGetDel(Publisher commands) { - return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { - Assert.notNull(command.getKey(), "Key must not be null"); - Assert.notNull(command.getFields(), "Fields must not be null"); + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFields(), "Fields must not be null"); - return cmd.hgetdel(command.getKey(), command.getFields().toArray(ByteBuffer[]::new)).collectList() - .map(value -> new MultiValueResponse<>(command, value.stream().map(v -> v.getValueOrElse(null)) - .collect(Collectors.toList()))); - })); - } + return cmd.hgetdel(command.getKey(), command.getFields().toArray(ByteBuffer[]::new)).collectList() + .map(value -> new MultiValueResponse<>(command, value.stream().map(v -> v.getValueOrElse(null)) + .collect(Collectors.toList()))); + })); + } - @Override - public Flux> hGetEx(Publisher commands) { + @Override + public Flux> hGetEx(Publisher commands) { - return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { - Assert.notNull(command.getKey(), "Key must not be null"); - Assert.notNull(command.getFields(), "Fields must not be null"); + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFields(), "Fields must not be null"); - return cmd.hgetex(command.getKey(), LettuceConverters.toHGetExArgs(command.getExpiration()), command.getFields().toArray(ByteBuffer[]::new)).collectList() - .map(value -> new MultiValueResponse<>(command, value.stream().map(v -> v.getValueOrElse(null)) - .collect(Collectors.toList()))); - })); - } + return cmd.hgetex(command.getKey(), LettuceConverters.toHGetExArgs(command.getExpiration()), command.getFields() + .toArray(ByteBuffer[]::new)).collectList() + .map(value -> new MultiValueResponse<>(command, value.stream().map(v -> v.getValueOrElse(null)) + .collect(Collectors.toList()))); + })); + } - @Override - public Flux> hSetEx(Publisher commands) { - return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + @Override + public Flux> hSetEx(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { - Assert.notNull(command.getKey(), "Key must not be null"); - Assert.notNull(command.getFieldValueMap(), "FieldValueMap must not be null"); + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFieldValueMap(), "FieldValueMap must not be null"); + Assert.notNull(command.getCondition(), "Condition must not be null"); - Map entries = command.getFieldValueMap(); + Map entries = command.getFieldValueMap(); - return cmd.hsetex(command.getKey(), - LettuceConverters.toHSetExArgs(command.getCondition(), command.getExpiration()), entries) - .map(LettuceConverters.longToBooleanConverter()::convert) - .map(value -> new BooleanResponse<>(command, value)); + return cmd.hsetex(command.getKey(), + LettuceConverters.toHSetExArgs(command.getCondition(), command.getExpiration()), entries) + .map(LettuceConverters.longToBooleanConverter()::convert) + .map(value -> new BooleanResponse<>(command, value)); - })); - } + })); + } private static Map.Entry toEntry(KeyValue kv) { diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index 9d44c8c2ba..560eacaf7c 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -23,6 +23,8 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.core.types.Expiration; @@ -254,31 +256,36 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * * @param hashFields must not be {@literal null}. * @return {@literal null} when used in pipeline / transaction. - * @since 3.1 + * @since 4.0 + * @see Redis Documentation: HGETDEL */ - List getAndDelete(@NonNull Collection<@NonNull HK> hashFields); + List getAndDelete(@NonNull Collection<@NonNull HK> hashFields); - /** - * Get and optionally expire the value for given {@code hashFields} from the hash at the bound key. Values are in the order of the - * requested hash fields. Absent field values are represented using {@literal null} in the resulting {@link List}. - * - * @param expiration is optional. - * @param hashFields must not be {@literal null}. - * @return never {@literal null}. - * @since 4.0 - */ - List getAndExpire(Expiration expiration, @NonNull Collection<@NonNull HK> hashFields); + /** + * Get and optionally expire the value for given {@code hashFields} from the hash at the bound key. Values are in the order of the + * requested hash fields. Absent field values are represented using {@literal null} in the resulting {@link List}. + * + * @param expiration is optional. + * @param hashFields must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + * @see Redis Documentation: HSETEX + */ + List getAndExpire(@Nullable Expiration expiration, @NonNull Collection<@NonNull HK> hashFields); - /** - * Set the value of one or more fields using data provided in {@code m} at the bound key, and optionally set their - * expiration time or time-to-live (TTL). The {@code condition} determines whether the fields are set. - * - * @param m must not be {@literal null}. - * @param condition is optional. Use {@link RedisHashCommands.HashFieldSetOption#IF_NONE_EXIST} (FNX) to only set the fields if - * none of them already exist, {@link RedisHashCommands.HashFieldSetOption#IF_ALL_EXIST} (FXX) to only set the - * fields if all of them already exist, or {@link RedisHashCommands.HashFieldSetOption#UPSERT} to set the fields - * unconditionally. - * @param expiration is optional. - */ - void putAndExpire(Map m, RedisHashCommands.HashFieldSetOption condition, Expiration expiration); + /** + * Set the value of one or more fields using data provided in {@code m} at the bound key, and optionally set their + * expiration time or time-to-live (TTL). The {@code condition} determines whether the fields are set. + * + * @param m must not be {@literal null}. + * @param condition must not be {@literal null}. Use {@link RedisHashCommands.HashFieldSetOption#IF_NONE_EXIST} (FNX) to only set the fields if + * none of them already exist, {@link RedisHashCommands.HashFieldSetOption#IF_ALL_EXIST} (FXX) to only set the + * fields if all of them already exist, or {@link RedisHashCommands.HashFieldSetOption#UPSERT} to set the fields + * unconditionally. + * @param expiration is optional. + * @since 4.0 + * @see Redis Documentation: HSETEX + */ + void putAndExpire(Map m, RedisHashCommands.@NonNull HashFieldSetOption condition, + @Nullable Expiration expiration); } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java index cf8ea5a0e1..77c78c02e7 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -186,8 +186,7 @@ public List multiGet(@NonNull K key, @NonNull Collection<@NonNull HK> fields byte[][] rawHashKeys = new byte[fields.size()][]; int counter = 0; - for (@NonNull - HK hashKey : fields) { + for (@NonNull HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } @@ -196,8 +195,8 @@ public List multiGet(@NonNull K key, @NonNull Collection<@NonNull HK> fields return deserializeHashValues(rawValues); } - @Override - public List getAndDelete(@NonNull K key, @NonNull Collection<@NonNull HK> fields) { + @Override + public List getAndDelete(@NonNull K key, @NonNull Collection<@NonNull HK> fields) { if (fields.isEmpty()) { return Collections.emptyList(); @@ -205,18 +204,17 @@ public List getAndDelete(@NonNull K key, @NonNull Collection<@NonNull HK> fi byte[] rawKey = rawKey(key); byte[][] rawHashKeys = new byte[fields.size()][]; - int counter = 0; - for (@NonNull - HK hashKey : fields) { + int counter = 0; + for (@NonNull HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } - List rawValues = execute(connection -> connection.hashCommands().hGetDel(rawKey, rawHashKeys)); + List rawValues = execute(connection -> connection.hashCommands().hGetDel(rawKey, rawHashKeys)); return deserializeHashValues(rawValues); } - @Override - public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, + @Override + public List getAndExpire(@NonNull K key, @Nullable Expiration expiration, @NonNull Collection<@NonNull HK> fields) { if (fields.isEmpty()) { @@ -226,8 +224,7 @@ public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, byte[] rawKey = rawKey(key); byte[][] rawHashKeys = new byte[fields.size()][]; int counter = 0; - for (@NonNull - HK hashKey : fields) { + for (@NonNull HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } List rawValues = execute(connection -> connection.hashCommands().hGetEx(rawKey, expiration, rawHashKeys)); @@ -235,23 +232,23 @@ public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, return deserializeHashValues(rawValues); } - @Override - public Boolean putAndExpire(@NonNull K key, @NonNull Map m, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - if (m.isEmpty()) { - return false; - } + @Override + public Boolean putAndExpire(@NonNull K key, @NonNull Map m, + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { + if (m.isEmpty()) { + return false; + } - byte[] rawKey = rawKey(key); + byte[] rawKey = rawKey(key); - Map hashes = new LinkedHashMap<>(m.size()); + Map hashes = new LinkedHashMap<>(m.size()); - for (Map.Entry entry : m.entrySet()) { - hashes.put(rawHashKey(entry.getKey()), rawHashValue(entry.getValue())); - } + for (Map.Entry entry : m.entrySet()) { + hashes.put(rawHashKey(entry.getKey()), rawHashValue(entry.getValue())); + } - return execute(connection -> connection.hashCommands().hSetEx(rawKey, hashes, condition, expiration)); - } + return execute(connection -> connection.hashCommands().hSetEx(rawKey, hashes, condition, expiration)); + } @Override public void put(@NonNull K key, @NonNull HK hashKey, HV value) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java index 3dc149940e..4b3c8f1e58 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java @@ -15,7 +15,6 @@ */ package org.springframework.data.redis.core; -import org.springframework.data.redis.connection.RedisHashCommands; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,7 +27,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Function; - +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -36,6 +35,7 @@ import org.springframework.data.redis.connection.ReactiveHashCommands; import org.springframework.data.redis.connection.ReactiveHashCommands.HashExpireCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.Expirations; @@ -112,40 +112,42 @@ public Mono> multiGet(H key, Collection hashKeys) { .flatMap(hks -> hashCommands.hMGet(rawKey(key), hks)).map(this::deserializeHashValues)); } - @Override - public Mono> getAndDelete(H key, Collection hashKeys) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashKeys, "Hash keys must not be null"); - Assert.notEmpty(hashKeys, "Hash keys must not be empty"); - - return createMono(hashCommands -> Flux.fromIterable(hashKeys) // - .map(this::rawHashKey) // - .collectList() // - .flatMap(hks -> hashCommands.hGetDel(rawKey(key), hks)).map(this::deserializeHashValues)); - } - - @Override - public Mono putAndExpire(H key, Map map, RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(map, "Map must not be null"); - - return createMono(hashCommands -> Flux.fromIterable(() -> map.entrySet().iterator()) // - .collectMap(entry -> rawHashKey(entry.getKey()), entry -> rawHashValue(entry.getValue())) // - .flatMap(serialized -> hashCommands.hSetEx(rawKey(key), serialized, condition, expiration))); - } - - @Override - public Mono> getAndExpire(H key, Expiration expiration, Collection hashKeys) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashKeys, "Hash keys must not be null"); - Assert.notEmpty(hashKeys, "Hash keys must not be empty"); - - return createMono(hashCommands -> Flux.fromIterable(hashKeys) // - .map(this::rawHashKey) // - .collectList() // - .flatMap(hks -> hashCommands.hGetEx(rawKey(key), expiration, hks)).map(this::deserializeHashValues)); - } + @Override + public Mono> getAndDelete(H key, Collection hashKeys) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashKeys, "Hash keys must not be null"); + Assert.notEmpty(hashKeys, "Hash keys must not be empty"); + + return createMono(hashCommands -> Flux.fromIterable(hashKeys) // + .map(this::rawHashKey) // + .collectList() // + .flatMap(hks -> hashCommands.hGetDel(rawKey(key), hks)).map(this::deserializeHashValues)); + } + + @Override + public Mono putAndExpire(H key, Map map, + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(map, "Map must not be null"); + Assert.notNull(condition, "Condition must not be null"); + + return createMono(hashCommands -> Flux.fromIterable(() -> map.entrySet().iterator()) // + .collectMap(entry -> rawHashKey(entry.getKey()), entry -> rawHashValue(entry.getValue())) // + .flatMap(serialized -> hashCommands.hSetEx(rawKey(key), serialized, condition, expiration))); + } + + @Override + public Mono> getAndExpire(H key, @Nullable Expiration expiration, Collection hashKeys) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashKeys, "Hash keys must not be null"); + Assert.notEmpty(hashKeys, "Hash keys must not be empty"); + + return createMono(hashCommands -> Flux.fromIterable(hashKeys) // + .map(this::rawHashKey) // + .collectList() // + .flatMap(hks -> hashCommands.hGetEx(rawKey(key), expiration, hks)).map(this::deserializeHashValues)); + } @Override public Mono increment(H key, HK hashKey, long delta) { diff --git a/src/main/java/org/springframework/data/redis/core/HashOperations.java b/src/main/java/org/springframework/data/redis/core/HashOperations.java index 61e1648f58..43d925400a 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -89,33 +89,36 @@ public interface HashOperations { * @param hashKeys must not be {@literal null}. * @return list of values for the given fields or {@literal null} when used in pipeline / transaction. * @since 4.0 + * @see Redis Documentation: HGETDEL */ List getAndDelete(@NonNull H key, @NonNull Collection<@NonNull HK> hashKeys); - /** - * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of - * the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * - * @param key must not be {@literal null}. - * @param expiration is optional. - * @param hashKeys must not be {@literal null}. - * @return list of values for the given fields or {@literal null} when used in pipeline / transaction. - * @since 4.0 - */ - List getAndExpire(@NonNull H key, Expiration expiration, @NonNull Collection<@NonNull HK> hashKeys); + /** + * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of + * the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * + * @param key must not be {@literal null}. + * @param expiration is optional. + * @param hashKeys must not be {@literal null}. + * @return list of values for the given fields or {@literal null} when used in pipeline / transaction. + * @since 4.0 + * @see Redis Documentation: HGETEX + */ + List getAndExpire(@NonNull H key, @Nullable Expiration expiration, @NonNull Collection<@NonNull HK> hashKeys); - /** - * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. - * - * @param key must not be {@literal null}. - * @param m must not be {@literal null}. - * @param condition is optional. - * @param expiration is optional. - * @return whether all fields were set or {@literal null} when used in pipeline / transaction. - * @since 4.0 - */ - Boolean putAndExpire(@NonNull H key, @NonNull Map m, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration); + /** + * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. + * + * @param key must not be {@literal null}. + * @param m must not be {@literal null}. + * @param condition must not be {@literal}. + * @param expiration is optional. + * @return whether all fields were set or {@literal null} when used in pipeline / transaction. + * @since 4.0 + *@see Redis Documentation: HSETEX + */ + Boolean putAndExpire(@NonNull H key, @NonNull Map m, + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration); /** * Increment {@code value} of a hash {@code hashKey} by the given {@code delta}. diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java index b7c69c2396..2c27b04352 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core; +import org.jspecify.annotations.NonNull; import org.springframework.data.redis.connection.RedisHashCommands; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -83,43 +84,43 @@ public interface ReactiveHashOperations { * @return */ Mono> multiGet(H key, Collection hashKeys); - - /** - * Get and remove the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the - * requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * When the last field is deleted, the key will also be deleted. - * - * @param key must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return never {@literal null}. - * @since 4.0 - */ - Mono> getAndDelete(H key, Collection hashKeys); - - /** - * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. - * - * @param key must not be {@literal null}. - * @param map must not be {@literal null}. - * @param condition is optional. - * @param expiration is optional. - * @return never {@literal null}. - * @since 4.0 - */ - Mono putAndExpire(H key, Map map, RedisHashCommands.HashFieldSetOption condition, - Expiration expiration); - - /** - * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the - * requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * - * @param key must not be {@literal null}. - * @param expiration is optional. - * @param hashKeys must not be {@literal null}. - * @return never {@literal null}. - * @since 4.0 - */ - Mono> getAndExpire(H key, Expiration expiration, Collection hashKeys); + + /** + * Get and remove the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the + * requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * When the last field is deleted, the key will also be deleted. + * + * @param key must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + */ + Mono> getAndDelete(H key, Collection hashKeys); + + /** + * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. + * + * @param key must not be {@literal null}. + * @param map must not be {@literal null}. + * @param condition must not be {@literal null}. + * @param expiration is optional. + * @return never {@literal null}. + * @since 4.0 + */ + Mono putAndExpire(H key, Map map, + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration); + + /** + * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the + * requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * + * @param key must not be {@literal null}. + * @param expiration is optional. + * @param hashKeys must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + */ + Mono> getAndExpire(H key, @Nullable Expiration expiration, Collection hashKeys); /** * Increment {@code value} of a hash {@code hashKey} by the given {@code delta}. diff --git a/src/main/java/org/springframework/data/redis/core/RedisCommand.java b/src/main/java/org/springframework/data/redis/core/RedisCommand.java index c0bc3f8a4d..99a7aeb8cf 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -147,7 +147,7 @@ public enum RedisCommand { HMSET("w", 3), // HPOP("rw", 3), HSET("w", 3, 3), // - HSETEX("w", 3), // + HSETEX("w", 3), // HSETNX("w", 3, 3), // HVALS("r", 1, 1), // HEXPIRE("w", 5), // diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index 056f443a93..272ceefca2 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -3727,156 +3727,68 @@ public void hTtlReturnsMinusTwoWhenFieldOrKeyMissing() { @Test // GH-3211 @EnabledOnCommand("HGETDEL") - public void hGetDelReturnsValueAndDeletesField() { + public void hGetDelWorksAsExpected() { - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetdel", "field-2", "value-2")); - actual.add(connection.hGetDel("hash-hgetdel", "field-1")); - actual.add(connection.hExists("hash-hgetdel", "field-1")); - actual.add(connection.hExists("hash-hgetdel", "field-2")); + connection.hSet("hash-hgetdel", "field-1", "value-1"); + connection.hSet("hash-hgetdel", "field-2", "value-2"); + connection.hSet("hash-hgetdel", "field-3", "value-3"); - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, List.of("value-1"), Boolean.FALSE, Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelReturnsNullWhenFieldDoesNotExist() { - - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hGetDel("hash-hgetdel", "missing-field")); - actual.add(connection.hExists("hash-hgetdel", "field-1")); - - verifyResults(Arrays.asList(Boolean.TRUE, Arrays.asList((Object) null), Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelReturnsNullWhenKeyDoesNotExist() { - - actual.add(connection.hGetDel("missing-hash", "field-1")); - - verifyResults(Arrays.asList(Arrays.asList((Object) null))); - } + // hgetdel first 2 fields + assertThat(connection.hGetDel("hash-hgetdel", "field-1", "field-2")) + .containsExactly("value-1", "value-2"); + assertThat(connection.hExists("hash-hgetdel", "field-1")).isFalse(); + assertThat(connection.hExists("hash-hgetdel", "field-2")).isFalse(); - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelMultipleFieldsReturnsValuesAndDeletesFields() { + // hgetdel non-existent field returns null + assertThat(connection.hGetDel("hash-hgetdel", "field-1")) + .containsExactly(null); - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetdel", "field-2", "value-2")); - actual.add(connection.hSet("hash-hgetdel", "field-3", "value-3")); - actual.add(connection.hGetDel("hash-hgetdel", "field-1", "field-2")); - actual.add(connection.hExists("hash-hgetdel", "field-1")); - actual.add(connection.hExists("hash-hgetdel", "field-2")); - actual.add(connection.hExists("hash-hgetdel", "field-3")); + // hgetdel last field + assertThat(connection.hGetDel("hash-hgetdel", "field-3")) + .containsExactly("value-3"); + assertThat(connection.hExists("hash-hgetdel", "field-3")).isFalse(); + assertThat(connection.exists("hash-hgetdel")).isFalse(); - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, - Arrays.asList("value-1", "value-2"), - Boolean.FALSE, Boolean.FALSE, Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelMultipleFieldsWithNonExistentFields() { - - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hGetDel("hash-hgetdel", "field-1", "missing-field")); - actual.add(connection.hExists("hash-hgetdel", "field-1")); - - verifyResults(Arrays.asList(Boolean.TRUE, - Arrays.asList("value-1", null), - Boolean.FALSE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelDeletesKeyWhenAllFieldsRemoved() { - - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetdel", "field-2", "value-2")); - actual.add(connection.hGetDel("hash-hgetdel", "field-1", "field-2")); - actual.add(connection.exists("hash-hgetdel")); - - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, - Arrays.asList("value-1", "value-2"), - Boolean.FALSE)); + // hgetdel non-existent hash returns null + assertThat(connection.hGetDel("hash-hgetdel", "field-1")) + .containsExactly(null); } @Test // GH-3211 @EnabledOnCommand("HGETEX") - public void hGetExReturnsValueAndSetsExpiration() { - - actual.add(connection.hSet("hash-hgetex", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetex", "field-2", "value-2")); - actual.add(connection.hGetEx("hash-hgetex", Expiration.seconds(60), "field-1")); - actual.add(connection.hExists("hash-hgetex", "field-1")); - actual.add(connection.hExists("hash-hgetex", "field-2")); - - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, List.of("value-1"), Boolean.TRUE, Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETEX") - public void hGetExReturnsNullWhenFieldDoesNotExist() { - - actual.add(connection.hSet("hash-hgetex", "field-1", "value-1")); - actual.add(connection.hGetEx("hash-hgetex", Expiration.seconds(60), "missing-field")); - actual.add(connection.hExists("hash-hgetex", "field-1")); - - verifyResults(Arrays.asList(Boolean.TRUE, Arrays.asList((Object) null), Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETEX") - public void hGetExReturnsNullWhenKeyDoesNotExist() { - - actual.add(connection.hGetEx("missing-hash", Expiration.seconds(60), "field-1")); - - verifyResults(Arrays.asList(Arrays.asList((Object) null))); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETEX") - public void hGetExMultipleFieldsReturnsValuesAndSetsExpiration() { + @LongRunningTest + public void hGetExWorksAsExpected() { - actual.add(connection.hSet("hash-hgetex", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetex", "field-2", "value-2")); - actual.add(connection.hSet("hash-hgetex", "field-3", "value-3")); - actual.add(connection.hGetEx("hash-hgetex", Expiration.seconds(120), "field-1", "field-2")); - actual.add(connection.hExists("hash-hgetex", "field-1")); - actual.add(connection.hExists("hash-hgetex", "field-2")); - actual.add(connection.hExists("hash-hgetex", "field-3")); + connection.hSet("hash-hgetex", "field-1", "value-1"); + connection.hSet("hash-hgetex", "field-2", "value-2"); + connection.hSet("hash-hgetex", "field-3", "value-3"); - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, - Arrays.asList("value-1", "value-2"), - Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)); - } + assertThat(connection.hGetEx("hash-hgetex", Expiration.seconds(2), "field-1", "field-2")) + .containsExactly("value-1", "value-2"); - @Test // GH-3211 - @EnabledOnCommand("HGETEX") - public void hGetExMultipleFieldsWithNonExistentFields() { + // non-existent field returns null + assertThat(connection.hGetEx("hash-hgetex", null, "no-such-field")).containsExactly(null); - actual.add(connection.hSet("hash-hgetex", "field-1", "value-1")); - actual.add(connection.hGetEx("hash-hgetex", Expiration.seconds(60), "field-1", "missing-field")); - actual.add(connection.hExists("hash-hgetex", "field-1")); + // non-existent hash returns null + assertThat(connection.hGetEx("no-such-key", null, "field-1")).containsExactly(null); - verifyResults(Arrays.asList(Boolean.TRUE, - Arrays.asList("value-1", null), - Boolean.TRUE)); + await().atMost(Duration.ofMillis(3000L)).until(() -> + !connection.hExists("hash-getex", "field-1") && !connection.hExists("hash-getex", "field-2")); } @Test // GH-3211 @EnabledOnCommand("HSETEX") - public void hSetExUpsertConditionSetsFieldsWithExpiration() { + @LongRunningTest + public void hSetExWorksAsExpected() { Map fieldMap = Map.of("field-1", "value-1", "field-2", "value-2"); - actual.add(connection.hSetEx("hash-hsetex", fieldMap, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.seconds(60))); - actual.add(connection.hExists("hash-hsetex", "field-1")); - actual.add(connection.hExists("hash-hsetex", "field-2")); - actual.add(connection.hGet("hash-hsetex", "field-1")); - actual.add(connection.hGet("hash-hsetex", "field-2")); + assertThat(connection.hSetEx("hash-hsetex", fieldMap, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.seconds(2))) + .isTrue(); + assertThat(connection.hGet("hash-hsetex", "field-1")).isEqualTo("value-1"); + assertThat(connection.hGet("hash-hsetex", "field-2")).isEqualTo("value-2"); - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, "value-1", "value-2")); + await().atMost(Duration.ofMillis(3000L)).until(() -> + !connection.hExists("hash-getex", "field-1") && !connection.hExists("hash-getex", "field-2")); } @Test // GH-3211 @@ -3933,50 +3845,6 @@ public void hSetExIfAllExistConditionFailsWhenSomeFieldsMissing() { verifyResults(Arrays.asList(Boolean.TRUE, Boolean.FALSE, "existing-value", Boolean.FALSE)); } - @Test // GH-3211 - @EnabledOnCommand("HSETEX") - public void hSetExWithDifferentExpirationPolicies() { - - // Test with seconds expiration - Map fieldMap1 = Map.of("field-1", "value-1"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap1, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.seconds(60))); - actual.add(connection.hExists("hash-hsetex-exp", "field-1")); - actual.add(connection.hGet("hash-hsetex-exp", "field-1")); - - // Test with milliseconds expiration - Map fieldMap2 = Map.of("field-2", "value-2"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap2, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.milliseconds(120000))); - actual.add(connection.hExists("hash-hsetex-exp", "field-2")); - actual.add(connection.hGet("hash-hsetex-exp", "field-2")); - - // Test with Duration expiration - Map fieldMap3 = Map.of("field-3", "value-3"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap3, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.from(Duration.ofMinutes(3)))); - actual.add(connection.hExists("hash-hsetex-exp", "field-3")); - actual.add(connection.hGet("hash-hsetex-exp", "field-3")); - - // Test with unix timestamp expiration (5 minutes from now) - long futureTimestamp = System.currentTimeMillis() / 1000 + 300; // 5 minutes from now - Map fieldMap4 = Map.of("field-4", "value-4"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap4, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.unixTimestamp(futureTimestamp, TimeUnit.SECONDS))); - actual.add(connection.hExists("hash-hsetex-exp", "field-4")); - actual.add(connection.hGet("hash-hsetex-exp", "field-4")); - - // Test with keepTtl expiration - Map fieldMap5 = Map.of("field-5", "value-5"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap5, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.keepTtl())); - actual.add(connection.hExists("hash-hsetex-exp", "field-5")); - actual.add(connection.hGet("hash-hsetex-exp", "field-5")); - - verifyResults(Arrays.asList( - Boolean.TRUE, Boolean.TRUE, "value-1", // seconds - Boolean.TRUE, Boolean.TRUE, "value-2", // milliseconds - Boolean.TRUE, Boolean.TRUE, "value-3", // Duration - Boolean.TRUE, Boolean.TRUE, "value-4", // unix timestamp - Boolean.TRUE, Boolean.TRUE, "value-5" // keepTtl - )); - } - @Test // DATAREDIS-694 void touchReturnsNrOfKeysTouched() { diff --git a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java index 5e9c1b38e6..0c14f0d90c 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java @@ -29,6 +29,7 @@ import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -61,6 +62,7 @@ import org.springframework.data.redis.connection.zset.DefaultTuple; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.StringRedisSerializer; /** @@ -541,6 +543,66 @@ public void testHVals() { verifyResults(Collections.singletonList(stringList)); } + @Test // GH-3211 + public void hGetDelBytes() { + doReturn(Collections.singletonList(barBytes)).when(nativeConnection).hGetDel(fooBytes, barBytes); + List deleted = connection.hGetDel(fooBytes, barBytes); + assertThat(deleted).containsExactly(barBytes); + } + + @Test // GH-3211 + public void hGetDel() { + doReturn(Collections.singletonList(barBytes)).when(nativeConnection).hGetDel(fooBytes, barBytes); + List deleted = connection.hGetDel(foo, bar); + assertThat(deleted).containsExactly(bar); + } + + @Test // GH-3211 + public void hGetExBytes() { + Expiration expiration = mock(); + doReturn(bytesList).when(nativeConnection).hGetEx(fooBytes, expiration, barBytes); + List values = connection.hGetEx(fooBytes, expiration, barBytes); + assertThat(values).containsExactly(barBytes); + } + + @Test // GH-3211 + public void hGetEx() { + Expiration expiration = mock(); + doReturn(bytesList).when(nativeConnection).hGetEx(fooBytes, expiration, barBytes); + List values = connection.hGetEx(foo, expiration, bar); + assertThat(values).containsExactly(bar); + } + + @Test // GH-3211 + public void hSetExBytes() { + Expiration expiration = mock(); + RedisHashCommands.HashFieldSetOption setOption = mock(); + Map fieldMap = Map.of(barBytes, bar2Bytes); + doReturn(Boolean.TRUE).when(nativeConnection).hSetEx(fooBytes, fieldMap, setOption, expiration); + assertThat(connection.hSetEx(fooBytes, fieldMap, setOption, expiration)).isTrue(); + } + + @Test // GH-3211 + public void hSetEx() { + Expiration expiration = mock(); + RedisHashCommands.HashFieldSetOption setOption = mock(); + Map stringMap = Map.of(bar, bar2); + doReturn(Boolean.TRUE).when(nativeConnection).hSetEx( + eq(fooBytes), + argThat(fieldMap -> isFieldMap(fieldMap, stringMap)), + eq(setOption), eq(expiration)); + assertThat(connection.hSetEx(foo, stringMap, setOption, expiration)).isTrue(); + } + + private boolean isFieldMap(Map fieldMap, Map stringMap) { + Map fieldMapAsStringMap = fieldMap.entrySet().stream() + .collect(Collectors.toMap( + entry -> new String(entry.getKey()), + entry -> new String(entry.getValue()) + )); + return fieldMapAsStringMap.equals(stringMap); + } + @Test public void testIncrBytes() { doReturn(2L).when(nativeConnection).incr(fooBytes); 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 aea8fdc6bd..754f17e889 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -445,17 +445,17 @@ public List hMGet(byte[] key, byte[]... fields) { return delegate.hMGet(key, fields); } - public List hGetDel(byte[] key, byte[]... fields) { - return delegate.hGetDel(key, fields); - } + public List hGetDel(byte[] key, byte[]... fields) { + return delegate.hGetDel(key, fields); + } - public List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { - return delegate.hGetEx(key, expiration, fields); - } + public List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { + return delegate.hGetEx(key, expiration, fields); + } - public Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { - return delegate.hSetEx(key, hashes, condition, expiration); - } + public Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { + return delegate.hSetEx(key, hashes, condition, expiration); + } public Long zRem(byte[] key, byte[]... values) { return delegate.zRem(key, values); diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java index 40eafd76c3..02c86c79f2 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java @@ -26,8 +26,12 @@ import redis.clients.jedis.Protocol; import redis.clients.jedis.params.GetExParams; +import redis.clients.jedis.params.HGetExParams; +import redis.clients.jedis.params.HSetExParams; import redis.clients.jedis.params.SetParams; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -35,11 +39,11 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; - import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; - import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.core.types.Expiration; @@ -405,4 +409,153 @@ private Map getRedisServerInfoMap(String name, int port) { map.put("parallel-syncs", "1"); return map; } + + @Nested + class ToHGetExParamsShould { + + @Test + void notSetAnyFieldsForNullExpiration() { + + assertThatParamsHasExpiration(JedisConverters.toHGetExParams(null), null, null); + } + + @Test + void setPersistForNonExpiringExpiration() { + + assertThatParamsHasExpiration(JedisConverters.toHGetExParams(Expiration.persistent()), Protocol.Keyword.PERSIST, + null); + } + + @Test + void setPxForExpirationWithMillisTimeUnit() { + + HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30_000, TimeUnit.MILLISECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.PX, 30_000L); + } + + @Test + void setPxAtForExpirationWithMillisUnixTimestamp() { + + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + HGetExParams params = JedisConverters.toHGetExParams( + Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.PXAT, fourHoursFromNowMillis); + } + + @Test + void setExForExpirationWithNonMillisTimeUnit() { + + HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30, TimeUnit.SECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.EX, 30L); + } + + @Test + void setExAtForExpirationWithNonMillisUnixTimestamp() { + + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + HGetExParams params = JedisConverters.toHGetExParams( + Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.EXAT, fourHoursFromNowSecs); + } + + private void assertThatParamsHasExpiration(HGetExParams params, Protocol.Keyword expirationType, + Long expirationValue) { + assertThat(params).extracting("expiration", "expirationValue").containsExactly(expirationType, expirationValue); + } + } + + @Nested + class ToHSetExParamsShould { + + @Test + void setFnxForNoneExistCondition() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.IF_NONE_EXIST, null); + assertThatParamsHasExistance(params, Protocol.Keyword.FNX); + } + + @Test + void setFxxForAllExistCondition() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.IF_ALL_EXIST, null); + assertThatParamsHasExistance(params, Protocol.Keyword.FXX); + } + + @Test + void notSetFnxNorFxxForUpsertCondition() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); + assertThatParamsHasExistance(params, null); + } + + @Test + void notSetAnyTimeFieldsForNullExpiration() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); + assertThatParamsHasExpiration(params, null, null); + } + + @Test + void notSetAnyTimeFieldsForNonExpiringExpiration() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.persistent()); + assertThatParamsHasExpiration(params, null, null); + } + + @Test + void setKeepTtlForKeepTtlExpiration() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.keepTtl()); + assertThatParamsHasExpiration(params, Protocol.Keyword.KEEPTTL, null); + } + + @Test + void setPxForExpirationWithMillisTimeUnit() { + + Expiration expiration = Expiration.from(30_000, TimeUnit.MILLISECONDS); + assertThatParamsHasExpiration( + JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), Protocol.Keyword.PX, + 30_000L); + } + + @Test + void setPxAtForExpirationWithMillisUnixTimestamp() { + + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS); + assertThatParamsHasExpiration( + JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), + Protocol.Keyword.PXAT, fourHoursFromNowMillis); + } + + @Test + void setExForExpirationWithNonMillisTimeUnit() { + + Expiration expiration = Expiration.from(30, TimeUnit.SECONDS); + assertThatParamsHasExpiration( + JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), Protocol.Keyword.EX, + 30L); + } + + @Test + void setExAtForExpirationWithNonMillisUnixTimestamp() { + + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS); + assertThatParamsHasExpiration( + JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), + Protocol.Keyword.EXAT, fourHoursFromNowSecs); + } + + private void assertThatParamsHasExistance(HSetExParams params, Protocol.Keyword existance) { + assertThat(params).extracting("existance").isEqualTo(existance); + } + + private void assertThatParamsHasExpiration(HSetExParams params, Protocol.Keyword expirationType, + Long expirationValue) { + assertThat(params).extracting("expiration", "expirationValue").containsExactly(expirationType, expirationValue); + } + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index 74cb969dc6..f7826377db 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -27,16 +27,20 @@ import io.lettuce.core.cluster.models.partitions.Partitions; import io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; +import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; @@ -329,4 +333,131 @@ void sentinelConfigurationShouldNotSetSentinelAuthIfUsernameIsPresentWithNoPassw assertThat(sentinel.getPassword()).isNull(); }); } + + @Nested // GH-3211 + class ToHGetExArgsShould { + + @Test + void notSetAnyFieldsForNullExpiration() { + + assertThat(LettuceConverters.toHGetExArgs(null)).extracting("ex", "exAt", "px", "pxAt", "persist") + .containsExactly(null, null, null, null, Boolean.FALSE); + } + + @Test + void setPersistForNonExpiringExpiration() { + + assertThat(LettuceConverters.toHGetExArgs(Expiration.persistent())).extracting("persist").isEqualTo(Boolean.TRUE); + } + + @Test + void setPxForExpirationWithMillisTimeUnit() { + + assertThat(LettuceConverters.toHGetExArgs(Expiration.from(30_000, TimeUnit.MILLISECONDS))).extracting("px") + .isEqualTo(30_000L); + } + + @Test + void setPxAtForExpirationWithMillisUnixTimestamp() { + + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + assertThat(LettuceConverters.toHGetExArgs( + Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS))).extracting("pxAt") + .isEqualTo(fourHoursFromNowMillis); + } + + @Test + void setExForExpirationWithNonMillisTimeUnit() { + + assertThat(LettuceConverters.toHGetExArgs(Expiration.from(30, TimeUnit.SECONDS))).extracting("ex").isEqualTo(30L); + } + + @Test + void setExAtForExpirationWithNonMillisUnixTimestamp() { + + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + assertThat( + LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS))).extracting( + "exAt").isEqualTo(fourHoursFromNowSecs); + } + } + + @Nested + class ToHSetExArgsShould { + + @Test + void setFnxForNoneExistCondition() { + + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.IF_NONE_EXIST, null)).extracting( + "fnx").isEqualTo(Boolean.TRUE); + } + + @Test + void setFxxForAllExistCondition() { + + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.IF_ALL_EXIST, null)).extracting( + "fxx").isEqualTo(Boolean.TRUE); + } + + @Test + void notSetFnxNorFxxForUpsertCondition() { + + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)).extracting("fnx", + "fxx").containsExactly(Boolean.FALSE, Boolean.FALSE); + } + + @Test + void notSetAnyTimeFieldsForNullExpiration() { + + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)).extracting("ex", + "exAt", "px", "pxAt").containsExactly(null, null, null, null); + } + + @Test + void notSetAnyTimeFieldsForNonExpiringExpiration() { + + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.persistent())).extracting("ex", "exAt", "px", "pxAt").containsExactly(null, null, null, null); + } + + @Test + void setKeepTtlForKeepTtlExpiration() { + + assertThat( + LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.keepTtl())).extracting( + "keepttl").isEqualTo(Boolean.TRUE); + } + + @Test + void setPxForExpirationWithMillisTimeUnit() { + + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.from(30_000, TimeUnit.MILLISECONDS))).extracting("px").isEqualTo(30_000L); + } + + @Test + void setPxAtForExpirationWithMillisUnixTimestamp() { + + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS); + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, expiration)).extracting( + "pxAt").isEqualTo(fourHoursFromNowMillis); + } + + @Test + void setExForExpirationWithNonMillisTimeUnit() { + + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.from(30, TimeUnit.SECONDS))).extracting("ex").isEqualTo(30L); + } + + @Test + void setExAtForExpirationWithNonMillisUnixTimestamp() { + + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS); + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, expiration)).extracting( + "exAt").isEqualTo(fourHoursFromNowSecs); + } + } }