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.
This commit is contained in:
@@ -70,12 +70,14 @@ interface CoroutineRepository : CoroutineCrudRepository<User, String> {
|
||||
suspend fun findOne(id: String): User
|
||||
|
||||
fun findByFirstname(firstname: String): Flow<User>
|
||||
|
||||
suspend fun findAllByFirstname(id: String): List<User>
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Coroutines repositories are built on reactive repositories to expose the non-blocking nature of data access through Kotlin's Coroutines.
|
||||
Methods on a Coroutines repository can be backed either by a query method or a custom implementation.
|
||||
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`.
|
||||
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`.
|
||||
|
||||
NOTE: Coroutines repositories are only discovered when the repository extends the `CoroutineCrudRepository` interface.
|
||||
|
||||
@@ -23,9 +23,11 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation;
|
||||
import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult;
|
||||
@@ -176,6 +178,10 @@ abstract class RepositoryMethodInvoker {
|
||||
return ReactiveWrapperConverters.toWrapper(result, returnedType);
|
||||
}
|
||||
|
||||
if (Collection.class.isAssignableFrom(returnedType)) {
|
||||
result = Flux.from(result).collectList();
|
||||
}
|
||||
|
||||
return AwaitKt.awaitFirstOrNull(result, continuation);
|
||||
} catch (Exception e) {
|
||||
multicaster.notifyListeners(method, args, computeInvocationResult(invocationResultCaptor.error(e)));
|
||||
@@ -188,8 +194,6 @@ abstract class RepositoryMethodInvoker {
|
||||
captured.getDuration());
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface Invokable {
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -77,7 +77,7 @@ public class QueryMethod {
|
||||
});
|
||||
|
||||
this.method = method;
|
||||
this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(method);
|
||||
this.unwrappedReturnType = potentiallyUnwrapReturnTypeFor(metadata, method);
|
||||
this.parameters = createParameters(method);
|
||||
this.metadata = metadata;
|
||||
|
||||
@@ -255,7 +255,7 @@ public class QueryMethod {
|
||||
return false;
|
||||
}
|
||||
|
||||
Class<?> returnType = method.getReturnType();
|
||||
Class<?> returnType = metadata.getReturnType(method).getType();
|
||||
|
||||
if (QueryExecutionConverters.supports(returnType) && !QueryExecutionConverters.isSingleValue(returnType)) {
|
||||
return true;
|
||||
@@ -268,13 +268,14 @@ public class QueryMethod {
|
||||
return ClassTypeInformation.from(unwrappedReturnType).isCollectionLike();
|
||||
}
|
||||
|
||||
private static Class<? extends Object> potentiallyUnwrapReturnTypeFor(Method method) {
|
||||
private static Class<? extends Object> potentiallyUnwrapReturnTypeFor(RepositoryMetadata metadata, Method method) {
|
||||
|
||||
if (QueryExecutionConverters.supports(method.getReturnType())) {
|
||||
TypeInformation<?> returnType = metadata.getReturnType(method);
|
||||
if (QueryExecutionConverters.supports(returnType.getType())) {
|
||||
|
||||
// unwrap only one level to handle cases like Future<List<Entity>> correctly.
|
||||
|
||||
TypeInformation<?> componentType = ClassTypeInformation.fromReturnTypeOf(method).getComponentType();
|
||||
TypeInformation<?> componentType = returnType.getComponentType();
|
||||
|
||||
if (componentType == null) {
|
||||
throw new IllegalStateException(
|
||||
@@ -284,7 +285,7 @@ public class QueryMethod {
|
||||
return componentType.getType();
|
||||
}
|
||||
|
||||
return method.getReturnType();
|
||||
return returnType.getType();
|
||||
}
|
||||
|
||||
private static void assertReturnTypeAssignable(Method method, Set<Class<?>> types) {
|
||||
|
||||
@@ -266,6 +266,26 @@ class CoroutineCrudRepositoryUnitTests {
|
||||
assertThat(emptyResult).isEmpty()
|
||||
}
|
||||
|
||||
@Test // DATACMNS-1802
|
||||
fun shouldBridgeSuspendedAsListMethod() {
|
||||
|
||||
val sample = User()
|
||||
|
||||
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null))).thenReturn(Flux.just(sample), Flux.empty<User>())
|
||||
|
||||
val result = runBlocking {
|
||||
coRepository.findSuspendedAsList("foo")
|
||||
}
|
||||
|
||||
assertThat(result).hasSize(1).containsOnly(sample)
|
||||
|
||||
val emptyResult = runBlocking {
|
||||
coRepository.findSuspendedAsList("foo")
|
||||
}
|
||||
|
||||
assertThat(emptyResult).isEmpty()
|
||||
}
|
||||
|
||||
interface MyCoRepository : CoroutineCrudRepository<User, String> {
|
||||
|
||||
suspend fun findOne(id: String): User
|
||||
@@ -273,5 +293,7 @@ class CoroutineCrudRepositoryUnitTests {
|
||||
fun findMultiple(id: String): Flow<User>
|
||||
|
||||
suspend fun findSuspendedMultiple(id: String): Flow<User>
|
||||
|
||||
suspend fun findSuspendedAsList(id: String): List<User>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user