From 0b8fbae2c320edbc77f5b62286b5bba90b4b09ff Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 25 Jun 2021 11:55:14 +0200 Subject: [PATCH] Add support for HRANDFIELD. Closes: #2048 Original Pull Request: #2104 --- src/main/asciidoc/new-features.adoc | 2 +- .../DefaultStringRedisConnection.java | 121 ++++++++++++--- .../connection/DefaultedRedisConnection.java | 28 ++++ .../connection/ReactiveHashCommands.java | 139 ++++++++++++++++++ .../redis/connection/RedisHashCommands.java | 52 +++++++ .../connection/StringRedisConnection.java | 52 +++++++ .../redis/connection/convert/Converters.java | 40 +++++ .../jedis/JedisClusterHashCommands.java | 69 +++++++++ .../connection/jedis/JedisHashCommands.java | 64 ++++++++ .../lettuce/LettuceHashCommands.java | 61 ++++++++ .../lettuce/LettuceReactiveHashCommands.java | 34 +++++ .../data/redis/core/AbstractOperations.java | 8 + .../data/redis/core/BoundHashOperations.java | 45 ++++++ .../core/DefaultBoundHashOperations.java | 40 +++++ .../redis/core/DefaultHashOperations.java | 63 ++++++++ .../core/DefaultReactiveHashOperations.java | 52 +++++++ .../data/redis/core/HashOperations.java | 49 ++++++ .../redis/core/ReactiveHashOperations.java | 48 ++++++ .../AbstractConnectionIntegrationTests.java | 46 ++++++ .../connection/RedisConnectionUnitTests.java | 16 ++ ...DefaultHashOperationsIntegrationTests.java | 39 +++++ ...eactiveHashOperationsIntegrationTests.java | 76 ++++++++++ 22 files changed, 1125 insertions(+), 19 deletions(-) diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 920916f20..ca9cf4962 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,7 +7,7 @@ This section briefly covers items that are new and noteworthy in the latest rele == New in Spring Data Redis 2.6 * Support for `SubscriptionListener` when using `MessageListener` for subscription confirmation callbacks. `ReactiveRedisMessageListenerContainer` and `ReactiveRedisOperations` provide `receiveLater(…)` and `listenToLater(…)` methods to await until Redis acknowledges the subscription. -* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `COPY`, `GETEX`, `GETDEL`, `ZPOPMIN`, `BZPOPMIN`, `ZPOPMAX`, `BZPOPMAX`, `ZMSCORE`, `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZUNION`). +* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `COPY`, `GETEX`, `GETDEL`, `ZPOPMIN`, `BZPOPMIN`, `ZPOPMAX`, `BZPOPMAX`, `ZMSCORE`, `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZUNION`, `HRANDFIELD`). [[new-in-2.5.0]] == New in Spring Data Redis 2.5 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 34222cb93..095e7e505 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -106,6 +106,26 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco @SuppressWarnings("rawtypes") private Queue txConverters = new LinkedList<>(); private boolean deserializePipelineAndTxResults = false; + private Entry convertEntry(Entry source) { + return new Entry() { + + @Override + public String getKey() { + return bytesToString.convert(source.getKey()); + } + + @Override + public String getValue() { + return bytesToString.convert(source.getValue()); + } + + @Override + public String setValue(String value) { + throw new UnsupportedOperationException("Cannot set value for entry"); + } + }; + } + private class DeserializingConverter implements Converter { public String convert(byte[] source) { return serializer.deserialize(source); @@ -2307,6 +2327,88 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return hIncrBy(serialize(key), serialize(field), delta); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandField(byte[]) + */ + @Nullable + @Override + public byte[] hRandField(byte[] key) { + return convertAndReturn(delegate.hRandField(key), Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandFieldWithValues(byte[]) + */ + @Nullable + @Override + public Entry hRandFieldWithValues(byte[] key) { + return convertAndReturn(delegate.hRandFieldWithValues(key), Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandField(byte[], long) + */ + @Nullable + @Override + public List hRandField(byte[] key, long count) { + return convertAndReturn(delegate.hRandField(key, count), Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandFieldWithValues(byte[], long) + */ + @Nullable + @Override + public List> hRandFieldWithValues(byte[] key, long count) { + return convertAndReturn(delegate.hRandFieldWithValues(key, count), Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#hRandField(java.lang.String) + */ + @Nullable + @Override + public String hRandField(String key) { + return convertAndReturn(delegate.hRandField(serialize(key)), bytesToString); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#hRandFieldWithValues(java.lang.String) + */ + @Nullable + @Override + public Entry hRandFieldWithValues(String key) { + return convertAndReturn(delegate.hRandFieldWithValues(serialize(key)), + (Converter, Entry>) this::convertEntry); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#hRandField(java.lang.String, long) + */ + @Nullable + @Override + public List hRandField(String key, long count) { + return convertAndReturn(delegate.hRandField(serialize(key), count), byteListToStringList); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#hRandFieldWithValues(java.lang.String, long) + */ + @Nullable + @Override + public List> hRandFieldWithValues(String key, long count) { + return convertAndReturn(delegate.hRandFieldWithValues(serialize(key), count), + new ListConverter<>(this::convertEntry)); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#hKeys(java.lang.String) @@ -3924,24 +4026,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco @Override public Cursor> hScan(String key, ScanOptions options) { - return new ConvertingCursor<>(this.delegate.hScan(this.serialize(key), options), - source -> new Entry() { - - @Override - public String getKey() { - return bytesToString.convert(source.getKey()); - } - - @Override - public String getValue() { - return bytesToString.convert(source.getValue()); - } - - @Override - public String setValue(String value) { - throw new UnsupportedOperationException("Cannot set value for entry in cursor"); - } - }); + return new ConvertingCursor<>(this.delegate.hScan(this.serialize(key), options), this::convertEntry); } /* 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 d7b2f346a..7d39e7479 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -1329,6 +1329,34 @@ public interface DefaultedRedisConnection extends RedisConnection { return hashCommands().hIncrBy(key, field, delta); } + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default byte[] hRandField(byte[] key) { + return hashCommands().hRandField(key); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default Entry hRandFieldWithValues(byte[] key) { + return hashCommands().hRandFieldWithValues(key); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hRandField(byte[] key, long count) { + return hashCommands().hRandField(key, count); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List> hRandFieldWithValues(byte[] key, long count) { + return hashCommands().hRandFieldWithValues(key, count); + } + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated 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 5e53d0d62..765634017 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -512,6 +512,145 @@ public interface ReactiveHashCommands { */ Flux> hLen(Publisher commands); + /** + * {@literal HRANDFIELD} {@link Command}. + * + * @author Mark Paluch + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + class HRandFieldCommand extends KeyCommand { + + private long count; + + private HRandFieldCommand(@Nullable ByteBuffer key, long count) { + + super(key); + + this.count = count; + } + + /** + * 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 HRandFieldCommand} with {@literal key} applied. + */ + public static HRandFieldCommand key(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return new HRandFieldCommand(key, 1); + } + + /** + * Applies the {@literal count}. Constructs a new command instance with all previously configured properties. If the + * provided {@code count} argument is positive, return a list of distinct fields, capped either at {@code count} or + * the hash size. If {@code count} is negative, the behavior changes and the command is allowed to return the same + * field multiple times. In this case, the number of returned fields is the absolute value of the specified count. + * + * @param count + * @return a new {@link HRandFieldCommand} with {@literal key} applied. + */ + public HRandFieldCommand count(long count) { + return new HRandFieldCommand(getKey(), count); + } + + public long getCount() { + return count; + } + } + + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + default Mono hRandField(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return hRandField(Mono.just(HRandFieldCommand.key(key).count(1))).flatMap(CommandResponse::getOutput).next(); + } + + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + default Mono> hRandFieldWithValues(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return hRandFieldWithValues(Mono.just(HRandFieldCommand.key(key).count(1))).flatMap(CommandResponse::getOutput) + .next(); + } + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + default Flux hRandField(ByteBuffer key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return hRandField(Mono.just(HRandFieldCommand.key(key).count(count))).flatMap(CommandResponse::getOutput); + } + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + default Flux> hRandFieldWithValues(ByteBuffer key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return hRandFieldWithValues(Mono.just(HRandFieldCommand.key(key).count(count))).flatMap(CommandResponse::getOutput); + } + + /** + * Get random fields of hash at {@literal key}. + * + * @param commands must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + Flux>> hRandField(Publisher commands); + + /** + * Get random fields along their values of hash at {@literal key}. + * + * @param commands must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + Flux>>> hRandFieldWithValues( + Publisher commands); + /** * Get key set (fields) of hash at {@literal key}. * 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 f55151622..bf083b978 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -173,6 +173,58 @@ public interface RedisHashCommands { @Nullable Map hGetAll(byte[] key); + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + byte[] hRandField(byte[] key); + + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + Map.Entry hRandFieldWithValues(byte[] key); + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + List hRandField(byte[] key, long count); + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + List> hRandFieldWithValues(byte[] key, long count); + /** * Use a {@link Cursor} to iterate over entries in hash at {@code key}. * 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 469aeed49..156af1909 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -1964,6 +1964,58 @@ public interface StringRedisConnection extends RedisConnection { */ Double hIncrBy(String key, String field, double delta); + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + String hRandField(String key); + + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + Map.Entry hRandFieldWithValues(String key); + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + List hRandField(String key, long count); + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + List> hRandFieldWithValues(String key, long count); + /** * Determine if given hash {@code field} exists. * diff --git a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java index ab679f81a..80fc7dbe1 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java @@ -461,6 +461,20 @@ abstract public class Converters { return source; } + /** + * Create an {@link Map.Entry} from {@code key} and {@code value}. + * + * @param key + * @param value + * @param + * @param + * @return + * @since 2.6 + */ + public static Map.Entry entryOf(K key, V value) { + return new SimpleEntry<>(key, value); + } + /** * @author Christoph Strobl * @since 1.8 @@ -639,4 +653,30 @@ abstract public class Converters { } } + + private static class SimpleEntry implements Map.Entry { + + private final K key; + private final V value; + + public SimpleEntry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + } } 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 8149dad35..43ffee1cd 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 @@ -30,6 +30,7 @@ import org.springframework.data.redis.core.Cursor; 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.util.Streamable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -167,6 +168,74 @@ class JedisClusterHashCommands implements RedisHashCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandField(byte[]) + */ + @Nullable + @Override + public byte[] hRandField(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + try { + return connection.getCluster().hrandfield(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandFieldWithValues(byte[]) + */ + @Nullable + @Override + public Entry hRandFieldWithValues(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + try { + Map map = connection.getCluster().hrandfieldWithValues(key, 1); + return map.isEmpty() ? null : map.entrySet().iterator().next(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandField(byte[], long) + */ + @Nullable + @Override + public List hRandField(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + try { + return connection.getCluster().hrandfield(key, count); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandFieldWithValues(byte[], long) + */ + @Nullable + @Override + public List> hRandFieldWithValues(byte[] key, long count) { + + try { + Map map = connection.getCluster().hrandfieldWithValues(key, count); + return Streamable.of(() -> map.entrySet().iterator()).toList(); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisHashCommands#hExists(byte[], byte[]) 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 57729efbc..9adb2486d 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 @@ -20,12 +20,14 @@ import redis.clients.jedis.MultiKeyPipelineBase; import redis.clients.jedis.ScanParams; import redis.clients.jedis.ScanResult; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.springframework.data.redis.connection.RedisHashCommands; +import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.KeyBoundCursor; import org.springframework.data.redis.core.ScanIteration; @@ -127,6 +129,67 @@ class JedisHashCommands implements RedisHashCommands { return connection.invoke().just(BinaryJedis::hgetAll, MultiKeyPipelineBase::hgetAll, key); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandField(byte[]) + */ + @Nullable + @Override + public byte[] hRandField(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(BinaryJedis::hrandfield, MultiKeyPipelineBase::hrandfield, key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandFieldWithValues(byte[]) + */ + @Nullable + @Override + public Entry hRandFieldWithValues(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke() + .from(BinaryJedis::hrandfieldWithValues, MultiKeyPipelineBase::hrandfieldWithValues, key, 1L) + .get(it -> it.isEmpty() ? null : it.entrySet().iterator().next()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandField(byte[], long) + */ + @Nullable + @Override + public List hRandField(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(BinaryJedis::hrandfield, MultiKeyPipelineBase::hrandfield, key, count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandFieldWithValues(byte[], long) + */ + @Nullable + @Override + public List> hRandFieldWithValues(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke() + .from(BinaryJedis::hrandfieldWithValues, MultiKeyPipelineBase::hrandfieldWithValues, key, count).get(it -> { + + List> entries = new ArrayList<>(it.size()); + it.forEach((k, v) -> entries.add(Converters.entryOf(k, v))); + + return entries; + }); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisHashCommands#hIncrBy(byte[], byte[], long) @@ -280,4 +343,5 @@ class JedisHashCommands implements RedisHashCommands { private boolean isQueueing() { return connection.isQueueing(); } + } 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 dd210fad7..aa8172a0d 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.KeyValue; import io.lettuce.core.MapScanCursor; import io.lettuce.core.ScanArgs; import io.lettuce.core.api.async.RedisHashAsyncCommands; @@ -25,6 +26,7 @@ import java.util.Map.Entry; import java.util.Set; import org.springframework.data.redis.connection.RedisHashCommands; +import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.KeyBoundCursor; import org.springframework.data.redis.core.ScanIteration; @@ -124,6 +126,60 @@ class LettuceHashCommands implements RedisHashCommands { return connection.invoke().just(RedisHashAsyncCommands::hgetall, key); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandField(byte[]) + */ + @Nullable + @Override + public byte[] hRandField(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(RedisHashAsyncCommands::hrandfield, key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandFieldWithValues(byte[]) + */ + @Nullable + @Override + public Entry hRandFieldWithValues(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().from(RedisHashAsyncCommands::hrandfieldWithvalues, key) + .get(LettuceHashCommands::toEntry); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandField(byte[], long) + */ + @Nullable + @Override + public List hRandField(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(RedisHashAsyncCommands::hrandfield, key, count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hRandFieldWithValues(byte[], long) + */ + @Nullable + @Override + public List> hRandFieldWithValues(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().fromMany(RedisHashAsyncCommands::hrandfieldWithvalues, key, count) + .toList(LettuceHashCommands::toEntry); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisHashCommands#hIncrBy(byte[], byte[], long) @@ -275,4 +331,9 @@ class LettuceHashCommands implements RedisHashCommands { return connection.invoke().just(RedisHashAsyncCommands::hstrlen, key, field); } + @Nullable + private static Entry toEntry(KeyValue value) { + return value.hasValue() ? Converters.entryOf(value.getKey(), value.getValue()) : null; + } + } 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 7c7a58f60..b1103de1e 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 @@ -36,6 +36,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyComm import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyScanCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.convert.Converters; import org.springframework.util.Assert; /** @@ -164,6 +165,39 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { })); } + @Override + public Flux>> hRandField(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).map(command -> { + + Assert.notNull(command.getKey(), "Command.getKey() must not be null!"); + + return new CommandResponse<>(command, cmd.hrandfield(command.getKey(), command.getCount())); + })); + } + + @Override + public Flux>>> hRandFieldWithValues( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).map(command -> { + + Assert.notNull(command.getKey(), "Command.getKey() must not be null!"); + + Flux> flux = cmd.hrandfieldWithvalues(command.getKey(), command.getCount()) + .handle((it, sink) -> { + + if (it.isEmpty()) { + return; + } + + sink.next(Converters.entryOf(it.getKey(), it.getValue())); + }); + + return new CommandResponse<>(command, flux); + })); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveHashCommands#hKeys(org.reactivestreams.Publisher) diff --git a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java index 4cd8a942d..3ec786953 100644 --- a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java +++ b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java @@ -282,6 +282,14 @@ abstract class AbstractOperations { return SerializationUtils.deserialize(rawKeys, hashKeySerializer()); } + @SuppressWarnings("unchecked") + List deserializeHashKeys(List rawKeys) { + if (hashKeySerializer() == null) { + return (List) rawKeys; + } + return SerializationUtils.deserialize(rawKeys, hashKeySerializer()); + } + @SuppressWarnings("unchecked") List deserializeHashValues(List rawValues) { if (hashValueSerializer() == null) { 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 ca7483dbc..47ce81ca5 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -88,6 +88,51 @@ public interface BoundHashOperations extends BoundKeyOperations { @Nullable Double increment(HK key, double delta); + /** + * Return a random field from the hash value stored at the bound key. + * + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + HK randomField(); + + /** + * Return a random field from the hash value stored at the bound key. + * + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + Map.Entry randomValue(); + + /** + * Return a random field from the hash value stored at the bound key. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param count number of fields to return. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + List randomFields(long count); + + /** + * Return a random field from the hash value stored at the bound key. + * + * @param count number of fields to return. Must be positive. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + Map randomValues(long count); + /** * Get key set (fields) of hash at the bound key. * diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java index 6b2ceb4b6..0493c294e 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java @@ -110,6 +110,46 @@ class DefaultBoundHashOperations extends DefaultBoundKeyOperations return ops.increment(getKey(), key, delta); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundHashOperations#randomField() + */ + @Nullable + @Override + public HK randomField() { + return ops.randomField(getKey()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundHashOperations#randomValue() + */ + @Nullable + @Override + public Entry randomValue() { + return ops.randomValue(getKey()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundHashOperations#randomFields(long) + */ + @Nullable + @Override + public List randomFields(long count) { + return ops.randomFields(getKey(), count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundHashOperations#randomValues(long) + */ + @Nullable + @Override + public Map randomValues(long count) { + return ops.randomValues(getKey(), count); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.BoundHashOperations#keys() 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 bb3ba72b9..dc096ffb8 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -24,7 +24,9 @@ import java.util.Map.Entry; import java.util.Set; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.redis.connection.convert.Converters; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Default implementation of {@link HashOperations}. @@ -91,6 +93,67 @@ class DefaultHashOperations extends AbstractOperations imp return execute(connection -> connection.hIncrBy(rawKey, rawHashKey, delta), true); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.HashOperations#randomField(java.lang.Object) + */ + @Nullable + @Override + public HK randomField(K key) { + + byte[] rawKey = rawKey(key); + return deserializeHashKey(execute(connection -> connection.hRandField(rawKey), true)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.HashOperations#randomValue(java.lang.Object) + */ + @Nullable + @Override + public Entry randomValue(K key) { + + byte[] rawKey = rawKey(key); + Entry rawEntry = execute(connection -> connection.hRandFieldWithValues(rawKey), true); + return rawEntry == null ? null + : Converters.entryOf(deserializeHashKey(rawEntry.getKey()), deserializeHashValue(rawEntry.getValue())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.HashOperations#randomFields(java.lang.Object, long) + */ + @Nullable + @Override + public List randomFields(K key, long count) { + + byte[] rawKey = rawKey(key); + List rawValues = execute(connection -> connection.hRandField(rawKey, count), true); + return deserializeHashKeys(rawValues); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.HashOperations#randomValues(java.lang.Object, long) + */ + @Nullable + @Override + public Map randomValues(K key, long count) { + + Assert.isTrue(count > 0, "Count must not be negative"); + byte[] rawKey = rawKey(key); + List> rawEntries = execute(connection -> connection.hRandFieldWithValues(rawKey, count), + true); + + if (rawEntries == null) { + return null; + } + + Map rawMap = new LinkedHashMap<>(rawEntries.size()); + rawEntries.forEach(entry -> rawMap.put(entry.getKey(), entry.getValue())); + return deserializeHashMap(rawMap); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.HashOperations#keys(java.lang.Object) 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 f2ac85714..a2fa529d5 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java @@ -144,6 +144,58 @@ class DefaultReactiveHashOperations implements ReactiveHashOperations .hIncrBy(rawKey(key), rawHashKey(hashKey), delta)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#randomField(H) + */ + @Override + public Mono randomField(H key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection // + .hashCommands().hRandField(rawKey(key))).map(this::readHashKey); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#randomValue(H) + */ + @Override + public Mono> randomValue(H key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection // + .hashCommands().hRandFieldWithValues(rawKey(key))).map(this::deserializeHashEntry); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#randomFields(H, long) + */ + @Override + public Flux randomFields(H key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createFlux(connection -> connection // + .hashCommands().hRandField(rawKey(key), count)).map(this::readHashKey); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#randomValues(H, long) + */ + @Override + public Flux> randomValues(H key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createFlux(connection -> connection // + .hashCommands().hRandFieldWithValues(rawKey(key), count)).map(this::deserializeHashEntry); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.ReactiveHashOperations#keys(java.lang.Object) 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 a4e474fc9..60b763e16 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -88,6 +88,55 @@ public interface HashOperations { */ Double increment(H key, HK hashKey, double delta); + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + HK randomField(H key); + + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + Map.Entry randomValue(H key); + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + List randomFields(H key, long count); + + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. Must be positive. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + @Nullable + Map randomValues(H key, long count); + /** * Get key set (fields) of hash at {@code key}. * 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 4d6f08777..562f6647d 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java @@ -87,6 +87,54 @@ public interface ReactiveHashOperations { */ Mono increment(H key, HK hashKey, double delta); + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + Mono randomField(H key); + + /** + * Return a random field from the hash value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + Mono> randomValue(H key); + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + Flux randomFields(H key, long count); + + /** + * Return a random field from the hash value stored at {@code key}. If the provided {@code count} argument is + * positive, return a list of distinct fields, capped either at {@code count} or the hash size. If {@code count} is + * negative, the behavior changes and the command is allowed to return the same field multiple times. In this case, + * the number of returned fields is the absolute value of the specified count. + * + * @param key must not be {@literal null}. + * @param count number of fields to return. + * @return {@literal null} if key does not exist or when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: HRANDFIELD + */ + Flux> randomValues(H key, long count); + /** * Get key set (fields) of hash at {@code key}. * 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 0aeb887d0..dbdc8742f 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -1269,6 +1269,52 @@ public abstract class AbstractConnectionIntegrationTests { verifyResults(Arrays.asList(new Object[] { true, largeNumber, -largeNumber })); } + @Test // GH-2048 + @EnabledOnCommand("HRANDFIELD") + void testHRandField() { + + String key = "hash"; + Map hash = new HashMap<>(); + hash.put("key1", "val1"); + hash.put("key2", "val2"); + hash.put("key3", "val3"); + hash.put("key4", "val4"); + + connection.hMSet(key, hash); + actual.add(connection.hRandField(key)); + actual.add(connection.hRandField(key, 2)); + actual.add(connection.hRandField(key, -10)); + + List results = getResults(); + assertThat((String) results.get(0)).isIn(hash.keySet()); + assertThat((List) results.get(1)).hasSize(2); + assertThat((List) results.get(2)).hasSize(10); + } + + @Test // GH-2048 + @EnabledOnCommand("HRANDFIELD") + void testHRandFieldWithValues() { + + String key = "hash"; + Map hash = new HashMap<>(); + hash.put("key1", "val1"); + hash.put("key2", "val2"); + hash.put("key3", "val3"); + hash.put("key4", "val4"); + + connection.hMSet(key, hash); + actual.add(connection.hRandFieldWithValues(key)); + actual.add(connection.hRandFieldWithValues(key, 2)); + actual.add(connection.hRandFieldWithValues(key, -10)); + + List results = getResults(); + assertThat(results.get(0)).isNotNull(); + assertThat((List) results.get(1)).hasSize(2); + + // Oh Jedis, JedisByteHashMap.ByteArrayWrapper. Why? + assertThat((List) results.get(2)).hasSizeGreaterThan(2); + } + @Test void testIncDecr() { 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 7f787b794..6f81c62e8 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -657,6 +657,22 @@ class RedisConnectionUnitTests { return delegate.hGetAll(key); } + public byte[] hRandField(byte[] key) { + return delegate.hRandField(key); + } + + public Entry hRandFieldWithValues(byte[] key) { + return delegate.hRandFieldWithValues(key); + } + + public List hRandField(byte[] key, long count) { + return delegate.hRandField(key, count); + } + + public List> hRandFieldWithValues(byte[] key, long count) { + return delegate.hRandFieldWithValues(key, count); + } + public Boolean move(byte[] key, int dbIndex) { return delegate.move(key, dbIndex); } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java index d9ce821d2..d023f082d 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java @@ -162,4 +162,43 @@ public class DefaultHashOperationsIntegrationTests { assertThat(hashOps.lengthOfValue(key, key1)).isEqualTo(Long.valueOf(val1.toString().length())); } + @ParameterizedRedisTest // GH-2048 + void randomField() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + assertThat(hashOps.randomField(key)).isIn(key1, key2); + assertThat(hashOps.randomFields(key, 2)).hasSize(2).contains(key1, key2); + } + + @ParameterizedRedisTest // GH-2048 + void randomValue() { + + assumeThat(hashKeyFactory).isNotInstanceOf(RawObjectFactory.class); + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + Map.Entry entry = hashOps.randomValue(key); + + if (entry.getKey().equals(key1)) { + assertThat(entry.getValue()).isEqualTo(val1); + } else { + assertThat(entry.getValue()).isEqualTo(val2); + } + + Map values = hashOps.randomValues(key, 10); + assertThat(values).hasSize(2).containsEntry(key1, val1).containsEntry(key2, val2); + } } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java index 3a46ee165..171e6ec1e 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java @@ -38,6 +38,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.LettuceTestClientResources; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -228,6 +229,81 @@ public class DefaultReactiveHashOperationsIntegrationTests { .verifyComplete(); } + @ParameterizedRedisTest // GH-2048 + @EnabledOnCommand("HRANDFIELD") + void randomField() { + + assumeThat(hashValueFactory).isNotInstanceOf(RawObjectFactory.class); + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + hashOperations.put(key, hashkey1, hashvalue1) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + hashOperations.put(key, hashkey2, hashvalue2) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + hashOperations.randomField(key) // + .as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual).isIn(hashkey1, hashkey2); + }).verifyComplete(); + + hashOperations.randomFields(key, -10) // + .collectList().as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual).hasSize(10); + }).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2048 + @EnabledOnCommand("HRANDFIELD") + void randomValue() { + + assumeThat(hashValueFactory).isNotInstanceOf(RawObjectFactory.class); + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + hashOperations.put(key, hashkey1, hashvalue1) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + hashOperations.put(key, hashkey2, hashvalue2) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + hashOperations.randomValue(key) // + .as(StepVerifier::create) // + .assertNext(actual -> { + + if (actual.getKey().equals(hashkey1)) { + assertThat(actual.getValue()).isEqualTo(hashvalue1); + } else { + assertThat(actual.getValue()).isEqualTo(hashvalue2); + } + }).verifyComplete(); + + hashOperations.randomValues(key, -10) // + .collectList().as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual).hasSize(10); + }).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 @SuppressWarnings("unchecked") void incrementDouble() {