Skip to content

Commit 66c6e05

Browse files
DATAREDIS-425 - Simplify usage of RedisCallback for retrieving values.
RedisCallbacks now be directly used for eg. accessing index values by key to retrieve a set of domain types by ids.
1 parent dd9db16 commit 66c6e05

File tree

3 files changed

+320
-2
lines changed

3 files changed

+320
-2
lines changed

src/asciidoc/reference/redis-repositories.adoc

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,12 +458,12 @@ WARNING: Referenced Objects are not subject of persisting changes when saving th
458458
Indexes set on properties of referenced types will not be resolved.
459459

460460
[[redis.repositories.queries]]
461-
== Query Methods
461+
== Queries and Query Methods
462462
Query methods allow automatic derivation of simple finder queries from the method name.
463463

464464
.Sample Repository finder Method
465465
====
466-
[source]
466+
[source,java]
467467
----
468468
public interface PersonRepository extends CrudRepository<Person, String> {
469469
@@ -474,3 +474,23 @@ public interface PersonRepository extends CrudRepository<Person, String> {
474474

475475
NOTE: Please make sure properties used in finder methods are set up correctly for indexing.
476476

477+
Using derived finder 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 customly added ones. All it takes is providing a `RedisCallback` that returns a single or `Iterable` set of _id_ values.
478+
479+
.Sample finder using RedisCallback
480+
====
481+
[source,java]
482+
----
483+
String user = //...
484+
485+
List<RedisSession> sessionsByUser = template.find(new RedisCallback<Set<byte[]>>() {
486+
487+
public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
488+
return connection
489+
.sMembers("sessions:securityContext.authentication.principal.username:" + user);
490+
}}, RedisSession.class);
491+
----
492+
====
493+
494+
495+
496+

src/main/java/org/springframework/data/redis/core/RedisKeyValueTemplate.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
*/
1616
package org.springframework.data.redis.core;
1717

18+
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.List;
21+
1822
import org.springframework.data.keyvalue.core.KeyValueAdapter;
1923
import org.springframework.data.keyvalue.core.KeyValueCallback;
2024
import org.springframework.data.keyvalue.core.KeyValueTemplate;
2125
import org.springframework.data.redis.core.mapping.RedisMappingContext;
26+
import org.springframework.util.Assert;
27+
import org.springframework.util.ClassUtils;
2228

2329
/**
2430
* Redis specific implementation of {@link KeyValueTemplate}.
@@ -47,6 +53,63 @@ public RedisMappingContext getMappingContext() {
4753
return (RedisMappingContext) super.getMappingContext();
4854
}
4955

56+
/**
57+
* Retrieve entities by resolving their {@literal id}s and converting them into required type. <br />
58+
* The callback provides either a single {@literal id} or an {@link Iterable} of {@literal id}s, used for retrieving
59+
* the actual domain types and shortcuts manual retrieval and conversion of {@literal id}s via {@link RedisTemplate}.
60+
*
61+
* <pre>
62+
* <code>
63+
* List&#60;RedisSession&#62; sessions = template.find(new RedisCallback&#60;Set&#60;byte[]&#62;&#62;() {
64+
* public Set&#60;byte[]&#60; doInRedis(RedisConnection connection) throws DataAccessException {
65+
* return connection
66+
* .sMembers("spring:session:sessions:securityContext.authentication.principal.username:user"
67+
* .getBytes());
68+
* }
69+
* }, RedisSession.class);
70+
* </code>
71+
*
72+
* <pre>
73+
* @param callback provides the to retrieve entity ids. Must not be {@literal null}.
74+
* @param type must not be {@literal null}.
75+
* @return empty list if not elements found.
76+
*/
77+
public <T> List<T> find(final RedisCallback<?> callback, final Class<T> type) {
78+
79+
Assert.notNull(callback, "Callback must not be null.");
80+
81+
return execute(new RedisKeyValueCallback<List<T>>() {
82+
83+
@Override
84+
public List<T> doInRedis(RedisKeyValueAdapter adapter) {
85+
86+
Object callbackResult = adapter.execute(callback);
87+
88+
if (callbackResult == null) {
89+
return Collections.emptyList();
90+
}
91+
92+
Iterable<?> ids = ClassUtils.isAssignable(Iterable.class, callbackResult.getClass()) ? (Iterable<?>) callbackResult
93+
: Collections.singleton(callbackResult);
94+
95+
List<T> result = new ArrayList<T>();
96+
for (Object id : ids) {
97+
98+
String idToUse = adapter.getConverter().getConversionService().canConvert(id.getClass(), String.class) ? adapter
99+
.getConverter().getConversionService().convert(id, String.class)
100+
: id.toString();
101+
102+
T candidate = findById(idToUse, type);
103+
if (candidate != null) {
104+
result.add(candidate);
105+
}
106+
}
107+
108+
return result;
109+
}
110+
});
111+
}
112+
50113
/**
51114
* Redis specific {@link KeyValueCallback}.
52115
*
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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 static org.hamcrest.core.Is.*;
19+
import static org.hamcrest.core.IsCollectionContaining.*;
20+
import static org.junit.Assert.*;
21+
22+
import java.util.Arrays;
23+
import java.util.Collections;
24+
import java.util.List;
25+
26+
import org.junit.After;
27+
import org.junit.AfterClass;
28+
import org.junit.Before;
29+
import org.junit.Test;
30+
import org.junit.runner.RunWith;
31+
import org.junit.runners.Parameterized;
32+
import org.junit.runners.Parameterized.Parameters;
33+
import org.springframework.dao.DataAccessException;
34+
import org.springframework.data.annotation.Id;
35+
import org.springframework.data.redis.ConnectionFactoryTracker;
36+
import org.springframework.data.redis.connection.RedisConnection;
37+
import org.springframework.data.redis.connection.RedisConnectionFactory;
38+
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
39+
import org.springframework.data.redis.core.mapping.RedisMappingContext;
40+
import org.springframework.util.ObjectUtils;
41+
42+
/**
43+
* @author Christoph Strobl
44+
*/
45+
@RunWith(Parameterized.class)
46+
public class RedisKeyValueTemplateTests {
47+
48+
RedisConnectionFactory connectionFactory;
49+
RedisKeyValueTemplate template;
50+
RedisTemplate<Object, Object> nativeTemplate;
51+
52+
public RedisKeyValueTemplateTests(RedisConnectionFactory connectionFactory) {
53+
54+
this.connectionFactory = connectionFactory;
55+
ConnectionFactoryTracker.add(connectionFactory);
56+
}
57+
58+
@Parameters
59+
public static List<RedisConnectionFactory> params() {
60+
61+
JedisConnectionFactory jedis = new JedisConnectionFactory();
62+
jedis.afterPropertiesSet();
63+
64+
return Collections.<RedisConnectionFactory> singletonList(jedis);
65+
}
66+
67+
@AfterClass
68+
public static void cleanUp() {
69+
ConnectionFactoryTracker.cleanUp();
70+
}
71+
72+
@Before
73+
public void setUp() {
74+
75+
nativeTemplate = new RedisTemplate<Object, Object>();
76+
nativeTemplate.setConnectionFactory(connectionFactory);
77+
nativeTemplate.afterPropertiesSet();
78+
79+
RedisMappingContext context = new RedisMappingContext();
80+
81+
RedisKeyValueAdapter adapter = new RedisKeyValueAdapter(nativeTemplate, context);
82+
template = new RedisKeyValueTemplate(adapter, context);
83+
}
84+
85+
@After
86+
public void tearDown() {
87+
88+
nativeTemplate.execute(new RedisCallback<Void>() {
89+
90+
@Override
91+
public Void doInRedis(RedisConnection connection) throws DataAccessException {
92+
93+
connection.flushDb();
94+
return null;
95+
}
96+
});
97+
}
98+
99+
/**
100+
* @see DATAREDIS-425
101+
*/
102+
@Test
103+
public void savesObjectCorrectly() {
104+
105+
final Person rand = new Person();
106+
rand.firstname = "rand";
107+
108+
template.insert(rand);
109+
110+
nativeTemplate.execute(new RedisCallback<Void>() {
111+
112+
@Override
113+
public Void doInRedis(RedisConnection connection) throws DataAccessException {
114+
115+
assertThat(connection.exists(("template-test-person:" + rand.id).getBytes()), is(true));
116+
return null;
117+
}
118+
});
119+
}
120+
121+
/**
122+
* @see DATAREDIS-425
123+
*/
124+
@Test
125+
public void findProcessesCallbackReturningSingleIdCorrectly() {
126+
127+
Person rand = new Person();
128+
rand.firstname = "rand";
129+
130+
final Person mat = new Person();
131+
mat.firstname = "mat";
132+
133+
template.insert(rand);
134+
template.insert(mat);
135+
136+
List<Person> result = template.find(new RedisCallback<byte[]>() {
137+
138+
@Override
139+
public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
140+
return mat.id.getBytes();
141+
}
142+
}, Person.class);
143+
144+
assertThat(result.size(), is(1));
145+
assertThat(result, hasItems(mat));
146+
}
147+
148+
/**
149+
* @see DATAREDIS-425
150+
*/
151+
@Test
152+
public void findProcessesCallbackReturningMultipleIdsCorrectly() {
153+
154+
final Person rand = new Person();
155+
rand.firstname = "rand";
156+
157+
final Person mat = new Person();
158+
mat.firstname = "mat";
159+
160+
template.insert(rand);
161+
template.insert(mat);
162+
163+
List<Person> result = template.find(new RedisCallback<List<byte[]>>() {
164+
165+
@Override
166+
public List<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
167+
return Arrays.asList(rand.id.getBytes(), mat.id.getBytes());
168+
}
169+
}, Person.class);
170+
171+
assertThat(result.size(), is(2));
172+
assertThat(result, hasItems(rand, mat));
173+
}
174+
175+
/**
176+
* @see DATAREDIS-425
177+
*/
178+
@Test
179+
public void findProcessesCallbackReturningNullCorrectly() {
180+
181+
Person rand = new Person();
182+
rand.firstname = "rand";
183+
184+
Person mat = new Person();
185+
mat.firstname = "mat";
186+
187+
template.insert(rand);
188+
template.insert(mat);
189+
190+
List<Person> result = template.find(new RedisCallback<List<byte[]>>() {
191+
192+
@Override
193+
public List<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
194+
return null;
195+
}
196+
}, Person.class);
197+
198+
assertThat(result.size(), is(0));
199+
}
200+
201+
@RedisHash("template-test-person")
202+
static class Person {
203+
204+
@Id String id;
205+
String firstname;
206+
207+
@Override
208+
public int hashCode() {
209+
210+
int result = ObjectUtils.nullSafeHashCode(firstname);
211+
return result + ObjectUtils.nullSafeHashCode(id);
212+
}
213+
214+
@Override
215+
public boolean equals(Object obj) {
216+
if (this == obj) {
217+
return true;
218+
}
219+
if (obj == null) {
220+
return false;
221+
}
222+
if (!(obj instanceof Person)) {
223+
return false;
224+
}
225+
Person that = (Person) obj;
226+
227+
if (!ObjectUtils.nullSafeEquals(this.firstname, that.firstname)) {
228+
return false;
229+
}
230+
231+
return ObjectUtils.nullSafeEquals(this.id, that.id);
232+
}
233+
234+
}
235+
}

0 commit comments

Comments
 (0)