Skip to content

DATAREDIS-425 - Add Support for basic CRUD and finder Operations backed by Hashes and Sets. #156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 48 commits into from

Conversation

christophstrobl
Copy link
Member

We now enable storing domain object as a flat Redis HASH and maintain additional SET structures to enable finder operations on simple properties.

  @Keyspace("persons");
  class Person {

    @Id String id;
    @Indexed String firstname;
    String lastname;

    Map<String, String> attributes;

    City city;

    @Reference Person mother;
  }

The above is stored in the HASH with key persons:1 as

  _class = org.example.Person
  id = 1
  firstname = rand
  lastname = al’thor
  attributes.[eye-color] = grey
  attributes.[hair-color] = red
  city.name = emond's field
  city.region = two rivers
  mother = persons:2

Complex types are flattened out to their full property path for each of the values provided. If the properties actual value type does not match the declared one the _class type hint is added to the entry.

  city._class = CityInAndor.class
  city.name = emond's field
  city.region = two rivers
  city.country = andor

Map and Collection like structures are stored with their key/index values as part of the property path.

  list.[0]._class = DomainType.class
  list.[0].property1 = ...

  map.[key-1]._class = DomainType.class
  map.[key-1].property1 = ...

Properties marked with @Reference are stored as semantic references by just storing the key to the referenced object HASH instead of embedding its values.

   mother = persons:2

Please note that referenced objects are not transitively updated/saved and that lazy loading of references will be part of future development.

A save operation therefore executes the following:

  # flatten domain type and add as hash
  HMSET persons:1 id 1 firstname rand …

  # add the newly inserted entry to the list of all entries of that type
  SADD persons 1

  # index the firstname for finder lookup
  SADD persons.firstname:rand 1

Simple finder operation like findByFirstname use SINTER to find matching

  SINTER persons.firstname:rand
  HGETALL persons:1

Besides resolving an index via the @Index annotation we also allow to add custom configuration via the indexConfiguration attribute of @EnableRedisRepositories.

  @Configuration
  @EnableRedisRepositories(indexConfiguration = CustomIndexConfiguration.class)
  class Config { }

  static class CustomIndexConfiguration extends IndexConfiguration {

    @Override
    protected Iterable<RedisIndexDefinition> initialConfiguration() {
      return Arrays.asList(
        new RedisIndexDefinition("persons", "lastname"),
      );
    }
  }

@thomasdarimont
Copy link

Cool stuff indeed! Well done Christoph!

While it is definetly cool to have those indexing capabilities based on redis, I wonder whether this approach would really scale - I mean yes you can generate index helper structures in redis but does this really mean one should do this?

I've seen some systems that stored domain objects for fast access by key in redis (hashes) in combination with indexing the domain objects with a search service like elasticsearch or solr.

Lookups by key can then be answered by just querying redis.
In order to retrieve domain objects with more complex queries based on field vales, one simply queries the searchservice which then returns the key where the actual domain object can be looked up with.

The bottom line is: it would be cool to be able to plug-in an query / index services on entity / repository level that could be implemented in a completly different technology, e.g. redis and elasticsearch (in this example).

Cheers,
Thomas

@christophstrobl
Copy link
Member Author

Thanks @thomasdarimont. The idea of having additional structures for data lookups is not to replace a fully fledged search service, but to have a convenient way of finding data by specific attributes. Like having http session data stored in Redis that you do not only want to look up by the session id but also by eg. a users email. Having that said and given the responsiveness of Redis I do think that this approach serves well. But true, the culprit hides in how it's used.

@thomasdarimont
Copy link

I totally get that - I was just wondering how far this would go ;-)
The idea I wanted to point out was that it would be cool to have some hooks (without any AOP / AspectJ trickery, perhaps via some kind of EntityListener in the sense of JPA built into the Repository infrastructure) to (amongst other things) easily allow search engines to index newly saved / updated domain objects.

