Skip to content

Commit 5911db1

Browse files
DATAREDIS-425 - Add Support for basic CRUD and finder Operations backed 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"), ); } }
1 parent 3667b39 commit 5911db1

22 files changed

+3496
-0
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ dependencies {
7979
compile "org.springframework:spring-tx:$springVersion"
8080
compile("org.springframework:spring-oxm:$springVersion", optional)
8181
compile "org.springframework:spring-aop:$springVersion"
82+
compile "org.springframework.data:spring-data-keyvalue:1.0.0.BUILD-SNAPSHOT"
8283

8384
// Redis Drivers
8485
compile("redis.clients:jedis:$jedisVersion", optional)
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/*
2+
* Copyright 2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.core;
17+
18+
import java.io.Serializable;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Map.Entry;
23+
import java.util.Set;
24+
25+
import org.springframework.beans.factory.DisposableBean;
26+
import org.springframework.dao.DataAccessException;
27+
import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
28+
import org.springframework.data.keyvalue.core.KeyValueAdapter;
29+
import org.springframework.data.redis.connection.RedisConnection;
30+
import org.springframework.data.redis.connection.RedisConnectionFactory;
31+
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
32+
import org.springframework.data.redis.core.convert.MappingRedisConverter;
33+
import org.springframework.data.redis.core.convert.RedisConverter;
34+
import org.springframework.data.redis.core.convert.RedisData;
35+
import org.springframework.data.redis.core.convert.ReferenceResolverImpl;
36+
import org.springframework.data.redis.core.index.IndexConfiguration;
37+
import org.springframework.data.util.CloseableIterator;
38+
39+
/**
40+
* Redis specific {@link KeyValueAdapter} implementation. Uses binary codec to read/write data from/to Redis.
41+
*
42+
* @author Christoph Strobl
43+
*/
44+
public class RedisKeyValueAdapter extends AbstractKeyValueAdapter {
45+
46+
private RedisOperations<?, ?> redisOps;
47+
48+
private MappingRedisConverter converter;
49+
50+
public RedisKeyValueAdapter() {
51+
this((IndexConfiguration) null);
52+
}
53+
54+
public RedisKeyValueAdapter(IndexConfiguration indexConfiguration) {
55+
56+
super(new RedisQueryEngine());
57+
58+
converter = new MappingRedisConverter(indexConfiguration, new ReferenceResolverImpl(this));
59+
60+
JedisConnectionFactory conFactory = new JedisConnectionFactory();
61+
conFactory.afterPropertiesSet();
62+
63+
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
64+
template.setConnectionFactory(conFactory);
65+
template.afterPropertiesSet();
66+
67+
this.redisOps = template;
68+
}
69+
70+
/**
71+
* @param redisOps
72+
* @param indexConfiguration
73+
*/
74+
public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, IndexConfiguration indexConfiguration) {
75+
76+
super(new RedisQueryEngine());
77+
78+
converter = new MappingRedisConverter(indexConfiguration, new ReferenceResolverImpl(this));
79+
this.redisOps = redisOps;
80+
}
81+
82+
/*
83+
* (non-Javadoc)
84+
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#put(java.io.Serializable, java.lang.Object, java.io.Serializable)
85+
*/
86+
public Object put(final Serializable id, final Object item, final Serializable keyspace) {
87+
88+
final RedisData rdo = new RedisData();
89+
converter.write(item, rdo);
90+
91+
redisOps.execute(new RedisCallback<Object>() {
92+
93+
@Override
94+
public Object doInRedis(RedisConnection connection) throws DataAccessException {
95+
96+
connection.hMSet(rdo.getKey(), rdo.getData());
97+
connection.sAdd(rdo.getKeyspace(), rdo.getId());
98+
99+
if (!rdo.getSimpleIndexKeys().isEmpty()) {
100+
for (byte[] index : rdo.getSimpleIndexKeys()) {
101+
connection.sAdd(index, rdo.getId());
102+
}
103+
}
104+
return null;
105+
}
106+
});
107+
108+
return item;
109+
}
110+
111+
/*
112+
* (non-Javadoc)
113+
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#contains(java.io.Serializable, java.io.Serializable)
114+
*/
115+
public boolean contains(final Serializable id, final Serializable keyspace) {
116+
117+
Boolean exists = redisOps.execute(new RedisCallback<Boolean>() {
118+
119+
@Override
120+
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
121+
return connection.sIsMember(converter.toBytes(keyspace), converter.toBytes(id));
122+
}
123+
});
124+
125+
return exists != null ? exists.booleanValue() : false;
126+
}
127+
128+
/*
129+
* (non-Javadoc)
130+
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#get(java.io.Serializable, java.io.Serializable)
131+
*/
132+
public Object get(Serializable id, Serializable keyspace) {
133+
134+
final byte[] binId = converter.convertToId(keyspace, id);
135+
136+
Map<byte[], byte[]> raw = redisOps.execute(new RedisCallback<Map<byte[], byte[]>>() {
137+
138+
@Override
139+
public Map<byte[], byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
140+
return connection.hGetAll(binId);
141+
}
142+
});
143+
144+
return converter.read(Object.class, new RedisData(raw));
145+
}
146+
147+
/*
148+
* (non-Javadoc)
149+
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#delete(java.io.Serializable, java.io.Serializable)
150+
*/
151+
public Object delete(final Serializable id, final Serializable keyspace) {
152+
153+
final byte[] binId = converter.toBytes(id);
154+
final byte[] binKeyspace = converter.toBytes(keyspace);
155+
156+
Object o = get(id, keyspace);
157+
158+
if (o != null) {
159+
160+
redisOps.execute(new RedisCallback<Void>() {
161+
162+
@Override
163+
public Void doInRedis(RedisConnection connection) throws DataAccessException {
164+
165+
connection.del(converter.convertToId(binKeyspace, binId));
166+
connection.sRem(binKeyspace, binId);
167+
168+
Set<byte[]> potentialIndex = connection.keys(converter.toBytes(keyspace + ".*"));
169+
170+
for (byte[] indexKey : potentialIndex) {
171+
try {
172+
connection.sRem(indexKey, binId);
173+
} catch (Exception e) {
174+
System.err.println(e);
175+
}
176+
}
177+
return null;
178+
}
179+
});
180+
181+
}
182+
return o;
183+
}
184+
185+
/*
186+
* (non-Javadoc)
187+
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#getAllOf(java.io.Serializable)
188+
*/
189+
public List<?> getAllOf(final Serializable keyspace) {
190+
191+
final byte[] binKeyspace = converter.toBytes(keyspace);
192+
193+
List<Map<byte[], byte[]>> raw = redisOps.execute(new RedisCallback<List<Map<byte[], byte[]>>>() {
194+
195+
@Override
196+
public List<Map<byte[], byte[]>> doInRedis(RedisConnection connection) throws DataAccessException {
197+
198+
final List<Map<byte[], byte[]>> rawData = new ArrayList<Map<byte[], byte[]>>();
199+
200+
Set<byte[]> members = connection.sMembers(binKeyspace);
201+
202+
for (byte[] id : members) {
203+
rawData.add(connection.hGetAll(converter.convertToId(binKeyspace, id)));
204+
}
205+
206+
return rawData;
207+
}
208+
});
209+
210+
List<Object> result = new ArrayList<Object>(raw.size());
211+
for (Map<byte[], byte[]> rawData : raw) {
212+
result.add(converter.read(Object.class, new RedisData(rawData)));
213+
}
214+
215+
return result;
216+
}
217+
218+
/*
219+
* (non-Javadoc)
220+
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#deleteAllOf(java.io.Serializable)
221+
*/
222+
public void deleteAllOf(final Serializable keyspace) {
223+
224+
redisOps.execute(new RedisCallback<Void>() {
225+
226+
@Override
227+
public Void doInRedis(RedisConnection connection) throws DataAccessException {
228+
229+
connection.del(converter.toBytes(keyspace));
230+
231+
Set<byte[]> potentialIndex = connection.keys(converter.toBytes(keyspace + ".*"));
232+
233+
for (byte[] indexKey : potentialIndex) {
234+
connection.del(indexKey);
235+
}
236+
return null;
237+
}
238+
});
239+
}
240+
241+
/*
242+
* (non-Javadoc)
243+
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#entries(java.io.Serializable)
244+
*/
245+
public CloseableIterator<Entry<Serializable, Object>> entries(Serializable keyspace) {
246+
throw new UnsupportedOperationException("Not yet implemented");
247+
}
248+
249+
/*
250+
* (non-Javadoc)
251+
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#count(java.io.Serializable)
252+
*/
253+
public long count(final Serializable keyspace) {
254+
255+
Long count = redisOps.execute(new RedisCallback<Long>() {
256+
257+
@Override
258+
public Long doInRedis(RedisConnection connection) throws DataAccessException {
259+
return connection.sCard(converter.toBytes(keyspace));
260+
}
261+
});
262+
263+
return count != null ? count.longValue() : 0;
264+
}
265+
266+
/**
267+
* Execute {@link RedisCallback} via underlying {@link RedisOperations}.
268+
*
269+
* @param callback must not be {@literal null}.
270+
* @see RedisOperations#execute(RedisCallback)
271+
* @return
272+
*/
273+
public <T> T execute(RedisCallback<T> callback) {
274+
return redisOps.execute(callback);
275+
}
276+
277+
/**
278+
* Get the {@link RedisConverter} in use.
279+
*
280+
* @return never {@literal null}.
281+
*/
282+
public RedisConverter getConverter() {
283+
return this.converter;
284+
}
285+
286+
public void clear() {
287+
// nothing to do
288+
}
289+
290+
/*
291+
* (non-Javadoc)
292+
* @see org.springframework.beans.factory.DisposableBean#destroy()
293+
*/
294+
public void destroy() throws Exception {
295+
296+
if (redisOps instanceof RedisTemplate) {
297+
RedisConnectionFactory connectionFactory = ((RedisTemplate<?, ?>) redisOps).getConnectionFactory();
298+
if (connectionFactory instanceof DisposableBean) {
299+
((DisposableBean) connectionFactory).destroy();
300+
}
301+
}
302+
}
303+
304+
}

0 commit comments

Comments
 (0)