Skip to content

Latest commit

 

History

History
931 lines (713 loc) · 33.4 KB

redis-repositories.adoc

File metadata and controls

931 lines (713 loc) · 33.4 KB

Redis Repositories

Working with Redis Repositories lets you seamlessly convert and store domain objects in Redis Hashes, apply custom mapping strategies, and use secondary indexes.

Important
Redis Repositories require at least Redis Server version 2.8.0 and do not work with transactions. Make sure to use a RedisTemplate with disabled transaction support.

Usage

Spring Data Redis lets you easily implement domain entities, as shown in the following example:

Example 1. Sample Person Entity
@RedisHash("people")
public class Person {

  @Id String id;
  String firstname;
  String lastname;
  Address address;
}

We have a pretty simple domain object here. Note that it has a @RedisHash annotation on its type and a property named id that is annotated with org.springframework.data.annotation.Id. Those two items are responsible for creating the actual key used to persist the hash.

Note
Properties annotated with @Id as well as those named id are considered as the identifier properties. Those with the annotation are favored over others.

To now actually have a component responsible for storage and retrieval, we need to define a repository interface, as shown in the following example:

Example 2. Basic Repository Interface To Persist Person Entities
public interface PersonRepository extends CrudRepository<Person, String> {

}

As our repository extends CrudRepository, it provides basic CRUD and finder operations. The thing we need in between to glue things together is the corresponding Spring configuration, shown in the following example:

Example 3. JavaConfig for Redis Repositories
@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

  @Bean
  public RedisConnectionFactory connectionFactory() {
    return new JedisConnectionFactory();
  }

  @Bean
  public RedisTemplate<?, ?> redisTemplate() {

    RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
    return template;
  }
}

Given the preceding setup, we can inject PersonRepository into our components, as shown in the following example:

Example 4. Access to Person Entities
@Autowired PersonRepository repo;

public void basicCrudOperations() {

  Person rand = new Person("rand", "al'thor");
  rand.setAddress(new Address("emond's field", "andor"));

  repo.save(rand);                                         (1)

  repo.findOne(rand.getId());                              (2)

  repo.count();                                            (3)

  repo.delete(rand);                                       (4)
}
  1. Generates a new id if the current value is null or reuses an already set id value and stores properties of type Person inside the Redis Hash with a key that has a pattern of keyspace:id — in this case, it might be people:5d67b7e1-8640-4475-beeb-c666fab4c0e5.

  2. Uses the provided id to retrieve the object stored at keyspace:id.

  3. Counts the total number of entities available within the keyspace, people, defined by @RedisHash on Person.

  4. Removes the key for the given object from Redis.

Object-to-Hash Mapping

The Redis Repository support persists Objects to Hashes. This requires an Object-to-Hash conversion which is done by a RedisConverter. The default implementation uses Converter for mapping property values to and from Redis native byte[].

Given the Person type from the previous sections, the default mapping looks like the following:

_class = org.example.Person                 (1)
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand                            (2)
lastname = al’thor
address.city = emond's field                (3)
address.country = andor
  1. The _class attribute is included on the root level as well as on any nested interface or abstract types.

  2. Simple property values are mapped by path.

  3. Properties of complex types are mapped by their dot path.

The following table describes the default mapping rules:

Table 1. Default Mapping Rules
Type Sample Mapped Value

Simple Type
(for example, String)

String firstname = "rand";

firstname = "rand"

Complex Type
(for example, Address)

Address address = new Address("emond’s field");

address.city = "emond’s field"

List
of Simple Type

List<String> nicknames = asList("dragon reborn", "lews therin");

nicknames.[0] = "dragon reborn",
nicknames.[1] = "lews therin"

Map
of Simple Type