The basic mechanics are already there (JPA EntitListener, MongoDB events or just a RepositoryInterceptor) etc. - wouldn't it be nice to simply call save(..) on a Repository with a domain object which would then also be automatically indexed with elasticsearch or SOLR (after TX completes)?
I could think of something like @EnableEntitySearch / @EnableEntityIndexing that would then use the search service of choice (e.g. elasticsearch or SOLR) based on what's on the classpath.

Currently users have to write additional code to index a saved entity to the search service which could be seen as a bit of boilerplate / cross-cutting concern (searchability?).

Perhaps this would make a nice new feature for SD Commons, don't you think?
Shall I create a JIRA for that?

@christophstrobl
Copy link
Member Author

@thomasdarimont you're already one step ahead. Though I see the use case for that I'm actually a little torn about it. Care to elaborate that at SpringOne? Maybe an issue would help us remember ;)

@christophstrobl christophstrobl force-pushed the issue/DATAREDIS-425 branch 3 times, most recently from c0a19c4 to 67ea6da Compare October 29, 2015 12:31
@christophstrobl christophstrobl force-pushed the issue/DATAREDIS-425 branch 4 times, most recently from d247fba to ebce1c2 Compare February 11, 2016 09:22
@mp911de mp911de force-pushed the issue/DATAREDIS-425 branch 3 times, most recently from 3524a47 to 692468f Compare February 19, 2016 14:40
@christophstrobl christophstrobl force-pushed the issue/DATAREDIS-425 branch 2 times, most recently from 07b5a66 to 2a522e5 Compare February 23, 2016 08:35
…ed by Hashes and Sets.

Prepare issue branch.
…ed by Hashes and Sets.

We now enable storing domain object as a flat Redis 'HASH' and maintain additional 'SET' structures to enable finder operations on simple properties.

  @keyspace("persons");
  class Person {

    @id String id;
    @indexed String firstname;
    String lastname;

    Map<String, String> attributes;

    City city;

    @reference Person mother;
  }

The above is stored in the HASH with key 'persons:1' as

  _class = org.example.Person
  id = 1
  firstname = rand
  lastname = al’thor
  attributes.[eye-color] = grey
  attributes.[hair-color] = red
  city.name = emond's field
  city.region = two rivers
  mother = persons:2

Complex types are flattened out to their full property path for each of the values provided. If the properties actual value type does not match the declared one the '_class' type hint is added to the entry.

  city._class = CityInAndor.class
  city.name = emond's field
  city.region = two rivers
  city.country = andor

Map and Collection like structures are stored with their key/index values as part of the property path. If the map/collection value type does not match the actutal objects one the '_class' type hint is added to the entry.

  list.[0]._class = DomainType.class
  list.[0].property1 = ...

  map.[key-1]._class = DomainType.class
  map.[key-1].property1 = ...

Properties marked with '@reference' are stored as semantic references by just storing the key to the referenced object 'HASH' instead of embedding its values.

   mother = persons:2

Please note that referenced objects are not transitively updated/saved and that lazy loading of references will be part of future development.

A 'save' operation therefore executes the following:

  # flatten domain type and add as hash
  HMSET persons:1 id 1 firstname rand …

  # add the newly inserted entry to the list of all entries of that type
  SADD persons 1

  # index the firstname for finder lookup
  SADD persons.firstname:rand 1

Simple finder operation like 'findByFirstname' use 'SINTER' to find matching

  SINTER persons.firstname:rand
  HGETALL persons:1

Besides resolving an index via the '@Index' annotation we also allow to add custom configuration via the 'indexConfiguration' attribute of '@EnableRedisRepositories'.

  @configuration
  @EnableRedisRepositories(indexConfiguration = CustomIndexConfiguration.class)
  class Config { }

  static class CustomIndexConfiguration extends IndexConfiguration {

    @OverRide
    protected Iterable<RedisIndexDefinition> initialConfiguration() {
      return Arrays.asList(
        new RedisIndexDefinition("persons", "lastname"),
      );
    }
  }
