diff --git a/src/main/asciidoc/kotlin-coroutines.adoc b/src/main/asciidoc/kotlin-coroutines.adoc index 87c5d9c9f..4e32e57d9 100644 --- a/src/main/asciidoc/kotlin-coroutines.adoc +++ b/src/main/asciidoc/kotlin-coroutines.adoc @@ -70,12 +70,14 @@ interface CoroutineRepository : CoroutineCrudRepository { suspend fun findOne(id: String): User fun findByFirstname(firstname: String): Flow + + suspend fun findAllByFirstname(id: String): List } ---- ==== 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. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java index efe2e789c..d458a4a83 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMethodInvoker.java @@ -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 diff --git a/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/src/main/java/org/springframework/data/repository/query/QueryMethod.java index eac876f32..7d851913a 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryMethod.java +++ b/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -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 potentiallyUnwrapReturnTypeFor(Method method) { + private static Class 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> 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> types) { diff --git a/src/test/kotlin/org/springframework/data/repository/kotlin/CoroutineCrudRepositoryUnitTests.kt b/src/test/kotlin/org/springframework/data/repository/kotlin/CoroutineCrudRepositoryUnitTests.kt index c8a340259..c40129219 100644 --- a/src/test/kotlin/org/springframework/data/repository/kotlin/CoroutineCrudRepositoryUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/repository/kotlin/CoroutineCrudRepositoryUnitTests.kt @@ -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()) + + val result = runBlocking { + coRepository.findSuspendedAsList("foo") + } + + assertThat(result).hasSize(1).containsOnly(sample) + + val emptyResult = runBlocking { + coRepository.findSuspendedAsList("foo") + } + + assertThat(emptyResult).isEmpty() + } + interface MyCoRepository : CoroutineCrudRepository { suspend fun findOne(id: String): User @@ -273,5 +293,7 @@ class CoroutineCrudRepositoryUnitTests { fun findMultiple(id: String): Flow suspend fun findSuspendedMultiple(id: String): Flow + + suspend fun findSuspendedAsList(id: String): List } }