Map<String, String> atts = asMap({"eye-color", "grey"}, {"…​

atts.[eye-color] = "grey",
atts.[hair-color] = "…​

List
of Complex Type

List<Address> addresses = asList(new Address("em…​

addresses.[0].city = "emond’s field",
addresses.[1].city = "…​

Map
of Complex Type

Map<String, Address> addresses = asMap({"home", new Address("em…​

addresses.[home].city = "emond’s field",
addresses.[work].city = "…​

Caution
Due to the flat representation structure, Map keys need to be simple types, such as String or Number.

Mapping behavior can be customized by registering the corresponding Converter in RedisCustomConversions. Those converters can take care of converting from and to a single byte[] as well as Map<String,byte[]>. The first one is suitable for (for example) converting a complex type to (for example) a binary JSON representation that still uses the default mappings hash structure. The second option offers full control over the resulting hash.

Warning
Writing objects to a Redis hash deletes the content from the hash and re-creates the whole hash, so data that has not been mapped is lost.

The following example shows two sample byte array converters:

Example 5. Sample byte[] Converters
@WritingConverter
public class AddressToBytesConverter implements Converter<Address, byte[]> {

  private final Jackson2JsonRedisSerializer<Address> serializer;

  public AddressToBytesConverter() {

    serializer = new Jackson2JsonRedisSerializer<Address>(Address.class);
    serializer.setObjectMapper(new ObjectMapper());
  }

  @Override
  public byte[] convert(Address value) {
    return serializer.serialize(value);
  }
}

@ReadingConverter
public class BytesToAddressConverter implements Converter<byte[], Address> {

  private final Jackson2JsonRedisSerializer<Address> serializer;

  public BytesToAddressConverter() {

    serializer = new Jackson2JsonRedisSerializer<Address>(Address.class);
    serializer.setObjectMapper(new ObjectMapper());
  }

  @Override
  public Address convert(byte[] value) {
    return serializer.deserialize(value);
  }
}

Using the preceding byte array Converter produces output similar to the following:

_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
address = { city : "emond's field", country : "andor" }

The following example shows two examples of Map converters:

Example 6. Sample Map<String,byte[]> Converters
@WritingConverter
public class AddressToMapConverter implements Converter<Address, Map<String,byte[]>> {

  @Override
  public Map<String,byte[]> convert(Address source) {
    return singletonMap("ciudad", source.getCity().getBytes());
  }
}

@ReadingConverter
public class MapToAddressConverter implements Converter<Address, Map<String, byte[]>> {

  @Override
  public Address convert(Map<String,byte[]> source) {
    return new Address(new String(source.get("ciudad")));
  }
}

Using the preceding Map Converter produces output similar to the following:

_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
ciudad = "emond's field"
Note
Custom conversions have no effect on index resolution. Secondary Indexes are still created, even for custom converted types.

Customizing Type Mapping

If you want to avoid writing the entire Java class name as type information and would rather like to use a key, you can use the @TypeAlias annotation on the entity class being persisted. If you need to customize the mapping even more, look at the TypeInformationMapper interface. An instance of that interface can be configured at the DefaultRedisTypeMapper, which can be configured on MappingRedisConverter.

The following example shows how to define a type alias for an entity:

Example 7. Defining @TypeAlias for an entity
@TypeAlias("pers")
class Person {

}

The resulting document contains pers as the value in a _class field.

Configuring Custom Type Mapping

The following example demonstrates how to configure a custom RedisTypeMapper in MappingRedisConverter:

Example 8. Configuring a custom RedisTypeMapper via Spring Java Config
class CustomRedisTypeMapper extends DefaultRedisTypeMapper {
  //implement custom type mapping here
}
@Configuration
class SampleRedisConfiguration {

  @Bean
  public MappingRedisConverter redisConverter(RedisMappingContext mappingContext,
        RedisCustomConversions customConversions, ReferenceResolver referenceResolver) {

    MappingRedisConverter mappingRedisConverter = new MappingRedisConverter(mappingContext, null, referenceResolver,
            customTypeMapper());

    mappingRedisConverter.setCustomConversions(customConversions);

    return mappingRedisConverter;
  }

  @Bean
  public RedisTypeMapper customTypeMapper() {
    return new CustomRedisTypeMapper();
  }
}

Keyspaces

Keyspaces define prefixes used to create the actual key for the Redis Hash. By default, the prefix is set to getClass().getName(). You can alter this default by setting @RedisHash on the aggregate root level or by setting up a programmatic configuration. However, the annotated keyspace supersedes any other configuration.

The following example shows how to set the keyspace configuration with the @EnableRedisRepositories annotation:

Example 9. Keyspace Setup via @EnableRedisRepositories
@Configuration
@EnableRedisRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class)
public class ApplicationConfig {

  //... RedisConnectionFactory and RedisTemplate Bean definitions omitted

  public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {

    @Override
    protected Iterable<KeyspaceSettings> initialConfiguration() {
      return Collections.singleton(new KeyspaceSettings(Person.class, "people"));
    }
  }
}

The following example shows how to programmatically set the keyspace:

Example 10. Programmatic Keyspace setup
@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

  //... RedisConnectionFactory and RedisTemplate Bean definitions omitted

  @Bean
  public RedisMappingContext keyValueMappingContext() {
    return new RedisMappingContext(
      new MappingConfiguration(new IndexConfiguration(), new MyKeyspaceConfiguration()));
  }

  public static class MyKeyspaceConfiguration extends KeyspaceConfiguration {

    @Override
    protected Iterable<KeyspaceSettings> initialConfiguration() {
      return Collections.singleton(new KeyspaceSettings(Person.class, "people"));
    }
  }
}

Secondary Indexes

Secondary indexes are used to enable lookup operations based on native Redis structures. Values are written to the according indexes on every save and are removed when objects are deleted or expire.

Simple Property Index

Given the sample Person entity shown earlier, we can create an index for firstname by annotating the property with @Indexed, as shown in the following example:

Example 11. Annotation driven indexing
@RedisHash("people")
public class Person {

  @Id String id;
  @Indexed String firstname;
  String lastname;
  Address address;
}

Indexes are built up for actual property values. Saving two Persons (for example, "rand" and "aviendha") results in setting up indexes similar to the following:

SADD people:firstname:rand e2c7dcee-b8cd-4424-883e-736ce564363e
SADD people:firstname:aviendha a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56

It is also possible to have indexes on nested elements. Assume Address has a city property that is annotated with @Indexed. In that case, once person.address.city is not null, we have Sets for each city, as shown in the following example:

SADD people:address.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e

Furthermore, the programmatic setup lets you define indexes on map keys and list properties, as shown in the following example:

@RedisHash("people")
public class Person {

  // ... other properties omitted

  Map<String,String> attributes;      (1)
  Map<String Person> relatives;       (2)
  List<Address> addresses;            (3)
}
  1. SADD people:attributes.map-key:map-value e2c7dcee-b8cd-4424-883e-736ce564363e

  2. SADD people:relatives.map-key.firstname:tam e2c7dcee-b8cd-4424-883e-736ce564363e

  3. SADD people:addresses.city:tear e2c7dcee-b8cd-4424-883e-736ce564363e

Caution
Indexes cannot be resolved on References.

As with keyspaces, you can configure indexes without needing to annotate the actual domain type, as shown in the following example:

Example 12. Index Setup with @EnableRedisRepositories
@Configuration
@EnableRedisRepositories(indexConfiguration = MyIndexConfiguration.class)
public class ApplicationConfig {

  //... RedisConnectionFactory and RedisTemplate Bean definitions omitted

  public static class MyIndexConfiguration extends IndexConfiguration {

    @Override
    protected Iterable<IndexDefinition> initialConfiguration() {
      return Collections.singleton(new SimpleIndexDefinition("people", "firstname"));
    }
  }
}

Again, as with keyspaces, you can programmatically configure indexes, as shown in the following example:

Example 13. Programmatic Index setup
@Configuration
@EnableRedisRepositories
public class ApplicationConfig {

  //... RedisConnectionFactory and RedisTemplate Bean definitions omitted

  @Bean
  public RedisMappingContext keyValueMappingContext() {
    return new RedisMappingContext(
      new MappingConfiguration(
        new KeyspaceConfiguration(), new MyIndexConfiguration()));
  }

  public static class MyIndexConfiguration extends IndexConfiguration {

    @Override
    protected Iterable<IndexDefinition> initialConfiguration() {
      return Collections.singleton(new SimpleIndexDefinition("people", "firstname"));
    }
  }
}

Geospatial Index

Assume the Address type contains a location property of type Point that holds the geo coordinates of the particular address. By annotating the property with @GeoIndexed, Spring Data Redis adds those values by using Redis GEO commands, as shown in the following example:

@RedisHash("people")
public class Person {

  Address address;

  // ... other properties omitted
}

public class Address {

  @GeoIndexed Point location;

  // ... other properties omitted
}

public interface PersonRepository extends CrudRepository<Person, String> {

  List<Person> findByAddressLocationNear(Point point, Distance distance);     (1)
  List<Person> findByAddressLocationWithin(Circle circle);                    (2)
}

Person rand = new Person("rand", "al'thor");
rand.setAddress(new Address(new Point(13.361389D, 38.115556D)));

repository.save(rand);                                                        (3)

repository.findByAddressLocationNear(new Point(15D, 37D), new Distance(200)); (4)
  1. Query method declaration on a nested property, using Point and Distance.

  2. Query method declaration on a nested property, using Circle to search within.

  3. GEOADD people:address:location 13.361389 38.115556 e2c7dcee-b8cd-4424-883e-736ce564363e

  4. GEORADIUS people:address:location 15.0 37.0 200.0 km

In the preceding example the, longitude and latitude values are stored by using GEOADD that use the object’s id as the member’s name. The finder methods allow usage of Circle or Point, Distance combinations for querying those values.

Note
It is not possible to combine near and within with other criteria.

Time To Live

Objects stored in Redis may be valid only for a certain amount of time. This is especially useful for persisting short-lived objects in Redis without having to remove them manually when they reach their end of life. The expiration time in seconds can be set with @RedisHash(timeToLive=…​) as well as by using KeyspaceSettings (see Keyspaces).

More flexible expiration times can be set by using the @TimeToLive annotation on either a numeric property or a method. However, do not apply @TimeToLive on both a method and a property within the same class. The following example shows the @TimeToLive annotation on a property and on a method:

Example 14. Expirations
public class TimeToLiveOnProperty {

  @Id
  private String id;

  @TimeToLive
  private Long expiration;
}

public class TimeToLiveOnMethod {

  @Id
  private String id;

  @TimeToLive
  public long getTimeToLive() {
  	return new Random().nextLong();
  }
}
Note
Annotating a property explicitly with @TimeToLive reads back the actual TTL or PTTL value from Redis. -1 indicates that the object has no associated expiration.

The repository implementation ensures subscription to Redis keyspace notifications via RedisMessageListenerContainer.

When the expiration is set to a positive value, the corresponding EXPIRE command is executed. In addition to persisting the original, a phantom copy is persisted in Redis and set to expire five minutes after the original one. This is done to enable the Repository support to publish RedisKeyExpiredEvent, holding the expired value in Spring’s ApplicationEventPublisher whenever a key expires, even though the original values have already been removed. Expiry events are received on all connected applications that use Spring Data Redis repositories.

By default, the key expiry listener is disabled when initializing the application. The startup mode can be adjusted via @EnableKeyspaceNotifications or RedisKeyValueAdapter to start the listener with the application or upon the first insert of an entity with a TTL. See EnableKeyspaceEvents for possible values.

The RedisKeyExpiredEvent holds a copy of the expired domain object as well as the key.

Note
Delaying or disabling the expiry event listener startup impacts RedisKeyExpiredEvent publishing. A disabled event listener does not publish expiry events. A delayed startup can cause loss of events because of the delayed listener initialization.
Note
The keyspace notification message listener alters notify-keyspace-events settings in Redis, if those are not already set. Existing settings are not overridden, so you must set up those settings correctly (or leave them empty). Note that CONFIG is disabled on AWS ElastiCache, and enabling the listener leads to an error.
Note
Redis Pub/Sub messages are not persistent. If a key expires while the application is down, the expiry event is not processed, which may lead to secondary indexes containing references to the expired object.

Persisting References

Marking properties with @Reference allows storing a simple key reference instead of copying values into the hash itself. On loading from Redis, references are resolved automatically and mapped back into the object, as shown in the following example:

Example 15. Sample Property Reference
_class = org.example.Person
id = e2c7dcee-b8cd-4424-883e-736ce564363e
firstname = rand
lastname = al’thor
mother = people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56      (1)
  1. Reference stores the whole key (keyspace:id) of the referenced object.

Warning
Referenced Objects are not persisted when the referencing object is saved. You must persist changes on referenced objects separately, since only the reference is stored. Indexes set on properties of referenced types are not resolved.

Persisting Partial Updates

In some cases, you need not load and rewrite the entire entity just to set a new value within it. A session timestamp for the last active time might be such a scenario where you want to alter one property. PartialUpdate lets you define set and delete actions on existing objects while taking care of updating potential expiration times of both the entity itself and index structures. The following example shows a partial update:

Example 16. Sample Partial Update
PartialUpdate<Person> update = new PartialUpdate<Person>("e2c7dcee", Person.class)
  .set("firstname", "mat")                                                           (1)
  .set("address.city", "emond's field")                                              (2)
  .del("age");                                                                       (3)

template.update(update);

update = new PartialUpdate<Person>("e2c7dcee", Person.class)
  .set("address", new Address("caemlyn", "andor"))                                   (4)
  .set("attributes", singletonMap("eye-color", "grey"));                             (5)

template.update(update);

update = new PartialUpdate<Person>("e2c7dcee", Person.class)
  .refreshTtl(true);                                                                 (6)
  .set("expiration", 1000);

template.update(update);
  1. Set the simple firstname property to mat.

  2. Set the simple 'address.city' property to 'emond’s field' without having to pass in the entire object. This does not work when a custom conversion is registered.

  3. Remove the age property.

  4. Set complex address property.

  5. Set a map of values, which removes the previously existing map and replaces the values with the given ones.

  6. Automatically update the server expiration time when altering Time To Live.

Note
Updating complex objects as well as map (or other collection) structures requires further interaction with Redis to determine existing values, which means that rewriting the entire entity might be faster.

Queries and Query Methods

Query methods allow automatic derivation of simple finder queries from the method name, as shown in the following example:

Example 17. Sample Repository finder Method
public interface PersonRepository extends CrudRepository<Person, String> {

  List<Person> findByFirstname(String firstname);
}
Note
Please make sure properties used in finder methods are set up for indexing.
Note
Query methods for Redis repositories support only queries for entities and collections of entities with paging.

Using derived query methods might not always be sufficient to model the queries to execute. RedisCallback offers more control over the actual matching of index structures or even custom indexes. To do so, provide a RedisCallback that returns a single or Iterable set of id values, as shown in the following example:

Example 18. Sample finder using RedisCallback
String user = //...

List<RedisSession> sessionsByUser = template.find(new RedisCallback<Set<byte[]>>() {

  public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
    return connection
      .sMembers("sessions:securityContext.authentication.principal.username:" + user);
  }}, RedisSession.class);

The following table provides an overview of the keywords supported for Redis and what a method containing that keyword essentially translates to:

Table 2. Supported keywords inside method names
Keyword Sample Redis snippet

And

findByLastnameAndFirstname

SINTER …:firstname:rand …:lastname:al’thor

Or

findByLastnameOrFirstname

SUNION …:firstname:rand …:lastname:al’thor

Is, Equals

findByFirstname, findByFirstnameIs, findByFirstnameEquals

SINTER …:firstname:rand

IsTrue

FindByAliveIsTrue

SINTER …:alive:1

IsFalse

findByAliveIsFalse

SINTER …:alive:0

Top,First

findFirst10ByFirstname,findTop5ByFirstname

Redis Repositories Running on a Cluster

You can use the Redis repository support in a clustered Redis environment. See the “[cluster]” section for ConnectionFactory configuration details. Still, some additional configuration must be done, because the default key distribution spreads entities and secondary indexes through out the whole cluster and its slots.

The following table shows the details of data on a cluster (based on previous examples):

Key Type Slot Node

people:e2c7dcee-b8cd-4424-883e-736ce564363e

id for hash

15171

127.0.0.1:7381

people:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56

id for hash

7373

127.0.0.1:7380

people:firstname:rand

index

1700

127.0.0.1:7379

Some commands (such as SINTER and SUNION) can only be processed on the server side when all involved keys map to the same slot. Otherwise, computation has to be done on client side. Therefore, it is useful to pin keyspaces to a single slot, which lets make use of Redis server side computation right away. The following table shows what happens when you do (note the change in the slot column and the port value in the node column):

Key Type Slot Node

{people}:e2c7dcee-b8cd-4424-883e-736ce564363e

id for hash

2399

127.0.0.1:7379

{people}:a9d4b3a0-50d3-4538-a2fc-f7fc2581ee56

id for hash

2399

127.0.0.1:7379

{people}:firstname:rand

index

2399

127.0.0.1:7379

Tip
Define and pin keyspaces by using @RedisHash("{yourkeyspace}") to specific slots when you use Redis cluster.

CDI Integration

Instances of the repository interfaces are usually created by a container, for which Spring is the most natural choice when working with Spring Data. Spring offers sophisticated for creating bean instances. Spring Data Redis ships with a custom CDI extension that lets you use the repository abstraction in CDI environments. The extension is part of the JAR, so, to activate it, drop the Spring Data Redis JAR into your classpath.

You can then set up the infrastructure by implementing a CDI Producer for the RedisConnectionFactory and RedisOperations, as shown in the following example:

class RedisOperationsProducer {


  @Produces
  RedisConnectionFactory redisConnectionFactory() {

    JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(new RedisStandaloneConfiguration());
    jedisConnectionFactory.afterPropertiesSet();

    return jedisConnectionFactory;
  }

  void disposeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception {

    if (redisConnectionFactory instanceof DisposableBean) {
      ((DisposableBean) redisConnectionFactory).destroy();
    }
  }

  @Produces
  @ApplicationScoped
  RedisOperations<byte[], byte[]> redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) {

    RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
    template.setConnectionFactory(redisConnectionFactory);
    template.afterPropertiesSet();

    return template;
  }

}

The necessary setup can vary, depending on your JavaEE environment.

The Spring Data Redis CDI extension picks up all available repositories as CDI beans and creates a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. Thus, obtaining an instance of a Spring Data repository is a matter of declaring an @Injected property, as shown in the following example:

class RepositoryClient {

  @Inject
  PersonRepository repository;

  public void businessMethod() {
    List<Person> people = repository.findAll();
  }
}

A Redis Repository requires RedisKeyValueAdapter and RedisKeyValueTemplate instances. These beans are created and managed by the Spring Data CDI extension if no provided beans are found. You can, however, supply your own beans to configure the specific properties of RedisKeyValueAdapter and RedisKeyValueTemplate.

Redis Repositories Anatomy

Redis as a store itself offers a very narrow low-level API leaving higher level functions, such as secondary indexes and query operations, up to the user.

This section provides a more detailed view of commands issued by the repository abstraction for a better understanding of potential performance implications.

Consider the following entity class as the starting point for all operations:

Example 19. Example entity
@RedisHash("people")
public class Person {

  @Id String id;
  @Indexed String firstname;
  String lastname;
  Address hometown;
}

public class Address {

  @GeoIndexed Point location;
}

Insert new

repository.save(new Person("rand", "al'thor"));
HMSET "people:19315449-cda2-4f5c-b696-9cb8018fa1f9" "_class" "Person" "id" "19315449-cda2-4f5c-b696-9cb8018fa1f9" "firstname" "rand" "lastname" "al'thor" (1)
SADD  "people" "19315449-cda2-4f5c-b696-9cb8018fa1f9"                           (2)
SADD  "people:firstname:rand" "19315449-cda2-4f5c-b696-9cb8018fa1f9"            (3)
SADD  "people:19315449-cda2-4f5c-b696-9cb8018fa1f9:idx" "people:firstname:rand" (4)
  1. Save the flattened entry as hash.

  2. Add the key of the hash written in <1> to the helper index of entities in the same keyspace.

  3. Add the key of the hash written in <2> to the secondary index of firstnames with the properties value.

  4. Add the index of <3> to the set of helper structures for entry to keep track of indexes to clean on delete/update.

Replace existing

repository.save(new Person("e82908cf-e7d3-47c2-9eec-b4e0967ad0c9", "Dragon Reborn", "al'thor"));
DEL       "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"                           (1)
HMSET     "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "_class" "Person" "id" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" "firstname" "Dragon Reborn" "lastname" "al'thor" (2)
SADD      "people" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"                         (3)
SMEMBERS  "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx"                       (4)
TYPE      "people:firstname:rand"                                                 (5)
SREM      "people:firstname:rand" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9"          (6)
DEL       "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx"                       (7)
SADD      "people:firstname:Dragon Reborn" "e82908cf-e7d3-47c2-9eec-b4e0967ad0c9" (8)
SADD      "people:e82908cf-e7d3-47c2-9eec-b4e0967ad0c9:idx" "people:firstname:Dragon Reborn" (9)
  1. Remove the existing hash to avoid leftovers of hash keys potentially no longer present.

  2. Save the flattened entry as hash.

  3. Add the key of the hash written in <1> to the helper index of entities in the same keyspace.

  4. Get existing index structures that might need to be updated.

  5. Check if the index exists and what type it is (text, geo, …).

  6. Remove a potentially existing key from the index.

  7. Remove the helper holding index information.

  8. Add the key of the hash added in <2> to the secondary index of firstnames with the properties value.

  9. Add the index of <6> to the set of helper structures for entry to keep track of indexes to clean on delete/update.

Save Geo Data

Geo indexes follow the same rules as normal text based ones but use geo structure to store values. Saving an entity that uses a Geo-indexed property results in the following commands:

GEOADD "people:hometown:location" "13.361389" "38.115556" "76900e94-b057-44bc-abcf-8126d51a621b"  (1)
SADD   "people:76900e94-b057-44bc-abcf-8126d51a621b:idx" "people:hometown:location"               (2)
  1. Add the key of the saved entry to the the geo index.

  2. Keep track of the index structure.

Find using simple index

repository.findByFirstname("egwene");
SINTER  "people:firstname:egwene"                     (1)
HGETALL "people:d70091b5-0b9a-4c0a-9551-519e61bc9ef3" (2)
HGETALL ...
  1. Fetch keys contained in the secondary index.

  2. Fetch each key returned by <1> individually.

Find using Geo Index

repository.findByHometownLocationNear(new Point(15, 37), new Distance(200, KILOMETERS));
GEORADIUS "people:hometown:location" "15.0" "37.0" "200.0" "km" (1)
HGETALL   "people:76900e94-b057-44bc-abcf-8126d51a621b"         (2)
HGETALL   ...
  1. Fetch keys contained in the secondary index.

  2. Fetch each key returned by <1> individually.