diff --git a/pom.xml b/pom.xml index 8657b1992a..563737e979 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 2.6.0-SNAPSHOT + 2.6.0-2049-SNAPSHOT Spring Data Redis diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index eab00c043a..b117424c9b 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`). +* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `COPY`, `GETEX`, `GETDEL`, `HRANDFIELD`, `ZRANDMEMBER`). [[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 a6348abafd..f6f081bbd7 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -83,6 +84,7 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco private Converter stringToBytes = new SerializingConverter(); private SetConverter tupleToStringTuple = new SetConverter<>(new TupleConverter()); private SetConverter stringTupleToTuple = new SetConverter<>(new StringTupleConverter()); + private ListConverter tupleListToStringTuple = new ListConverter<>(new TupleConverter()); private ListConverter byteListToStringList = new ListConverter<>(bytesToString); private MapConverter byteMapToStringMap = new MapConverter<>(bytesToString); private MapConverter stringMapToByteMap = new MapConverter<>(stringToBytes); @@ -104,6 +106,26 @@ public StringRecord convert(ByteRecord source) { @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); @@ -277,7 +299,6 @@ public Long decrBy(byte[] key, long value) { return convertAndReturn(delegate.decrBy(key, value), Converters.identityConverter()); } - /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisKeyCommands#del(byte[][]) @@ -1530,7 +1551,8 @@ public Set zRangeByScore(byte[] key, double min, double max) { */ @Override public Set zRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { - return convertAndReturn(delegate.zRangeByScoreWithScores(key, min, max, offset, count), Converters.identityConverter()); + return convertAndReturn(delegate.zRangeByScoreWithScores(key, min, max, offset, count), + Converters.identityConverter()); } /* @@ -1602,7 +1624,8 @@ public Set zRevRangeByScore(byte[] key, Range range, Limit limit) { */ @Override public Set zRevRangeByScoreWithScores(byte[] key, double min, double max, long offset, long count) { - return convertAndReturn(delegate.zRevRangeByScoreWithScores(key, min, max, offset, count), Converters.identityConverter()); + return convertAndReturn(delegate.zRevRangeByScoreWithScores(key, min, max, offset, count), + Converters.identityConverter()); } /* @@ -1853,7 +1876,8 @@ public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... k */ @Override public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { - return convertAndReturn(delegate.evalSha(scriptSha1, returnType, numKeys, keysAndArgs), Converters.identityConverter()); + return convertAndReturn(delegate.evalSha(scriptSha1, returnType, numKeys, keysAndArgs), + Converters.identityConverter()); } /* @@ -1862,7 +1886,8 @@ public T evalSha(String scriptSha1, ReturnType returnType, int numKeys, byte */ @Override public T evalSha(byte[] scriptSha1, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { - return convertAndReturn(delegate.evalSha(scriptSha1, returnType, numKeys, keysAndArgs), Converters.identityConverter()); + return convertAndReturn(delegate.evalSha(scriptSha1, returnType, numKeys, keysAndArgs), + Converters.identityConverter()); } // @@ -1949,6 +1974,7 @@ public String bRPopLPush(int timeout, String srcKey, String dstKey) { public Boolean copy(String sourceKey, String targetKey, boolean replace) { return copy(serialize(sourceKey), serialize(targetKey), replace); } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#decr(java.lang.String) @@ -1976,7 +2002,6 @@ public Long del(String... keys) { return del(serializeMulti(keys)); } - /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#unlink(java.lang.String[]) @@ -2112,6 +2137,88 @@ public Double hIncrBy(String key, String field, double delta) { 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) @@ -2879,6 +2986,78 @@ public Long zInterStore(String destKey, String... sets) { return zInterStore(serialize(destKey), serializeMulti(sets)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMember(byte[]) + */ + @Override + public byte[] zRandMember(byte[] key) { + return delegate.zRandMember(key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMember(byte[], long) + */ + @Override + public List zRandMember(byte[] key, long count) { + return delegate.zRandMember(key, count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMemberWithScore(byte[]) + */ + @Override + public Tuple zRandMemberWithScore(byte[] key) { + return delegate.zRandMemberWithScore(key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMemberWithScore(byte[], long) + */ + @Override + public List zRandMemberWithScore(byte[] key, long count) { + return delegate.zRandMemberWithScore(key, count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#zRandMember(java.lang.String) + */ + @Override + public String zRandMember(String key) { + return convertAndReturn(delegate.zRandMember(serialize(key)), bytesToString); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#zRandMember(java.lang.String, long) + */ + @Override + public List zRandMember(String key, long count) { + return convertAndReturn(delegate.zRandMember(serialize(key), count), byteListToStringList); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#zRandMemberWithScore(java.lang.String) + */ + @Override + public StringTuple zRandMemberWithScore(String key) { + return convertAndReturn(delegate.zRandMemberWithScore(serialize(key)), new TupleConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#zRandMemberWithScores(java.lang.String, long) + */ + @Override + public List zRandMemberWithScores(String key, long count) { + return convertAndReturn(delegate.zRandMemberWithScore(serialize(key), count), tupleListToStringTuple); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#zRange(java.lang.String, long, long) @@ -3600,24 +3779,7 @@ public String getClientName() { @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); } /* @@ -3860,7 +4022,8 @@ public RecordId xAdd(StringRecord record, XAddOptions options) { */ @Override public List xClaimJustId(String key, String group, String consumer, XClaimOptions options) { - return convertAndReturn(delegate.xClaimJustId(serialize(key), group, consumer, options), Converters.identityConverter()); + return convertAndReturn(delegate.xClaimJustId(serialize(key), group, consumer, options), + Converters.identityConverter()); } /* @@ -3897,7 +4060,8 @@ public String xGroupCreate(String key, ReadOffset readOffset, String group) { */ @Override public String xGroupCreate(String key, ReadOffset readOffset, String group, boolean mkStream) { - return convertAndReturn(delegate.xGroupCreate(serialize(key), group, readOffset, mkStream), Converters.identityConverter()); + return convertAndReturn(delegate.xGroupCreate(serialize(key), group, readOffset, mkStream), + Converters.identityConverter()); } /* @@ -3970,7 +4134,8 @@ public PendingMessagesSummary xPending(String key, String groupName) { @Override public PendingMessages xPending(String key, String groupName, String consumer, org.springframework.data.domain.Range range, Long count) { - return convertAndReturn(delegate.xPending(serialize(key), groupName, consumer, range, count), Converters.identityConverter()); + return convertAndReturn(delegate.xPending(serialize(key), groupName, consumer, range, count), + Converters.identityConverter()); } /* @@ -4267,7 +4432,8 @@ private T convertAndReturn(@Nullable Object value, Converter converter) { } return value == null ? null - : ObjectUtils.nullSafeEquals(converter, Converters.identityConverter()) ? (T) value : (T) converter.convert(value); + : ObjectUtils.nullSafeEquals(converter, Converters.identityConverter()) ? (T) value + : (T) converter.convert(value); } private void addResultConverter(Converter converter) { 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 fad7b59af8..959487a8d2 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -977,6 +977,34 @@ default Long zInterStore(byte[] destKey, byte[]... sets) { return zSetCommands().zInterStore(destKey, sets); } + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ + @Override + @Deprecated + default byte[] zRandMember(byte[] key) { + return zSetCommands().zRandMember(key); + } + + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ + @Override + @Deprecated + default List zRandMember(byte[] key, long count) { + return zSetCommands().zRandMember(key, count); + } + + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ + @Override + @Deprecated + default Tuple zRandMemberWithScore(byte[] key) { + return zSetCommands().zRandMemberWithScore(key); + } + + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ + @Override + @Deprecated + default List zRandMemberWithScore(byte[] key, long count) { + return zSetCommands().zRandMemberWithScore(key, count); + } + /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ @Override @Deprecated @@ -1203,6 +1231,34 @@ default Long hIncrBy(byte[] key, byte[] field, long delta) { 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 5e53d0d629..7656340178 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 @@ default Mono hLen(ByteBuffer key) { */ 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/ReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java index ed307120c2..499e039719 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java @@ -479,6 +479,142 @@ default Mono zIncrBy(ByteBuffer key, Number increment, ByteBuffer value) */ Flux> zIncrBy(Publisher commands); + /** + * {@code ZRANDMEMBER} command parameters. + * + * @author Mark Paluch + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + class ZRandMemberCommand extends KeyCommand { + + private final long count; + + private ZRandMemberCommand(@Nullable ByteBuffer key, long count) { + + super(key); + this.count = count; + } + + /** + * Creates a new {@link ZRandMemberCommand} given the number of values to retrieve. + * + * @param nrValuesToRetrieve + * @return a new {@link ZRandMemberCommand} for a number of values to retrieve. + */ + public static ZRandMemberCommand valueCount(long nrValuesToRetrieve) { + return new ZRandMemberCommand(null, nrValuesToRetrieve); + } + + /** + * Creates a new {@link ZRandMemberCommand} to retrieve one random member. + * + * @return a new {@link ZRandMemberCommand} to retrieve one random member. + */ + public static ZRandMemberCommand singleValue() { + return new ZRandMemberCommand(null, 1); + } + + /** + * Applies the {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param key must not be {@literal null}. + * @return a new {@link ZRandMemberCommand} with {@literal key} applied. + */ + public ZRandMemberCommand from(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return new ZRandMemberCommand(key, count); + } + + /** + * @return + */ + public long getCount() { + return count; + } + } + + /** + * Get random element from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + default Mono zRandMember(ByteBuffer key) { + return zRandMember(Mono.just(ZRandMemberCommand.singleValue().from(key))).flatMap(CommandResponse::getOutput) + .next(); + } + + /** + * Get {@code count} random elements from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count if the provided {@code count} argument is positive, return a list of distinct fields, capped either at + * {@code count} or the set size. If {@code count} is negative, the behavior changes and the command is + * allowed to return the same value multiple times. In this case, the number of returned values is the + * absolute value of the specified count. + * @return + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + default Flux zRandMember(ByteBuffer key, long count) { + return zRandMember(Mono.just(ZRandMemberCommand.valueCount(count).from(key))).flatMap(CommandResponse::getOutput); + } + + /** + * Get random elements from sorted set at {@code key}. + * + * @param commands must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + Flux>> zRandMember(Publisher commands); + + /** + * Get random element from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + default Mono zRandMemberWithScore(ByteBuffer key) { + return zRandMemberWithScore(Mono.just(ZRandMemberCommand.singleValue().from(key))) + .flatMap(CommandResponse::getOutput).next(); + } + + /** + * Get {@code count} random elements from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count if the provided {@code count} argument is positive, return a list of distinct fields, capped either at + * {@code count} or the set size. If {@code count} is negative, the behavior changes and the command is + * allowed to return the same value multiple times. In this case, the number of returned values is the + * absolute value of the specified count. + * @return + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + default Flux zRandMemberWithScore(ByteBuffer key, long count) { + return zRandMemberWithScore(Mono.just(ZRandMemberCommand.valueCount(count).from(key))) + .flatMap(CommandResponse::getOutput); + } + + /** + * Get random elements from sorted set at {@code key}. + * + * @param commands must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + Flux>> zRandMemberWithScore(Publisher commands); + /** * {@code ZRANK}/{@literal ZREVRANK} command parameters. * 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 f55151622a..bf083b9785 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/RedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java index fa8f5ef741..2e9c58d37d 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java @@ -633,6 +633,58 @@ default Long zAdd(byte[] key, Set tuples) { @Nullable Double zIncrBy(byte[] key, double increment, byte[] value); + /** + * Get random element from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @return can be {@literal null}. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + byte[] zRandMember(byte[] key); + + /** + * Get {@code count} random elements from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count if the provided {@code count} argument is positive, return a list of distinct fields, capped either at + * {@code count} or the set size. If {@code count} is negative, the behavior changes and the command is + * allowed to return the same value multiple times. In this case, the number of returned values is the + * absolute value of the specified count. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + List zRandMember(byte[] key, long count); + + /** + * Get random element from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @return can be {@literal null}. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + Tuple zRandMemberWithScore(byte[] key); + + /** + * Get {@code count} random elements from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count if the provided {@code count} argument is positive, return a list of distinct fields, capped either at + * {@code count} or the set size. If {@code count} is negative, the behavior changes and the command is + * allowed to return the same value multiple times. In this case, the number of returned values is the + * absolute value of the specified count. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + List zRandMemberWithScore(byte[] key, long count); + /** * Determine the index of element with {@code value} in a sorted set. * 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 d828fbe692..3c711aa120 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -1229,6 +1229,58 @@ default Long lPos(String key, String element) { */ Double zIncrBy(String key, double increment, String value); + /** + * Get random element from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @return can be {@literal null}. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + String zRandMember(String key); + + /** + * Get {@code count} random elements from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count if the provided {@code count} argument is positive, return a list of distinct fields, capped either at + * {@code count} or the set size. If {@code count} is negative, the behavior changes and the command is + * allowed to return the same value multiple times. In this case, the number of returned values is the + * absolute value of the specified count. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + List zRandMember(String key, long count); + + /** + * Get random element from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @return can be {@literal null}. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + StringTuple zRandMemberWithScore(String key); + + /** + * Get {@code count} random elements from sorted set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count if the provided {@code count} argument is positive, return a list of distinct fields, capped either at + * {@code count} or the set size. If {@code count} is negative, the behavior changes and the command is + * allowed to return the same value multiple times. In this case, the number of returned values is the + * absolute value of the specified count. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + List zRandMemberWithScores(String key, long count); + /** * Determine the index of element with {@code value} in a sorted set. * @@ -1742,6 +1794,58 @@ default Set zRevRangeByLex(String key, Range range) { */ 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 ab679f81a6..80fc7dbe1f 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 @@ public static Object parse(Object source, String sourcePath, Map + * @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 @@ private SlotRange parseSlotRange(String[] args) { } } + + 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 8149dad35f..43ffee1cd2 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.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 @@ public Double hIncrBy(byte[] key, byte[] field, double delta) { } } + /* + * (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/JedisClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java index 53df378987..913d7e0b2c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterZSetCommands.java @@ -18,7 +18,10 @@ import redis.clients.jedis.ScanParams; import redis.clients.jedis.ZParams; +import java.util.ArrayList; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -60,7 +63,8 @@ public Boolean zAdd(byte[] key, double score, byte[] value, ZAddArgs args) { Assert.notNull(value, "Value must not be null!"); try { - return JedisConverters.toBoolean(connection.getCluster().zadd(key, score, value, JedisConverters.toZAddParams(args))); + return JedisConverters + .toBoolean(connection.getCluster().zadd(key, score, value, JedisConverters.toZAddParams(args))); } catch (Exception ex) { throw convertJedisAccessException(ex); } @@ -119,6 +123,74 @@ public Double zIncrBy(byte[] key, double increment, byte[] value) { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMember(byte[]) + */ + @Override + public byte[] zRandMember(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + try { + return connection.getCluster().zrandmember(key); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMember(byte[], long) + */ + @Override + public List zRandMember(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + try { + return new ArrayList<>(connection.getCluster().zrandmember(key, count)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMemberWithScore(byte[]) + */ + @Override + public Tuple zRandMemberWithScore(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + try { + Set tuples = connection.getCluster().zrandmemberWithScores(key, 1); + + return tuples.isEmpty() ? null : JedisConverters.toTuple(tuples.iterator().next()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMemberWithScore(byte[], long) + */ + @Override + public List zRandMemberWithScore(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + try { + Set tuples = connection.getCluster().zrandmemberWithScores(key, count); + + return tuples.stream().map(JedisConverters::toTuple).collect(Collectors.toList()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisZSetCommands#zRank(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 57729efbc5..9adb2486db 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.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 @@ public Map hGetAll(byte[] key) { 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 @@ private boolean isPipelined() { private boolean isQueueing() { return connection.isQueueing(); } + } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java index 2f6b4f67e7..1a1def243e 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisZSetCommands.java @@ -21,14 +21,13 @@ import redis.clients.jedis.ScanParams; import redis.clients.jedis.ScanResult; import redis.clients.jedis.ZParams; -import redis.clients.jedis.params.ZAddParams; import java.nio.charset.StandardCharsets; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.springframework.data.redis.connection.RedisZSetCommands; -import org.springframework.data.redis.connection.RedisZSetCommands.ZAddArgs.Flag; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.KeyBoundCursor; import org.springframework.data.redis.core.ScanIteration; @@ -60,7 +59,8 @@ public Boolean zAdd(byte[] key, double score, byte[] value, ZAddArgs args) { Assert.notNull(key, "Key must not be null!"); Assert.notNull(value, "Value must not be null!"); - return connection.invoke().from(BinaryJedis::zadd, MultiKeyPipelineBase::zadd, key, score, value, JedisConverters.toZAddParams(args)) + return connection.invoke() + .from(BinaryJedis::zadd, MultiKeyPipelineBase::zadd, key, score, value, JedisConverters.toZAddParams(args)) .get(JedisConverters::toBoolean); } @@ -105,6 +105,65 @@ public Double zIncrBy(byte[] key, double increment, byte[] value) { return connection.invoke().just(BinaryJedis::zincrby, MultiKeyPipelineBase::zincrby, key, increment, value); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMember(byte[]) + */ + @Override + public byte[] zRandMember(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(BinaryJedis::zrandmember, MultiKeyPipelineBase::zrandmember, key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMember(byte[], long) + */ + @Override + public List zRandMember(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().fromMany(BinaryJedis::zrandmember, MultiKeyPipelineBase::zrandmember, key, count) + .toList(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMemberWithScore(byte[]) + */ + @Override + public Tuple zRandMemberWithScore(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke() + .from(BinaryJedis::zrandmemberWithScores, MultiKeyPipelineBase::zrandmemberWithScores, key, 1L).get(it -> { + + if (it.isEmpty()) { + return null; + } + + return JedisConverters.toTuple(it.iterator().next()); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMemberWithScore(byte[], long) + */ + @Override + public List zRandMemberWithScore(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke() + .fromMany(BinaryJedis::zrandmemberWithScores, MultiKeyPipelineBase::zrandmemberWithScores, key, count) + .toList(JedisConverters::toTuple); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisZSetCommands#zRank(byte[], byte[]) 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 dd210fad74..aa8172a0dd 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.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 @@ public Map hGetAll(byte[] key) { 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 @@ public Long hStrLen(byte[] key, byte[] field) { 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 7c7a58f602..b1103de1e2 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.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 @@ public Flux> hLen(Publisher comman })); } + @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/connection/lettuce/LettuceReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java index a46f061d07..b11e170302 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java @@ -156,6 +156,39 @@ public Flux> zIncrBy(Publisher>> zRandMember( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).map(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return new CommandResponse<>(command, cmd.zrandmember(command.getKey(), command.getCount())); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRandMemberWithScore(Publisher) + */ + @Override + public Flux>> zRandMemberWithScore( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).map(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return new CommandResponse<>(command, cmd.zrandmemberWithScores(command.getKey(), command.getCount()) + .map(sc -> new DefaultTuple(getBytes(sc), sc.getScore()))); + })); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRank(org.reactivestreams.Publisher) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java index 7376860d81..f7dc313523 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceZSetCommands.java @@ -105,6 +105,56 @@ public Double zIncrBy(byte[] key, double increment, byte[] value) { return connection.invoke().just(RedisSortedSetAsyncCommands::zincrby, key, increment, value); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMember(byte[]) + */ + @Override + public byte[] zRandMember(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(RedisSortedSetAsyncCommands::zrandmember, key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMember(byte[], long) + */ + @Override + public List zRandMember(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().just(RedisSortedSetAsyncCommands::zrandmember, key, count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMemberWithScore(byte[]) + */ + @Override + public Tuple zRandMemberWithScore(byte[] key) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().from(RedisSortedSetAsyncCommands::zrandmemberWithScores, key) + .get(LettuceConverters::toTuple); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zRandMemberWithScore(byte[], long) + */ + @Override + public List zRandMemberWithScore(byte[] key, long count) { + + Assert.notNull(key, "Key must not be null!"); + + return connection.invoke().fromMany(RedisSortedSetAsyncCommands::zrandmemberWithScores, key, count) + .toList(LettuceConverters::toTuple); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisZSetCommands#zRank(byte[], byte[]) 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 53048d9b4a..40f85e559d 100644 --- a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java +++ b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -223,7 +224,7 @@ Set deserializeValues(Set rawValues) { return SerializationUtils.deserialize(rawValues, valueSerializer()); } - Set> deserializeTupleValues(Collection rawValues) { + Set> deserializeTupleValues(Set rawValues) { if (rawValues == null) { return null; } @@ -234,6 +235,17 @@ Set> deserializeTupleValues(Collection rawValues) { return set; } + List> deserializeTupleValues(List rawValues) { + if (rawValues == null) { + return null; + } + List> set = new ArrayList<>(rawValues.size()); + for (Tuple rawValue : rawValues) { + set.add(deserializeTuple(rawValue)); + } + return set; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) TypedTuple deserializeTuple(Tuple tuple) { Object value = tuple.getValue(); @@ -277,6 +289,14 @@ Set deserializeHashKeys(Set rawKeys) { 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 ca7483dbcd..47ce81ca52 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/BoundZSetOperations.java b/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java index 8ff3df2eee..59fe58c5f2 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.List; import java.util.Set; import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate; @@ -102,6 +103,73 @@ public interface BoundZSetOperations extends BoundKeyOperations { @Nullable Double incrementScore(V value, double delta); + /** + * Get random element from set at the bound key. + * + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + V randomMember(); + + /** + * Get {@code count} distinct random elements from set at the bound key. + * + * @param count nr of members to return + * @return empty {@link Set} if {@code key} does not exist. + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + Set distinctRandomMembers(long count); + + /** + * Get {@code count} random elements from set at the bound key. + * + * @param count nr of members to return. + * @return empty {@link List} if {@code key} does not exist or {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + List randomMembers(long count); + + /** + * Get random element with its score from set at the bound key. + * + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + TypedTuple randomMemberWithScore(); + + /** + * Get {@code count} distinct random elements with their score from set at the bound key. + * + * @param count nr of members to return + * @return empty {@link Set} if {@code key} does not exist. + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + Set> distinctRandomMembersWithScore(long count); + + /** + * Get {@code count} random elements with their score from set at the bound key. + * + * @param key must not be {@literal null}. + * @param count nr of members to return. + * @return empty {@link List} if {@code key} does not exist or {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + List> randomMembersWithScore(long count); + /** * Determine the index of element with {@code value} in a sorted set. * 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 6b2ceb4b69..0493c294ec 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 @@ public Double increment(HK key, double delta) { 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/DefaultBoundZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundZSetOperations.java index f9a4a42bee..92fb5134dd 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundZSetOperations.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.List; import java.util.Set; import org.springframework.data.redis.connection.DataType; @@ -25,6 +26,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.Range; import org.springframework.data.redis.connection.RedisZSetCommands.Weights; import org.springframework.data.redis.core.ZSetOperations.TypedTuple; +import org.springframework.lang.Nullable; /** * Default implementation for {@link BoundZSetOperations}. @@ -96,6 +98,64 @@ public Double incrementScore(V value, double delta) { return ops.incrementScore(getKey(), value, delta); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundZSetOperations#randomMember() + */ + @Override + public V randomMember() { + return ops.randomMember(getKey()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundZSetOperations#distinctRandomMembers(long) + */ + @Nullable + @Override + public Set distinctRandomMembers(long count) { + return ops.distinctRandomMembers(getKey(), count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundZSetOperations#randomMembers(long) + */ + @Nullable + @Override + public List randomMembers(long count) { + return ops.randomMembers(getKey(), count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundZSetOperations#randomMemberWithScore() + */ + @Override + public TypedTuple randomMemberWithScore() { + return ops.randomMemberWithScore(getKey()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundZSetOperations#distinctRandomMembersWithScore(long) + */ + @Nullable + @Override + public Set> distinctRandomMembersWithScore(long count) { + return ops.distinctRandomMembersWithScore(getKey(), count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundZSetOperations#randomMembersWithScore(long) + */ + @Nullable + @Override + public List> randomMembersWithScore(long count) { + return ops.randomMembersWithScore(getKey(), count); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.BoundZSetOperations#getOperations() 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 bb3ba72b92..dc096ffb87 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.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 @@ public Double increment(K key, HK hashKey, double delta) { 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 f2ac85714a..a2fa529d5f 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 @@ public Mono increment(H key, HK hashKey, double delta) { .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/DefaultReactiveZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java index 0995bc96d0..893838c8fb 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java @@ -119,6 +119,82 @@ public Mono incrementScore(K key, V value, double delta) { return createMono(connection -> connection.zIncrBy(rawKey(key), delta, rawValue(value))); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#randomMember(K) + */ + @Override + public Mono randomMember(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.zRandMember(rawKey(key))).map(this::readValue); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#distinctRandomMembers(K, long) + */ + @Override + public Flux distinctRandomMembers(K key, long count) { + + Assert.notNull(key, "Key must not be null!"); + Assert.isTrue(count > 0, "Negative count not supported. Use randomMembers to allow duplicate elements."); + + return createFlux(connection -> connection.zRandMember(rawKey(key), count)).map(this::readValue); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#randomMembers(K, long) + */ + @Override + public Flux randomMembers(K key, long count) { + + Assert.notNull(key, "Key must not be null!"); + Assert.isTrue(count > 0, "Use a positive number for count. This method is already allowing duplicate elements."); + + return createFlux(connection -> connection.zRandMember(rawKey(key), -count)).map(this::readValue); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#randomMemberWithScore(K) + */ + @Override + public Mono> randomMemberWithScore(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.zRandMemberWithScore(rawKey(key))).map(this::readTypedTuple); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#distinctRandomMembersWithScore(K, long) + */ + @Override + public Flux> distinctRandomMembersWithScore(K key, long count) { + + Assert.notNull(key, "Key must not be null!"); + Assert.isTrue(count > 0, "Negative count not supported. Use randomMembers to allow duplicate elements."); + + return createFlux(connection -> connection.zRandMemberWithScore(rawKey(key), count)).map(this::readTypedTuple); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#randomMembersWithScore(K, long) + */ + @Override + public Flux> randomMembersWithScore(K key, long count) { + + Assert.notNull(key, "Key must not be null!"); + Assert.isTrue(count > 0, "Use a positive number for count. This method is already allowing duplicate elements."); + + return createFlux(connection -> connection.zRandMemberWithScore(rawKey(key), -count)).map(this::readTypedTuple); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.ReactiveZSetOperations#rank(java.lang.Object, java.lang.Object) diff --git a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java index 79ae492d80..45eb503763 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java @@ -17,6 +17,8 @@ import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate; @@ -26,6 +28,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.Weights; import org.springframework.data.redis.connection.RedisZSetCommands.ZAddArgs; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Default implementation of {@link ZSetOperations}. @@ -163,6 +166,90 @@ public Long intersectAndStore(K key, Collection otherKeys, K destKey, Aggrega return execute(connection -> connection.zInterStore(rawDestKey, aggregate, weights, rawKeys), true); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ZSetOperations#randomMember(java.lang.Object) + */ + @Override + public V randomMember(K key) { + + byte[] rawKey = rawKey(key); + + return deserializeValue(execute(connection -> connection.zRandMember(rawKey), true)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ZSetOperations#distinctRandomMembers(java.lang.Object, long) + */ + @Override + public Set distinctRandomMembers(K key, long count) { + + Assert.isTrue(count > 0, "Negative count not supported. Use randomMembers to allow duplicate elements."); + + byte[] rawKey = rawKey(key); + + List result = execute(connection -> connection.zRandMember(rawKey, count), true); + return result != null ? deserializeValues(new LinkedHashSet<>(result)) : null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ZSetOperations#randomMembers(java.lang.Object, long) + */ + @Override + public List randomMembers(K key, long count) { + + Assert.isTrue(count > 0, "Use a positive number for count. This method is already allowing duplicate elements."); + + byte[] rawKey = rawKey(key); + + List result = execute(connection -> connection.zRandMember(rawKey, count), true); + return deserializeValues(result); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ZSetOperations#randomMemberWithScore(java.lang.Object) + */ + @Override + public TypedTuple randomMemberWithScore(K key) { + + byte[] rawKey = rawKey(key); + + return deserializeTuple(execute(connection -> connection.zRandMemberWithScore(rawKey), true)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ZSetOperations#distinctRandomMembersWithScore(java.lang.Object, long) + */ + @Override + public Set> distinctRandomMembersWithScore(K key, long count) { + + Assert.isTrue(count > 0, "Negative count not supported. Use randomMembers to allow duplicate elements."); + + byte[] rawKey = rawKey(key); + + List result = execute(connection -> connection.zRandMemberWithScore(rawKey, count), true); + return result != null ? deserializeTupleValues(new LinkedHashSet<>(result)) : null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ZSetOperations#randomMembersWithScore(java.lang.Object, long) + */ + @Override + public List> randomMembersWithScore(K key, long count) { + + Assert.isTrue(count > 0, "Use a positive number for count. This method is already allowing duplicate elements."); + + byte[] rawKey = rawKey(key); + + List result = execute(connection -> connection.zRandMemberWithScore(rawKey, count), true); + return result != null ? deserializeTupleValues(result) : null; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.ZSetOperations#range(java.lang.Object, long, long) 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 a4e474fc9b..60b763e169 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 4d6f087774..562f6647dc 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/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java index 1914da9664..026939284d 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java @@ -80,6 +80,74 @@ public interface ReactiveZSetOperations { */ Mono incrementScore(K key, V value, double delta); + /** + * Get random element from set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + Mono randomMember(K key); + + /** + * Get {@code count} distinct random elements from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count nr of members to return + * @return + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + Flux distinctRandomMembers(K key, long count); + + /** + * Get {@code count} random elements from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count nr of members to return. + * @return + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + Flux randomMembers(K key, long count); + + /** + * Get random element with its score from set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + Mono> randomMemberWithScore(K key); + + /** + * Get {@code count} distinct random elements with their score from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count nr of members to return + * @return + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + Flux> distinctRandomMembersWithScore(K key, long count); + + /** + * Get {@code count} random elements with their score from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count nr of members to return. + * @return + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + Flux> randomMembersWithScore(K key, long count); + /** * Determine the index of element with {@code value} in a sorted set. * diff --git a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java index 822b8a47a7..76047a615c 100644 --- a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.List; import java.util.Set; import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate; @@ -133,6 +134,78 @@ static TypedTuple of(V value, @Nullable Double score) { @Nullable Double incrementScore(K key, V value, double delta); + /** + * Get random element from set at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + V randomMember(K key); + + /** + * Get {@code count} distinct random elements from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count nr of members to return + * @return empty {@link Set} if {@code key} does not exist. + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + Set distinctRandomMembers(K key, long count); + + /** + * Get {@code count} random elements from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count nr of members to return. + * @return empty {@link List} if {@code key} does not exist or {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + List randomMembers(K key, long count); + + /** + * Get random element with its score from set at {@code key}. + * + * @param key must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + TypedTuple randomMemberWithScore(K key); + + /** + * Get {@code count} distinct random elements with their score from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count nr of members to return + * @return empty {@link Set} if {@code key} does not exist. + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + Set> distinctRandomMembersWithScore(K key, long count); + + /** + * Get {@code count} random elements with their score from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count nr of members to return. + * @return empty {@link List} if {@code key} does not exist or {@literal null} when used in pipeline / transaction. + * @throws IllegalArgumentException if count is negative. + * @since 2.6 + * @see Redis Documentation: ZRANDMEMBER + */ + @Nullable + List> randomMembersWithScore(K key, long count); + /** * Determine the index of element with {@code value} in a sorted set. * 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 cacafc14fe..5c66d368a3 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 @@ void testHashIncrDecrByLong() { 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() { @@ -1869,6 +1915,39 @@ void testZInterStoreAggWeights() { new DefaultStringTuple("James".getBytes(), "James", 12d))) })); } + @Test // GH-2049 + @EnabledOnCommand("ZRANDMEMBER") + void testZRandMember() { + + actual.add(connection.zAdd("myset", 2, "Bob")); + actual.add(connection.zAdd("myset", 1, "James")); + actual.add(connection.zRandMember("myset")); + actual.add(connection.zRandMember("myset", 2)); + + List results = getResults(); + + assertThat(results.get(2)).isNotNull(); + assertThat(new LinkedHashSet<>((Collection) results.get(3))) + .isEqualTo(new LinkedHashSet<>(Arrays.asList("Bob", "James"))); + } + + @Test // GH-2049 + @EnabledOnCommand("ZRANDMEMBER") + void testZRandMemberWithScore() { + + actual.add(connection.zAdd("myset", 2, "Bob")); + actual.add(connection.zAdd("myset", 1, "James")); + actual.add(connection.zRandMemberWithScore("myset")); + actual.add(connection.zRandMemberWithScores("myset", 2)); + + List results = getResults(); + + assertThat(results.get(2)).isNotNull(); + assertThat(new LinkedHashSet<>((Collection) results.get(3))) + .isEqualTo(new LinkedHashSet<>(Arrays.asList(new DefaultStringTuple("Bob".getBytes(), "Bob", 2d), + new DefaultStringTuple("James".getBytes(), "James", 1d)))); + } + @Test void testZRangeWithScores() { actual.add(connection.zAdd("myset", 2, "Bob")); diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java index 90547fb6b2..43a8acd220 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java @@ -610,6 +610,12 @@ public interface ClusterConnectionTests { // DATAREDIS-315 void zInterStoreShouldWorkForSameSlotKeys(); + // GH-2049 + void zRandMemberShouldReturnResultCorrectly(); + + // GH-2049 + void zRandMemberWithScoreShouldReturnResultCorrectly(); + // DATAREDIS-315 void zRangeByLexShouldReturnResultCorrectly(); 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 019f0f4916..de3b3043e1 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 @@ public Map hGetAll(byte[] key) { 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/connection/jedis/JedisClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java index 1ad156d01d..d6c20d46c9 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java @@ -2026,6 +2026,29 @@ public void zInterStoreShouldWorkForSameSlotKeys() { assertThat(nativeConnection.zrange(SAME_SLOT_KEY_3_BYTES, 0, -1)).contains(VALUE_2_BYTES); } + @Test // GH-2049 + @EnabledOnCommand("ZRANDMEMBER") + public void zRandMemberShouldReturnResultCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + assertThat(clusterConnection.zRandMember(KEY_1_BYTES)).isIn(VALUE_1_BYTES, VALUE_2_BYTES); + assertThat(clusterConnection.zRandMember(KEY_1_BYTES, 2)).hasSize(2).contains(VALUE_1_BYTES, VALUE_2_BYTES); + + } + + @Test // GH-2049 + @EnabledOnCommand("ZRANDMEMBER") + public void zRandMemberWithScoreShouldReturnResultCorrectly() { + + nativeConnection.zadd(KEY_1_BYTES, 10D, VALUE_1_BYTES); + nativeConnection.zadd(KEY_1_BYTES, 20D, VALUE_2_BYTES); + + assertThat(clusterConnection.zRandMemberWithScore(KEY_1_BYTES)).isNotNull(); + assertThat(clusterConnection.zRandMemberWithScore(KEY_1_BYTES, 2)).hasSize(2); + } + @Test // DATAREDIS-315 public void zRangeByLexShouldReturnResultCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java index e628e234ed..67324ec24e 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java @@ -2067,6 +2067,29 @@ public void zInterStoreShouldWorkForSameSlotKeys() { assertThat(nativeConnection.zrange(SAME_SLOT_KEY_3, 0, -1)).contains(VALUE_2); } + @Test // GH-2049 + @EnabledOnCommand("ZRANDMEMBER") + public void zRandMemberShouldReturnResultCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + + assertThat(clusterConnection.zRandMember(KEY_1_BYTES)).isIn(VALUE_1_BYTES, VALUE_2_BYTES); + assertThat(clusterConnection.zRandMember(KEY_1_BYTES, 2)).hasSize(2).contains(VALUE_1_BYTES, VALUE_2_BYTES); + + } + + @Test // GH-2049 + @EnabledOnCommand("ZRANDMEMBER") + public void zRandMemberWithScoreShouldReturnResultCorrectly() { + + nativeConnection.zadd(KEY_1, 10D, VALUE_1); + nativeConnection.zadd(KEY_1, 20D, VALUE_2); + + assertThat(clusterConnection.zRandMemberWithScore(KEY_1_BYTES)).isNotNull(); + assertThat(clusterConnection.zRandMemberWithScore(KEY_1_BYTES, 2)).hasSize(2); + } + @Test // DATAREDIS-315 public void zRangeByLexShouldReturnResultCorrectly() { 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 d9ce821d25..d023f082d9 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 @@ void lengthOfValue() throws IOException { 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 3a46ee165e..171e6ec1e7 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.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 @@ void increment() { .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() { diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java index 2950a54d0a..09eb2c8d8d 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java @@ -26,6 +26,7 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.springframework.data.domain.Range; import org.springframework.data.redis.ByteBufferObjectFactory; @@ -38,6 +39,7 @@ import org.springframework.data.redis.core.ReactiveOperationsTestParams.Fixture; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -140,6 +142,47 @@ void incrementScore() { zSetOperations.incrementScore(key, value, 1.1).as(StepVerifier::create).expectNext(43.2).verifyComplete(); } + @ParameterizedRedisTest // GH-2049 + @EnabledOnCommand("ZRANDMEMBER") + void randomMember() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + zSetOperations.add(key, value1, 42.1).as(StepVerifier::create).expectNext(true).verifyComplete(); + zSetOperations.add(key, value2, 10).as(StepVerifier::create).expectNext(true).verifyComplete(); + + zSetOperations.randomMember(key).as(StepVerifier::create).consumeNextWith(actual -> { + + assertThat(actual).isIn(value1, value2); + }).verifyComplete(); + + zSetOperations.randomMembers(key, 2).as(StepVerifier::create).expectNextCount(2).verifyComplete(); + zSetOperations.distinctRandomMembers(key, 2).as(StepVerifier::create).expectNextCount(2).verifyComplete(); + } + + @ParameterizedRedisTest // GH-2049 + @Disabled("https://github.com/redis/redis/issues/9160") + @EnabledOnCommand("ZRANDMEMBER") + void randomMemberWithScore() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + zSetOperations.add(key, value1, 42.1).as(StepVerifier::create).expectNext(true).verifyComplete(); + zSetOperations.add(key, value2, 10).as(StepVerifier::create).expectNext(true).verifyComplete(); + + zSetOperations.randomMemberWithScore(key).as(StepVerifier::create).consumeNextWith(actual -> { + + assertThat(actual).isIn(new DefaultTypedTuple<>(value1, 42.1d), new DefaultTypedTuple<>(value2, 10d)); + }).verifyComplete(); + + zSetOperations.randomMembersWithScore(key, 2).as(StepVerifier::create).expectNextCount(2).verifyComplete(); + zSetOperations.distinctRandomMembersWithScore(key, 2).as(StepVerifier::create).expectNextCount(2).verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 void rank() { diff --git a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java index aa1860e006..fd443b8b2e 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsIntegrationTests.java @@ -26,15 +26,18 @@ import java.util.Set; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.springframework.data.redis.DoubleAsStringObjectFactory; import org.springframework.data.redis.DoubleObjectFactory; import org.springframework.data.redis.LongAsStringObjectFactory; import org.springframework.data.redis.LongObjectFactory; import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.connection.RedisZSetCommands; import org.springframework.data.redis.connection.RedisZSetCommands.Weights; import org.springframework.data.redis.core.ZSetOperations.TypedTuple; +import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.MethodSource; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; @@ -143,6 +146,49 @@ void testIncrementScore() { assertThat(tuple).isEqualTo(new DefaultTypedTuple<>(value1, 5.7)); } + @ParameterizedRedisTest // GH-2049 + @EnabledOnCommand("ZRANDMEMBER") + void testRandomMember() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + + zSetOps.add(key, value1, 1.9); + zSetOps.add(key, value2, 3.7); + zSetOps.add(key, value3, 5.8); + + assertThat(zSetOps.randomMember(key)).isIn(value1, value2, value3); + assertThat(zSetOps.randomMembers(key, 2)).hasSize(2).containsAnyOf(value1, value2, value3); + assertThat(zSetOps.distinctRandomMembers(key, 2)).hasSize(2).containsAnyOf(value1, value2, value3); + } + + @ParameterizedRedisTest // GH-2049 + @Disabled("https://github.com/redis/redis/issues/9160") + @EnabledOnCommand("ZRANDMEMBER") + void testRandomMemberWithScore() { + + assumeThat(valueFactory).isNotInstanceOf(RawObjectFactory.class); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + + zSetOps.add(key, value1, 1.9); + zSetOps.add(key, value2, 3.7); + zSetOps.add(key, value3, 5.8); + + assertThat(zSetOps.randomMemberWithScore(key)).isIn(new DefaultTypedTuple<>(value1, 1.9d), + new DefaultTypedTuple<>(value2, 3.7d), new DefaultTypedTuple<>(value3, 5.8d)); + assertThat(zSetOps.randomMembersWithScore(key, 2)).hasSize(2).containsAnyOf(new DefaultTypedTuple<>(value1, 1.9d), + new DefaultTypedTuple<>(value2, 3.7d), new DefaultTypedTuple<>(value3, 5.8d)); + assertThat(zSetOps.distinctRandomMembersWithScore(key, 2)).hasSize(2).containsAnyOf( + new DefaultTypedTuple<>(value1, 1.9d), new DefaultTypedTuple<>(value2, 3.7d), + new DefaultTypedTuple<>(value3, 5.8d)); + } + @ParameterizedRedisTest void testRangeByScoreOffsetCount() {