Since we depend on changes there.
We need to have a dedicated RepositoryFactory to be able to tweak query creation. Now derived queries are freshly instantiated new for every execution.
…ontext.

Added "redisTemplateRef" to @EnableRedisRepositories. The default points to "redisTemplate".
- Introduce Bucket to not operate directly on a Map of byte[].
- Introduce IndexData to not operate directly on Map of byte[] for indexes.
- Move index updates to IndexWriter.
- Additional tests.
- Add hamcrest matcher for newly introduced Bucket.
- Renamed IndexedDataWriter to IndexWriter.
- Introduced IndexResolver.
- Added unit tests for writing index values.
- Align keys for indexes with OHM.
- Introduced RedisHash allowing to define TTL for types.
- Add RedisKeySpaceEvents.
- Add RedisMessageListeners for Keyspace Events
- Trigger helper structures clean up based upon Redis events.
- Store phantom key to be able to load expired value after key is actually already gone.
- Introduce RedisPersistentEntity.
- Use MappingContext for setting up template and Adapter.
- Allow programatic TTL configuration.
Enable custom conversions for more influence on conversions.

Still needs some polishing aka tests, factoryBeans, documentation and the such but gives a glimpse on what’s the purpose of it.

Important: we also need to check for potential index structures on custom converted objects, which has not been implemented so far.
Introduce annotation that allows to mark a single numeric property on aggregate root level to hold a dynamic timeout value. That supersedes any other configured timeout. This allows to define timeouts dynamically on every put/save operation.

@RedisHash
class Person {

  @id String id;
  @timetolive Long ttl;
}
christophstrobl and others added 10 commits February 26, 2016 14:10
Follow changes introduced in the spring-data-keyvalue module.
…found.

Allow fields named "id" to be considered as identifier property even when no explicit "@id" annotation is present. Favor fields with explicit id annotation over others and throw MappingException when ambiguous id declaration is found.

Additionally fail fast when repository interfaces reference types that do not declare an id property.
We ship converters for JSR-310 types (LocalDate/Time, ZonedDateTime, Period, Duration and ZoneId) to map between UTF-8-encoded byte[] and JDK 8 date/time types.

This change requires to build the project on Java 8.
…tion.

Provide property information when resolving index structures inside maps and lists. This avoids conversion problems and allows value lookup inside those types.
We removed the burden of converting raw hashes from ReferenceResolver and delegate this to the RedisConverter.
We now export Redis Repositories in a CDI environment. Repositories can be injected using @Inject. The CDI extension requires at least RedisOperations to be provided. Other beans like RedisKeyValueAdapter and RedisKeyValueTemplate can be provided by the user. If no RedisKeyValueAdapter/RedisKeyValueTemplate beans are found, the CDI extension creates own managed instances.
@mp911de
Copy link
Member

mp911de commented Mar 3, 2016

Redis Repositories require currently @RedisHash on domain classes to create repositories. RepositoryConfigurationDelegate enables strict repo config mode because it discovers KeyValueRepositoryFactory and RedisRepositoryFactory.

odrotbohm and others added 3 commits March 7, 2016 13:54
We now favor shadowing fields over using protected accessor methods.
Default query initialization changed to new, so we can delete some code here. nice!
Use @keyspace as meta annotation on @RedisHash and overwrite value using @AliasFor.
We now preserve the item order when converting list elements. This does not mean that we place elements at the exact position retrieved from the store but rather maintain their order based on the index value.
christophstrobl added a commit that referenced this pull request Mar 14, 2016
…ed by Hashes and Sets.

We now enable storing domain object as a flat Redis 'HASH' and maintain additional 'SET' structures to enable finder operations on simple properties.

  @RedisHash("persons");
  class Person {

    @id String id;
    @indexed String firstname;
    String lastname;

    Map<String, String> attributes;

    City city;

    @reference Person mother;
  }

