Skip to content

Commit 82cb36f

Browse files
committed
DATACMNS-1802 - Add support for suspend repository query methods returning List<T>.
We now support returning List<T> from a suspended repository query method to simplify consumption of collection queries.
1 parent af00979 commit 82cb36f

File tree

4 files changed

+38
-9
lines changed

4 files changed

+38
-9
lines changed

src/main/asciidoc/kotlin-coroutines.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,14 @@ interface CoroutineRepository : CoroutineCrudRepository<User, String> {
7070
suspend fun findOne(id: String): User
7171
7272
fun findByFirstname(firstname: String): Flow<User>
73+
74+
suspend fun findAllByFirstname(id: String): List<User>
7375
}
7476
----
7577
====
7678

7779
Coroutines repositories are built on reactive repositories to expose the non-blocking nature of data access through Kotlin's Coroutines.
7880
Methods on a Coroutines repository can be backed either by a query method or a custom implementation.
79-
Invoking a custom implementation method propagates the Coroutines invocation to the actual implementation method if the custom method is `suspend`able without requiring the implementation method to return a reactive type such as `Mono` or `Flux`.
81+
Invoking a custom implementation method propagates the Coroutines invocation to the actual implementation method if the custom method is `suspend`-able without requiring the implementation method to return a reactive type such as `Mono` or `Flux`.
8082

8183
NOTE: Coroutines repositories are only discovered when the repository extends the `CoroutineCrudRepository` interface.

src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323

2424
import java.lang.reflect.InvocationTargetException;
2525
import java.lang.reflect.Method;
26+
import java.util.Collection;
2627
import java.util.stream.Stream;
2728

2829
import org.reactivestreams.Publisher;
30+
2931
import org.springframework.core.KotlinDetector;
3032
import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation;
3133
import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult;
@@ -176,6 +178,10 @@ private Object doInvokeReactiveToSuspended(Class<?> repositoryInterface, Reposit
176178
return ReactiveWrapperConverters.toWrapper(result, returnedType);
177179
}
178180

181+
if (Collection.class.isAssignableFrom(returnedType)) {
182+
result = Flux.from(result).collectList();
183+
}
184+
179185
return AwaitKt.awaitFirstOrNull(result, continuation);
180186
} catch (Exception e) {
181187
multicaster.notifyListeners(method, args, computeInvocationResult(invocationResultCaptor.error(e)));
@@ -188,8 +194,6 @@ private RepositoryMethodInvocation computeInvocationResult(RepositoryMethodInvoc
188194
captured.getDuration());
189195
}
190196

191-
192-
193197
interface Invokable {
194198

195199
@Nullable

src/main/java/org/springframework/data/repository/query/QueryMethod.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public QueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory
7777
});
7878

7979
this.method = method;
80-
this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(method);
80+
this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(metadata, method);
8181
this.parameters = createParameters(method);
8282
this.metadata = metadata;
8383

@@ -255,7 +255,7 @@ private boolean calculateIsCollectionQuery() {
255255
return false;
256256
}
257257

258-
Class<?> returnType = method.getReturnType();
258+
Class<?> returnType = metadata.getReturnType(method).getType();
259259

260260
if (QueryExecutionConverters.supports(returnType) && !QueryExecutionConverters.isSingleValue(returnType)) {
261261
return true;
@@ -268,13 +268,14 @@ private boolean calculateIsCollectionQuery() {
268268
return ClassTypeInformation.from(unwrappedReturnType).isCollectionLike();
269269
}
270270

271-
private static Class<? extends Object> potentiallyUnwrapReturnTypeFor(Method method) {
271+
private static Class<? extends Object> potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) {
272272

273-
if (QueryExecutionConverters.supports(method.getReturnType())) {
273+
TypeInformation<?> returnType = metadata.getReturnType(method);
274+
if (QueryExecutionConverters.supports(returnType.getType())) {
274275

275276
// unwrap only one level to handle cases like Future<List<Entity>> correctly.
276277

277-
TypeInformation<?> componentType = ClassTypeInformation.fromReturnTypeOf(method).getComponentType();
278+
TypeInformation<?> componentType = returnType.getComponentType();
278279

279280
if (componentType == null) {
280281
throw new IllegalStateException(
@@ -284,7 +285,7 @@ private static Class<? extends Object> potentiallyUnwrapReturnTypeFor(Method met
284285
return componentType.getType();
285286
}
286287

287-
return method.getReturnType();
288+
return returnType.getType();
288289
}
289290

290291
private static void assertReturnTypeAssignable(Method method, Set<Class<?>> types) {

src/test/kotlin/org/springframework/data/repository/kotlin/CoroutineCrudRepositoryUnitTests.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,34 @@ class CoroutineCrudRepositoryUnitTests {
266266
assertThat(emptyResult).isEmpty()
267267
}
268268

269+
@Test // DATACMNS-1802
270+
fun shouldBridgeSuspendedAsListMethod() {
271+
272+
val sample = User()
273+
274+
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null))).thenReturn(Flux.just(sample), Flux.empty<User>())
275+
276+
val result = runBlocking {
277+
coRepository.findSuspendedAsList("foo")
278+
}
279+
280+
assertThat(result).hasSize(1).containsOnly(sample)
281+
282+
val emptyResult = runBlocking {
283+
coRepository.findSuspendedAsList("foo")
284+
}
285+
286+
assertThat(emptyResult).isEmpty()
287+
}
288+
269289
interface MyCoRepository : CoroutineCrudRepository<User, String> {
270290

271291
suspend fun findOne(id: String): User
272292

273293
fun findMultiple(id: String): Flow<User>
274294

275295
suspend fun findSuspendedMultiple(id: String): Flow<User>
296+
297+
suspend fun findSuspendedAsList(id: String): List<User>
276298
}
277299
}

0 commit comments

Comments
 (0)