Skip to content

Commit 12d41c1

Browse files
raphaeldelioRaphael De Lio
andauthored
feature: implementing TTL capability to saveAll overwritten methods (#101)
* Implementing TTL capability to saveAll overwritten methods * Implementing processing of audit annotations on new saveAll methods. Plus tests. * Fixing Default TTL for Simple Document Repository Co-authored-by: Raphael De Lio <[email protected]>
1 parent 617b93b commit 12d41c1

File tree

8 files changed

+297
-16
lines changed

8 files changed

+297
-16
lines changed

redis-om-spring/src/main/java/com/redis/om/spring/repository/support/RedisDocumentRepositoryFactory.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.springframework.data.keyvalue.repository.query.SpelQueryCreator;
1010
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
1111
import org.springframework.data.projection.ProjectionFactory;
12+
import org.springframework.data.redis.core.mapping.RedisMappingContext;
1213
import org.springframework.data.repository.core.EntityInformation;
1314
import org.springframework.data.repository.core.NamedQueries;
1415
import org.springframework.data.repository.core.RepositoryInformation;
@@ -37,6 +38,8 @@ public class RedisDocumentRepositoryFactory extends KeyValueRepositoryFactory {
3738
private final RedisModulesOperations<?> rmo;
3839
private final KeyspaceToIndexMap keyspaceToIndexMap;
3940

41+
private RedisMappingContext mappingContext;
42+
4043
/**
4144
* Creates a new {@link KeyValueRepositoryFactory} for the given
4245
* {@link KeyValueOperations} and {@link RedisModulesOperations}.
@@ -47,8 +50,9 @@ public class RedisDocumentRepositoryFactory extends KeyValueRepositoryFactory {
4750
public RedisDocumentRepositoryFactory( //
4851
KeyValueOperations keyValueOperations, //
4952
RedisModulesOperations<?> rmo, //
50-
KeyspaceToIndexMap keyspaceToIndexMap) {
51-
this(keyValueOperations, rmo, keyspaceToIndexMap, DEFAULT_QUERY_CREATOR);
53+
KeyspaceToIndexMap keyspaceToIndexMap, //
54+
RedisMappingContext mappingContext) {
55+
this(keyValueOperations, rmo, keyspaceToIndexMap, DEFAULT_QUERY_CREATOR, mappingContext);
5256
}
5357

5458
/**
@@ -62,9 +66,10 @@ public RedisDocumentRepositoryFactory( //
6266
KeyValueOperations keyValueOperations, //
6367
RedisModulesOperations<?> rmo, //
6468
KeyspaceToIndexMap keyspaceToIndexMap, //
65-
Class<? extends AbstractQueryCreator<?, ?>> queryCreator) {
69+
Class<? extends AbstractQueryCreator<?, ?>> queryCreator, //
70+
RedisMappingContext mappingContext) {
6671

67-
this(keyValueOperations, rmo, keyspaceToIndexMap, queryCreator, RediSearchQuery.class);
72+
this(keyValueOperations, rmo, keyspaceToIndexMap, queryCreator, RediSearchQuery.class, mappingContext);
6873
}
6974

7075
/**
@@ -80,7 +85,8 @@ public RedisDocumentRepositoryFactory( //
8085
RedisModulesOperations<?> rmo, //
8186
KeyspaceToIndexMap keyspaceToIndexMap, //
8287
Class<? extends AbstractQueryCreator<?, ?>> queryCreator, //
83-
Class<? extends RepositoryQuery> repositoryQueryType) {
88+
Class<? extends RepositoryQuery> repositoryQueryType, //
89+
RedisMappingContext mappingContext ) {
8490

8591
super(keyValueOperations, queryCreator, repositoryQueryType);
8692

@@ -91,13 +97,15 @@ public RedisDocumentRepositoryFactory( //
9197
this.keyspaceToIndexMap = keyspaceToIndexMap;
9298
this.queryCreator = queryCreator;
9399
this.repositoryQueryType = repositoryQueryType;
100+
this.mappingContext = mappingContext;
94101
}
95102

96103
@Override
97104
protected Object getTargetRepository(RepositoryInformation repositoryInformation) {
98-
99105
EntityInformation<?, ?> entityInformation = getEntityInformation(repositoryInformation.getDomainType());
100-
return super.getTargetRepositoryViaReflection(repositoryInformation, entityInformation, keyValueOperations, rmo, keyspaceToIndexMap);
106+
return super.getTargetRepositoryViaReflection(
107+
repositoryInformation, entityInformation, keyValueOperations, rmo, keyspaceToIndexMap, mappingContext
108+
);
101109
}
102110

103111
@Override

redis-om-spring/src/main/java/com/redis/om/spring/repository/support/RedisDocumentRepositoryFactoryBean.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.springframework.beans.factory.annotation.Autowired;
66
import org.springframework.data.keyvalue.core.KeyValueOperations;
77
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean;
8+
import org.springframework.data.redis.core.mapping.RedisMappingContext;
89
import org.springframework.data.repository.Repository;
910
import org.springframework.data.repository.query.RepositoryQuery;
1011
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -18,6 +19,8 @@ public class RedisDocumentRepositoryFactoryBean<T extends Repository<S, ID>, S,
1819
private @Nullable RedisModulesOperations<String> rmo;
1920
@Autowired
2021
private @Nullable KeyspaceToIndexMap keyspaceToIndexMap;
22+
@Autowired
23+
private @Nullable RedisMappingContext mappingContext;
2124

2225
/**
2326
* Creates a new {@link RedisDocumentRepositoryFactoryBean} for the given repository
@@ -30,10 +33,11 @@ public RedisDocumentRepositoryFactoryBean(Class<? extends T> repositoryInterface
3033
}
3134

3235
@Override
33-
protected final RedisDocumentRepositoryFactory createRepositoryFactory(KeyValueOperations operations,
34-
Class<? extends AbstractQueryCreator<?, ?>> queryCreator, Class<? extends RepositoryQuery> repositoryQueryType) {
35-
36-
return new RedisDocumentRepositoryFactory(operations, rmo, keyspaceToIndexMap, queryCreator, repositoryQueryType);
36+
protected final RedisDocumentRepositoryFactory createRepositoryFactory(
37+
KeyValueOperations operations,
38+
Class<? extends AbstractQueryCreator<?, ?>> queryCreator, Class<? extends RepositoryQuery> repositoryQueryType
39+
) {
40+
return new RedisDocumentRepositoryFactory(operations, rmo, keyspaceToIndexMap, queryCreator, repositoryQueryType, this.mappingContext);
3741
}
3842

3943
@Override

redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
package com.redis.om.spring.repository.support;
22

3+
import java.lang.reflect.Field;
4+
import java.lang.reflect.Method;
5+
import java.time.LocalDate;
6+
import java.time.LocalDateTime;
37
import java.util.ArrayList;
8+
import java.util.Date;
49
import java.util.List;
10+
import java.util.concurrent.TimeUnit;
511
import java.util.stream.Collectors;
612
import java.util.stream.StreamSupport;
713

14+
import com.google.common.base.Optional;
815
import com.google.gson.Gson;
916
import com.google.gson.GsonBuilder;
1017
import com.redis.om.spring.convert.MappingRedisOMConverter;
1118
import com.redis.om.spring.id.ULIDIdentifierGenerator;
1219
import com.redis.om.spring.serialization.gson.GsonBuidlerFactory;
20+
import com.redis.om.spring.util.ObjectUtils;
1321
import org.apache.commons.lang3.StringUtils;
22+
import org.springframework.beans.PropertyAccessor;
23+
import org.springframework.beans.PropertyAccessorFactory;
1424
import org.springframework.beans.factory.annotation.Qualifier;
25+
import org.springframework.data.annotation.CreatedDate;
26+
import org.springframework.data.annotation.LastModifiedDate;
1527
import org.springframework.data.domain.Page;
1628
import org.springframework.data.domain.PageImpl;
1729
import org.springframework.data.domain.Pageable;
@@ -20,8 +32,11 @@
2032
import org.springframework.data.keyvalue.repository.support.SimpleKeyValueRepository;
2133
import org.springframework.data.redis.core.RedisTemplate;
2234
import org.springframework.data.redis.core.SetOperations;
35+
import org.springframework.data.redis.core.TimeToLive;
36+
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
2337
import org.springframework.data.redis.core.convert.RedisData;
2438
import org.springframework.data.redis.core.convert.ReferenceResolverImpl;
39+
import org.springframework.data.redis.core.mapping.RedisMappingContext;
2540
import org.springframework.data.repository.core.EntityInformation;
2641

2742
import com.redis.om.spring.KeyspaceToIndexMap;
@@ -31,6 +46,7 @@
3146
import com.redislabs.modules.rejson.Path;
3247
import org.springframework.util.Assert;
3348
import org.springframework.util.ClassUtils;
49+
import org.springframework.util.ReflectionUtils;
3450
import redis.clients.jedis.Jedis;
3551
import redis.clients.jedis.Pipeline;
3652
import redis.clients.jedis.commands.ProtocolCommand;
@@ -47,11 +63,14 @@ public class SimpleRedisDocumentRepository<T, ID> extends SimpleKeyValueReposito
4763
protected MappingRedisOMConverter mappingConverter;
4864
private final ULIDIdentifierGenerator generator;
4965

66+
private RedisMappingContext mappingContext;
67+
5068
@SuppressWarnings("unchecked")
5169
public SimpleRedisDocumentRepository(EntityInformation<T, ID> metadata, //
5270
KeyValueOperations operations, //
5371
@Qualifier("redisModulesOperations") RedisModulesOperations<?> rmo, //
54-
KeyspaceToIndexMap keyspaceToIndexMap) {
72+
KeyspaceToIndexMap keyspaceToIndexMap, //
73+
RedisMappingContext mappingContext) {
5574
super(metadata, operations);
5675
this.modulesOperations = (RedisModulesOperations<String>)rmo;
5776
this.metadata = metadata;
@@ -61,6 +80,7 @@ public SimpleRedisDocumentRepository(EntityInformation<T, ID> metadata, //
6180
new ReferenceResolverImpl(modulesOperations.getTemplate()));
6281
this.generator = ULIDIdentifierGenerator.INSTANCE;
6382
this.gson = this.gsonBuilder.create();
83+
this.mappingContext = mappingContext;
6484
}
6585

6686
@Override
@@ -124,13 +144,20 @@ public <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
124144
Pipeline pipeline = jedis.pipelined();
125145

126146
for (S entity : entities) {
147+
boolean isNew = metadata.isNew(entity);
148+
127149
KeyValuePersistentEntity<?, ?> keyValueEntity = mappingConverter.getMappingContext().getRequiredPersistentEntity(ClassUtils.getUserClass(entity));
128-
Object id = metadata.isNew(entity) ? generator.generateIdentifierOfType(keyValueEntity.getIdProperty().getTypeInformation()) : (String) keyValueEntity.getPropertyAccessor(entity).getProperty(keyValueEntity.getIdProperty());
150+
Object id = isNew ? generator.generateIdentifierOfType(keyValueEntity.getIdProperty().getTypeInformation()) : (String) keyValueEntity.getPropertyAccessor(entity).getProperty(keyValueEntity.getIdProperty());
129151
keyValueEntity.getPropertyAccessor(entity).setProperty(keyValueEntity.getIdProperty(), id);
130152

153+
String keyspace = keyValueEntity.getKeySpace();
154+
byte[] objectKey = createKey(keyspace, id.toString());
155+
156+
processAuditAnnotations(objectKey, entity, isNew);
157+
Optional<Long> maybeTtl = getTTLForEntity(entity);
158+
131159
RedisData rdo = new RedisData();
132160
mappingConverter.write(entity, rdo);
133-
byte[] objectKey = createKey(rdo.getKeyspace(), id.toString());
134161

135162
pipeline.sadd(rdo.getKeyspace(), id.toString());
136163

@@ -140,6 +167,10 @@ public <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
140167
args.add(SafeEncoder.encode(this.gson.toJson(entity)));
141168
pipeline.sendCommand(Command.SET, args.toArray(new byte[args.size()][]));
142169

170+
if (maybeTtl.isPresent()) {
171+
pipeline.expire(objectKey, maybeTtl.get());
172+
}
173+
143174
saved.add(entity);
144175
}
145176
pipeline.sync();
@@ -160,6 +191,60 @@ public byte[] createKey(String keyspace, String id) {
160191
return this.mappingConverter.toBytes(keyspace + ":" + id);
161192
}
162193

194+
private boolean expires(RedisData data) {
195+
return data.getTimeToLive() != null && data.getTimeToLive() > 0L;
196+
}
197+
198+
private void processAuditAnnotations(byte[] redisKey, Object item, boolean isNew) {
199+
var auditClass = isNew ? CreatedDate.class : LastModifiedDate.class;
200+
201+
List<Field> fields = com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(item.getClass(), auditClass);
202+
if (!fields.isEmpty()) {
203+
PropertyAccessor accessor = PropertyAccessorFactory.forBeanPropertyAccess(item);
204+
fields.forEach(f -> {
205+
if (f.getType() == Date.class) {
206+
accessor.setPropertyValue(f.getName(), new Date(System.currentTimeMillis()));
207+
} else if (f.getType() == LocalDateTime.class) {
208+
accessor.setPropertyValue(f.getName(), LocalDateTime.now());
209+
} else if (f.getType() == LocalDate.class) {
210+
accessor.setPropertyValue(f.getName(), LocalDate.now());
211+
}
212+
});
213+
}
214+
}
215+
216+
private Optional<Long> getTTLForEntity(Object entity) {
217+
KeyspaceConfiguration keyspaceConfig = mappingContext.getMappingConfiguration().getKeyspaceConfiguration();
218+
if (keyspaceConfig.hasSettingsFor(entity.getClass())) {
219+
var settings = keyspaceConfig.getKeyspaceSettings(entity.getClass());
220+
221+
if (org.springframework.util.StringUtils.hasText(settings.getTimeToLivePropertyName())) {
222+
Method ttlGetter;
223+
try {
224+
Field fld = ReflectionUtils.findField(entity.getClass(), settings.getTimeToLivePropertyName());
225+
ttlGetter = ObjectUtils.getGetterForField(entity.getClass(), fld);
226+
Long ttlPropertyValue = ((Number) ReflectionUtils.invokeMethod(ttlGetter, entity)).longValue();
227+
228+
ReflectionUtils.invokeMethod(ttlGetter, entity);
229+
230+
if (ttlPropertyValue != null) {
231+
TimeToLive ttl = fld.getAnnotation(TimeToLive.class);
232+
if (!ttl.unit().equals(TimeUnit.SECONDS)) {
233+
return Optional.of(TimeUnit.SECONDS.convert(ttlPropertyValue, ttl.unit()));
234+
} else {
235+
return Optional.of(ttlPropertyValue);
236+
}
237+
}
238+
} catch (SecurityException | IllegalArgumentException e) {
239+
return Optional.absent();
240+
}
241+
} else if (settings != null && settings.getTimeToLive() != null && settings.getTimeToLive() > 0) {
242+
return Optional.of(settings.getTimeToLive());
243+
}
244+
}
245+
return Optional.absent();
246+
}
247+
163248
private enum Command implements ProtocolCommand {
164249
SET("JSON.SET");
165250

redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
package com.redis.om.spring.repository.support;
22

3+
import java.lang.reflect.Field;
4+
import java.time.LocalDate;
5+
import java.time.LocalDateTime;
36
import java.util.ArrayList;
7+
import java.util.Date;
48
import java.util.List;
59
import java.util.Optional;
610
import java.util.stream.Collectors;
711
import java.util.stream.StreamSupport;
812

13+
import org.springframework.beans.PropertyAccessor;
14+
import org.springframework.beans.PropertyAccessorFactory;
915
import org.springframework.beans.factory.annotation.Qualifier;
16+
import org.springframework.data.annotation.CreatedDate;
17+
import org.springframework.data.annotation.LastModifiedDate;
1018
import org.springframework.data.domain.Page;
1119
import org.springframework.data.domain.PageImpl;
1220
import org.springframework.data.domain.PageRequest;
@@ -211,17 +219,27 @@ public <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
211219
Pipeline pipeline = jedis.pipelined();
212220

213221
for (S entity : entities) {
222+
boolean isNew = metadata.isNew(entity);
223+
214224
KeyValuePersistentEntity<?, ?> keyValueEntity = mappingConverter.getMappingContext().getRequiredPersistentEntity(ClassUtils.getUserClass(entity));
215-
Object id = metadata.isNew(entity) ? generator.generateIdentifierOfType(keyValueEntity.getIdProperty().getTypeInformation()) : (String) keyValueEntity.getPropertyAccessor(entity).getProperty(keyValueEntity.getIdProperty());
225+
Object id = isNew ? generator.generateIdentifierOfType(keyValueEntity.getIdProperty().getTypeInformation()) : (String) keyValueEntity.getPropertyAccessor(entity).getProperty(keyValueEntity.getIdProperty());
216226
keyValueEntity.getPropertyAccessor(entity).setProperty(keyValueEntity.getIdProperty(), id);
217227

228+
String keyspace = keyValueEntity.getKeySpace();
229+
byte[] objectKey = createKey(keyspace, id.toString());
230+
231+
processAuditAnnotations(objectKey, entity, isNew);
232+
218233
RedisData rdo = new RedisData();
219234
mappingConverter.write(entity, rdo);
220-
byte[] objectKey = createKey(rdo.getKeyspace(), id.toString());
221235

222236
pipeline.sadd(rdo.getKeyspace(), id.toString());
223237
pipeline.hmset(objectKey, rdo.getBucket().rawMap());
224238

239+
if (expires(rdo)) {
240+
pipeline.expire(objectKey, rdo.getTimeToLive());
241+
}
242+
225243
saved.add(entity);
226244
}
227245
pipeline.sync();
@@ -234,4 +252,25 @@ public byte[] createKey(String keyspace, String id) {
234252
return this.mappingConverter.toBytes(keyspace + ":" + id);
235253
}
236254

255+
private boolean expires(RedisData data) {
256+
return data.getTimeToLive() != null && data.getTimeToLive() > 0L;
257+
}
258+
259+
private void processAuditAnnotations(byte[] redisKey, Object item, boolean isNew) {
260+
var auditClass = isNew ? CreatedDate.class : LastModifiedDate.class;
261+
262+
List<Field> fields = com.redis.om.spring.util.ObjectUtils.getFieldsWithAnnotation(item.getClass(), auditClass);
263+
if (!fields.isEmpty()) {
264+
PropertyAccessor accessor = PropertyAccessorFactory.forBeanPropertyAccess(item);
265+
fields.forEach(f -> {
266+
if (f.getType() == Date.class) {
267+
accessor.setPropertyValue(f.getName(), new Date(System.currentTimeMillis()));
268+
} else if (f.getType() == LocalDateTime.class) {
269+
accessor.setPropertyValue(f.getName(), LocalDateTime.now());
270+
} else if (f.getType() == LocalDate.class) {
271+
accessor.setPropertyValue(f.getName(), LocalDate.now());
272+
}
273+
});
274+
}
275+
}
237276
}

redis-om-spring/src/test/java/com/redis/om/spring/annotations/document/BasicRedisDocumentMappingTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,4 +301,28 @@ void testFindByTagsIn() {
301301
assertTrue(names.contains("Microsoft"));
302302
}
303303

304+
@Test
305+
void testAuditAnnotationsOnSaveAll() {
306+
Company redis = Company.of("RedisInc", 2011, LocalDate.of(2021, 5, 1), new Point(-122.066540, 37.377690), "[email protected]");
307+
Company microsoft = Company.of("Microsoft", 1975, LocalDate.of(2022, 8, 15), new Point(-122.124500, 47.640160), "[email protected]");
308+
309+
repository.saveAll(List.of(redis, microsoft));
310+
311+
microsoft.setPubliclyListed(true);
312+
313+
repository.saveAll(List.of(microsoft));
314+
315+
assertEquals(2, repository.count());
316+
317+
Iterable<Company> companies = repository.findAllById(List.of(redis.getId(), microsoft.getId()));
318+
319+
assertAll( //
320+
() -> assertThat(companies).hasSize(2), //
321+
() -> assertThat(companies).containsExactly(redis, microsoft), //
322+
() -> assertThat(redis.getCreatedDate()).isNotNull(), //
323+
() -> assertThat(redis.getLastModifiedDate()).isNull(), //
324+
() -> assertThat(microsoft.getCreatedDate()).isNotNull(), //
325+
() -> assertThat(microsoft.getLastModifiedDate()).isNotNull()
326+
);
327+
}
304328
}

0 commit comments

Comments
 (0)