The above is stored in the HASH with key 'persons:1' as

  _class = org.example.Person
  id = 1
  firstname = rand
  lastname = al’thor
  attributes.[eye-color] = grey
  attributes.[hair-color] = red
  city.name = emond's field
  city.region = two rivers
  mother = persons:2

Complex types are flattened out to their full property path for each of the values provided. If the properties actual value type does not match the declared one the '_class' type hint is added to the entry.

  city._class = CityInAndor.class
  city.name = emond's field
  city.region = two rivers
  city.country = andor

Map and Collection like structures are stored with their key/index values as part of the property path. If the map/collection value type does not match the actutal objects one the '_class' type hint is added to the entry.

  list.[0]._class = DomainType.class
  list.[0].property1 = ...

  map.[key-1]._class = DomainType.class
  map.[key-1].property1 = ...

Properties marked with '@reference' are stored as semantic references by just storing the key to the referenced object 'HASH' instead of embedding its values.

   mother = persons:2

Please note that referenced objects are not transitively updated/saved and that lazy loading of references will be part of future development.

A 'save' operation therefore executes the following:

  # flatten domain type and add as hash
  HMSET persons:1 id 1 firstname rand …

  # add the newly inserted entry to the list of all entries of that type
  SADD persons 1

  # index the firstname for finder lookup
  SADD persons.firstname:rand 1

Simple finder operation like 'findByFirstname' use 'SINTER' to find matching

  SINTER persons.firstname:rand
  HGETALL persons:1

Besides resolving an index via the '@Index' annotation we also allow to add custom configuration via the 'indexConfiguration' attribute of '@EnableRedisRepositories'.

  @configuration
  @EnableRedisRepositories(indexConfiguration = CustomIndexConfiguration.class)
  class Config { }

  static class CustomIndexConfiguration extends IndexConfiguration {

    @OverRide
    protected Iterable<RedisIndexDefinition> initialConfiguration() {
      return Arrays.asList(
        new SimpleIndexDefinition("persons", "lastname"),
      );
    }
  }

The '@timetolive' annotation allows to define a property or method providing an expiration time when storing the key in redis.

@RedisHash
class Person {

  @id String id;
  @timetolive Long ttl;
}

Original Pull Request: #156
christophstrobl added a commit that referenced this pull request Mar 14, 2016
- Add a composite IndexResolver implementation that iterates over a given collection of delegate IndexResolver instances and collects IndexedData from those.
- Break up cycle involving ReferenceResolver and let the resolver just returns the raw hash.
- Remove IndexType and use dedicated classes for index definitions.
- Fix pagination error and follow up to changes introduced via DATAKV-123.

Original Pull Request: #156
christophstrobl pushed a commit that referenced this pull request Mar 14, 2016
…ce documentation.

We ship converters for JSR-310 types (LocalDate/Time, ZonedDateTime, Period, Duration and ZoneId) to map between UTF-8-encoded byte[] and JDK 8 date/time types.

We also export Redis Repositories in a CDI environment. Repositories can be injected using @Inject. The CDI extension requires at least RedisOperations to be provided. Other beans like RedisKeyValueAdapter and RedisKeyValueTemplate can be provided by the user. If no RedisKeyValueAdapter/RedisKeyValueTemplate beans are found, the CDI extension creates own managed instances.

Original Pull Request: #156
christophstrobl pushed a commit that referenced this pull request Mar 14, 2016
We now favor shadowing fields over using protected accessor methods.

Original Pull Request: #156
christophstrobl added a commit that referenced this pull request Mar 14, 2016
Default query initialization changed to new, so we can delete some code here. 

Original Pull Request: #156
christophstrobl added a commit that referenced this pull request Mar 14, 2016
We now preserve the item order when converting list elements. This does not mean that we place elements at the exact position retrieved from the store but rather maintain their order based on the index value.

Additionally applied some documentation polishing and cluster tests.

Original Pull Request: #156
@christophstrobl christophstrobl deleted the issue/DATAREDIS-425 branch March 14, 2016 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants