From 185ab2b5c90e4c3b6597e3d741b19132aa4b3edf Mon Sep 17 00:00:00 2001 From: Brian Sam-Bodden Date: Fri, 5 Sep 2025 15:09:15 -0700 Subject: [PATCH] fix: optimize existsById() to use Redis EXISTS command instead of data retrieval (#657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existsById() method in both RedisDocumentRepository and RedisEnhancedRepository was inefficiently retrieving entire data structures when only existence validation was needed. This optimization replaces the inherited Spring Data behavior with direct Redis EXISTS commands, providing significant performance gains. Changes: - Override existsById() in SimpleRedisDocumentRepository and SimpleRedisEnhancedRepository - Use connection.keyCommands().exists() for O(1) existence checks - Properly handle composite IDs by implementing getKeyForId() helper methods - Apply ID filters and maintain full backward compatibility Performance impact: - Documents: existsById() → findById() → JSON.GET → full document deserialization - Hashes: existsById() → findById() → HGETALL → full hash retrieval - After: existsById() → EXISTS → boolean result only - Eliminates unnecessary network transfer and CPU overhead for existence checks --- .../SimpleRedisDocumentRepository.java | 45 +++++++++++++++++++ .../SimpleRedisEnhancedRepository.java | 45 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java index 3c7219c2..4be29447 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java @@ -32,6 +32,7 @@ import org.springframework.data.keyvalue.core.KeyValueOperations; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; import org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository; +import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.TimeToLive; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; @@ -722,6 +723,50 @@ public boolean exists(Example example) { return count(example) > 0; } + @Override + public boolean existsById(ID id) { + Assert.notNull(id, "The given id must not be null"); + + // Use direct Jedis EXISTS command for optimal performance + // Construct key properly for composite IDs + String fullKey = getKeyForId(id); + + return Boolean.TRUE.equals(modulesOperations.template().execute((RedisCallback) connection -> connection + .keyCommands().exists(fullKey.getBytes()))); + } + + private String getKeyForId(Object id) { + // Get the mapping context's entity info + RedisEnhancedPersistentEntity persistentEntity = (RedisEnhancedPersistentEntity) mappingContext + .getRequiredPersistentEntity(metadata.getJavaType()); + + String stringId; + + // Handle composite IDs + if (persistentEntity.isIdClassComposite()) { + BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(id); + List idParts = new ArrayList<>(); + for (RedisPersistentProperty idProperty : persistentEntity.getIdProperties()) { + Object propertyValue = wrapper.getPropertyValue(idProperty.getName()); + if (propertyValue != null) { + idParts.add(propertyValue.toString()); + } + } + stringId = String.join(":", idParts); + } else { + stringId = mappingConverter.getConversionService().convert(id, String.class); + } + + // Apply ID filters if they exist + var maybeIdentifierFilter = indexer.getIdentifierFilterFor(metadata.getJavaType()); + if (maybeIdentifierFilter.isPresent()) { + IdentifierFilter filter = (IdentifierFilter) maybeIdentifierFilter.get(); + stringId = filter.filter(stringId); + } + + return getKeyspace() + stringId; + } + // ------------------------------------------------------------------------- // Query By Example Fluent API - QueryByExampleExecutor // ------------------------------------------------------------------------- diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java index adcf3c9a..8ff0ce65 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; import org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository; import org.springframework.data.redis.core.PartialUpdate; +import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.convert.RedisData; import org.springframework.data.redis.core.convert.ReferenceResolverImpl; @@ -596,6 +597,50 @@ public boolean exists(Example example) { return count(example) > 0; } + @Override + public boolean existsById(ID id) { + Assert.notNull(id, "The given id must not be null"); + + // Use direct Jedis EXISTS command for optimal performance + // Construct key properly for composite IDs + String fullKey = getKeyForId(id); + + return Boolean.TRUE.equals(modulesOperations.template().execute((RedisCallback) connection -> connection + .keyCommands().exists(fullKey.getBytes()))); + } + + private String getKeyForId(Object id) { + // Get the mapping context's entity info + RedisEnhancedPersistentEntity persistentEntity = (RedisEnhancedPersistentEntity) mappingConverter + .getMappingContext().getRequiredPersistentEntity(metadata.getJavaType()); + + String stringId; + + // Handle composite IDs + if (persistentEntity.isIdClassComposite()) { + BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(id); + List idParts = new ArrayList<>(); + for (RedisPersistentProperty idProperty : persistentEntity.getIdProperties()) { + Object propertyValue = wrapper.getPropertyValue(idProperty.getName()); + if (propertyValue != null) { + idParts.add(propertyValue.toString()); + } + } + stringId = String.join(":", idParts); + } else { + stringId = mappingConverter.getConversionService().convert(id, String.class); + } + + // Apply ID filters if they exist + var maybeIdentifierFilter = indexer.getIdentifierFilterFor(metadata.getJavaType()); + if (maybeIdentifierFilter.isPresent()) { + IdentifierFilter filter = (IdentifierFilter) maybeIdentifierFilter.get(); + stringId = filter.filter(stringId); + } + + return getKeyspace() + stringId; + } + // ------------------------------------------------------------------------- // Query By Example Fluent API - QueryByExampleExecutor // -------------------------------------------------------------------------