Browse Source
We now support Kotlin Coroutines repositories backed by reactive repositories. Coroutine repositories can either invoke reactive query methods or implemented methods that are backed either by methods returning a reactive wrapper or that are native suspended functions.
Exclude Coroutines Continuation from bindable parameters and name discovery and do not unwrap results for suspended functions returning a reactive type.
interface CoroutinesRepository : CoroutineCrudRepository<User, String> {
suspend fun findOne(id: String): User
fun findByFirstname(firstname: String): Flow<User>
}
Original pull request: #415.
pull/428/head
21 changed files with 1146 additions and 138 deletions
@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
|
||||
/* |
||||
* Copyright 2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.core.support; |
||||
|
||||
import kotlin.coroutines.Continuation; |
||||
import kotlin.reflect.KFunction; |
||||
import kotlinx.coroutines.reactive.AwaitKt; |
||||
|
||||
import java.lang.reflect.Method; |
||||
|
||||
import org.reactivestreams.Publisher; |
||||
|
||||
import org.springframework.core.KotlinDetector; |
||||
import org.springframework.data.repository.util.ReactiveWrapperConverters; |
||||
import org.springframework.data.util.KotlinReflectionUtils; |
||||
import org.springframework.data.util.ReflectionUtils; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* Metadata for a implementation {@link Method} invocation. This value object encapsulates whether the called and the |
||||
* backing method are regular methods or suspendable Kotlin coroutines methods. It also allows invocation of suspendable |
||||
* methods by backing the invocation using methods returning reactive types. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 2.3 |
||||
*/ |
||||
class ImplementationInvocationMetadata { |
||||
|
||||
private final boolean suspendedDeclaredMethod; |
||||
private final boolean suspendedBaseClassMethod; |
||||
private final boolean reactiveBaseClassMethod; |
||||
|
||||
ImplementationInvocationMetadata(Method declaredMethod, Method baseClassMethod) { |
||||
|
||||
if (!KotlinDetector.isKotlinReflectPresent()) { |
||||
suspendedDeclaredMethod = false; |
||||
suspendedBaseClassMethod = false; |
||||
reactiveBaseClassMethod = false; |
||||
return; |
||||
} |
||||
|
||||
KFunction<?> declaredFunction = ReflectionUtils.isKotlinClass(declaredMethod.getDeclaringClass()) |
||||
? KotlinReflectionUtils.findKotlinFunction(declaredMethod) |
||||
: null; |
||||
KFunction<?> baseClassFunction = ReflectionUtils.isKotlinClass(baseClassMethod.getDeclaringClass()) |
||||
? KotlinReflectionUtils.findKotlinFunction(baseClassMethod) |
||||
: null; |
||||
|
||||
suspendedDeclaredMethod = declaredFunction != null && declaredFunction.isSuspend(); |
||||
suspendedBaseClassMethod = baseClassFunction != null && baseClassFunction.isSuspend(); |
||||
this.reactiveBaseClassMethod = !suspendedBaseClassMethod |
||||
&& ReactiveWrapperConverters.supports(baseClassMethod.getReturnType()); |
||||
} |
||||
|
||||
@Nullable |
||||
public Object invoke(Method methodToCall, Object instance, Object[] args) throws Throwable { |
||||
|
||||
if (suspendedDeclaredMethod && !suspendedBaseClassMethod && reactiveBaseClassMethod) { |
||||
return invokeReactiveToSuspend(methodToCall, instance, args); |
||||
} |
||||
|
||||
return methodToCall.invoke(instance, args); |
||||
} |
||||
|
||||
@Nullable |
||||
@SuppressWarnings({ "unchecked", "ConstantConditions" }) |
||||
private Object invokeReactiveToSuspend(Method methodToCall, Object instance, Object[] args) |
||||
throws ReflectiveOperationException { |
||||
|
||||
Object[] invocationArguments = new Object[args.length - 1]; |
||||
System.arraycopy(args, 0, invocationArguments, 0, invocationArguments.length); |
||||
Object result = methodToCall.invoke(instance, invocationArguments); |
||||
|
||||
Publisher<?> publisher; |
||||
if (result instanceof Publisher) { |
||||
publisher = (Publisher<?>) result; |
||||
} else { |
||||
publisher = ReactiveWrapperConverters.toWrapper(result, Publisher.class); |
||||
} |
||||
|
||||
return AwaitKt.awaitFirstOrNull(publisher, (Continuation) args[args.length - 1]); |
||||
} |
||||
|
||||
boolean canInvoke(Method invokedMethod, Method backendMethod) { |
||||
|
||||
if (suspendedDeclaredMethod == suspendedBaseClassMethod) { |
||||
return invokedMethod.getParameterCount() == backendMethod.getParameterCount(); |
||||
} |
||||
|
||||
if (suspendedDeclaredMethod && reactiveBaseClassMethod) { |
||||
return invokedMethod.getParameterCount() - 1 == backendMethod.getParameterCount(); |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
|
||||
/** |
||||
* Support for reactive repository. |
||||
* Support for reactive repositories. |
||||
*/ |
||||
@org.springframework.lang.NonNullApi |
||||
package org.springframework.data.repository.reactive; |
||||
|
||||
@ -0,0 +1,171 @@
@@ -0,0 +1,171 @@
|
||||
/* |
||||
* Copyright 2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.util; |
||||
|
||||
import kotlin.jvm.JvmClassMappingKt; |
||||
import kotlin.reflect.KCallable; |
||||
import kotlin.reflect.KClass; |
||||
import kotlin.reflect.KFunction; |
||||
import kotlin.reflect.KMutableProperty; |
||||
import kotlin.reflect.KProperty; |
||||
import kotlin.reflect.KType; |
||||
import kotlin.reflect.jvm.KTypesJvm; |
||||
import kotlin.reflect.jvm.ReflectJvmMapping; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.Optional; |
||||
import java.util.stream.Stream; |
||||
|
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* Reflection utility methods specific to Kotlin reflection. Requires Kotlin classes to be present to avoid linkage |
||||
* errors. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 2.3 |
||||
*/ |
||||
public final class KotlinReflectionUtils { |
||||
|
||||
private KotlinReflectionUtils() {} |
||||
|
||||
/** |
||||
* Returns a {@link KFunction} instance corresponding to the given Java {@link Method} instance, or {@code null} if |
||||
* this method cannot be represented by a Kotlin function. |
||||
* |
||||
* @param method the method to look up. |
||||
* @return the {@link KFunction} or {@code null} if the method cannot be looked up. |
||||
*/ |
||||
@Nullable |
||||
public static KFunction<?> findKotlinFunction(Method method) { |
||||
|
||||
KFunction<?> kotlinFunction = ReflectJvmMapping.getKotlinFunction(method); |
||||
|
||||
if (kotlinFunction == null) { |
||||
|
||||
// Fallback to own lookup because there's no public Kotlin API for that kind of lookup until
|
||||
// https://youtrack.jetbrains.com/issue/KT-20768 gets resolved.
|
||||
return findKFunction(method).orElse(null); |
||||
} |
||||
|
||||
return kotlinFunction; |
||||
} |
||||
|
||||
/** |
||||
* Returns the {@link Class return type} of a Kotlin {@link Method}. Supports regular and suspended methods. |
||||
* |
||||
* @param method the method to inspect, typically any synthetic JVM {@link Method}. |
||||
* @return return type of the method. |
||||
*/ |
||||
public static Class<?> getReturnType(Method method) { |
||||
|
||||
KFunction<?> kotlinFunction = KotlinReflectionUtils.findKotlinFunction(method); |
||||
|
||||
if (kotlinFunction == null) { |
||||
throw new IllegalArgumentException(String.format("Cannot resolve %s to a KFunction!", method)); |
||||
} |
||||
|
||||
return JvmClassMappingKt.getJavaClass(KTypesJvm.getJvmErasure(kotlinFunction.getReturnType())); |
||||
} |
||||
|
||||
/** |
||||
* Returns {@literal} whether the given {@link MethodParameter} is nullable. Its declaring method can reference a |
||||
* Kotlin function, property or interface property. |
||||
* |
||||
* @return {@literal true} if {@link MethodParameter} is nullable. |
||||
* @since 2.0.1 |
||||
*/ |
||||
static boolean isNullable(MethodParameter parameter) { |
||||
|
||||
Method method = parameter.getMethod(); |
||||
|
||||
if (method == null) { |
||||
throw new IllegalStateException(String.format("Cannot obtain method from parameter %s!", parameter)); |
||||
} |
||||
|
||||
KFunction<?> kotlinFunction = findKotlinFunction(method); |
||||
|
||||
if (kotlinFunction == null) { |
||||
throw new IllegalArgumentException(String.format("Cannot resolve %s to a Kotlin function!", parameter)); |
||||
} |
||||
|
||||
// Special handling for Coroutines
|
||||
if (kotlinFunction.isSuspend() && isLast(parameter)) { |
||||
return false; |
||||
} |
||||
|
||||
// see https://github.com/spring-projects/spring-framework/issues/23991
|
||||
if (kotlinFunction.getParameters().size() > parameter.getParameterIndex() + 1) { |
||||
|
||||
KType type = parameter.getParameterIndex() == -1 //
|
||||
? kotlinFunction.getReturnType() //
|
||||
: kotlinFunction.getParameters().get(parameter.getParameterIndex() + 1).getType(); |
||||
|
||||
return type.isMarkedNullable(); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
private static boolean isLast(MethodParameter parameter) { |
||||
return parameter.getParameterIndex() == parameter.getMethod().getParameterCount() - 1; |
||||
} |
||||
|
||||
/** |
||||
* Lookup a {@link Method} to a {@link KFunction}. |
||||
* |
||||
* @param method the JVM {@link Method} to look up. |
||||
* @return {@link Optional} wrapping a possibly existing {@link KFunction}. |
||||
*/ |
||||
private static Optional<? extends KFunction<?>> findKFunction(Method method) { |
||||
|
||||
KClass<?> kotlinClass = JvmClassMappingKt.getKotlinClass(method.getDeclaringClass()); |
||||
|
||||
return kotlinClass.getMembers() //
|
||||
.stream() //
|
||||
.flatMap(KotlinReflectionUtils::toKFunctionStream) //
|
||||
.filter(it -> isSame(it, method)) //
|
||||
.findFirst(); |
||||
} |
||||
|
||||
private static Stream<? extends KFunction<?>> toKFunctionStream(KCallable<?> it) { |
||||
|
||||
if (it instanceof KMutableProperty<?>) { |
||||
|
||||
KMutableProperty<?> property = (KMutableProperty<?>) it; |
||||
return Stream.of(property.getGetter(), property.getSetter()); |
||||
} |
||||
|
||||
if (it instanceof KProperty<?>) { |
||||
|
||||
KProperty<?> property = (KProperty<?>) it; |
||||
return Stream.of(property.getGetter()); |
||||
} |
||||
|
||||
if (it instanceof KFunction<?>) { |
||||
return Stream.of((KFunction<?>) it); |
||||
} |
||||
|
||||
return Stream.empty(); |
||||
} |
||||
|
||||
private static boolean isSame(KFunction<?> function, Method method) { |
||||
|
||||
Method javaMethod = ReflectJvmMapping.getJavaMethod(function); |
||||
return javaMethod != null && javaMethod.equals(method); |
||||
} |
||||
} |
||||
@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
|
||||
/* |
||||
* Copyright 2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.kotlin |
||||
|
||||
import kotlinx.coroutines.flow.Flow |
||||
import org.springframework.data.repository.NoRepositoryBean |
||||
import org.springframework.data.repository.Repository |
||||
import reactor.core.publisher.Mono |
||||
|
||||
/** |
||||
* Interface for generic CRUD operations using Kotlin Coroutines on a repository for a specific type. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 2.3 |
||||
* @see Flow |
||||
*/ |
||||
@NoRepositoryBean |
||||
interface CoCrudRepository<T, ID> : Repository<T, ID> { |
||||
|
||||
/** |
||||
* Saves a given entity. Use the returned instance for further operations as the save operation might have changed the |
||||
* entity instance completely. |
||||
* |
||||
* @param entity must not be null. |
||||
* @return the saved entity. |
||||
* @throws IllegalArgumentException in case the given entity is null. |
||||
*/ |
||||
suspend fun <S : T> save(entity: S): T |
||||
|
||||
/** |
||||
* Saves all given entities. |
||||
* |
||||
* @param entities must not be null. |
||||
* @return [Flow] emitting the saved entities. |
||||
* @throws IllegalArgumentException in case the given [entities][Flow] or one of its entities is |
||||
* null. |
||||
*/ |
||||
fun <S : T> saveAll(entities: Iterable<S>): Flow<S> |
||||
|
||||
/** |
||||
* Saves all given entities. |
||||
* |
||||
* @param entityStream must not be null. |
||||
* @return [Flow] emitting the saved entities. |
||||
* @throws IllegalArgumentException in case the given [entityStream][Flow] is null. |
||||
*/ |
||||
fun <S : T> saveAll(entityStream: Flow<S>): Flow<S> |
||||
|
||||
/** |
||||
* Retrieves an entity by its id. |
||||
* |
||||
* @param id must not be null. |
||||
* @return [Mono] emitting the entity with the given id or empty if none found. |
||||
* @throws IllegalArgumentException in case the given id is null. |
||||
*/ |
||||
suspend fun findById(id: ID): T? |
||||
|
||||
/** |
||||
* Returns whether an entity with the given id exists. |
||||
* |
||||
* @param id must not be null. |
||||
* @return true if an entity with the given id exists, false otherwise. |
||||
* @throws IllegalArgumentException in case the given id is null. |
||||
*/ |
||||
suspend fun existsById(id: ID): Boolean |
||||
|
||||
/** |
||||
* Returns all instances of the type. |
||||
* |
||||
* @return [Flow] emitting all entities. |
||||
*/ |
||||
fun findAll(): Flow<T> |
||||
|
||||
/** |
||||
* Returns all instances of the type `T` with the given IDs. |
||||
* |
||||
* |
||||
* If some or all ids are not found, no entities are returned for these IDs. |
||||
* |
||||
* |
||||
* Note that the order of elements in the result is not guaranteed. |
||||
* |
||||
* @param ids must not be null nor contain any null values. |
||||
* @return [Flow] emitting the found entities. The size can be equal or less than the number of given |
||||
* ids. |
||||
* @throws IllegalArgumentException in case the given [ids][Iterable] or one of its items is null. |
||||
*/ |
||||
fun findAllById(ids: Iterable<ID>): Flow<T> |
||||
|
||||
/** |
||||
* Returns the number of entities available. |
||||
* |
||||
* @return number of entities. |
||||
*/ |
||||
suspend fun count(): Long |
||||
|
||||
/** |
||||
* Deletes the entity with the given id. |
||||
* |
||||
* @param id must not be null. |
||||
* @throws IllegalArgumentException in case the given id is null. |
||||
*/ |
||||
suspend fun deleteById(id: ID) |
||||
|
||||
/** |
||||
* Deletes a given entity. |
||||
* |
||||
* @param entity must not be null. |
||||
* @throws IllegalArgumentException in case the given entity is null. |
||||
*/ |
||||
suspend fun delete(entity: T) |
||||
|
||||
/** |
||||
* Deletes the given entities. |
||||
* |
||||
* @param entities must not be null. |
||||
* @throws IllegalArgumentException in case the given [entities][Iterable] or one of its entities is |
||||
* null. |
||||
*/ |
||||
suspend fun deleteAll(entities: Iterable<T>) |
||||
|
||||
/** |
||||
* Deletes all entities managed by the repository. |
||||
*/ |
||||
suspend fun deleteAll() |
||||
} |
||||
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
/* |
||||
* Copyright 2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.kotlin |
||||
|
||||
import kotlinx.coroutines.flow.Flow |
||||
import org.springframework.data.domain.Sort |
||||
import org.springframework.data.repository.NoRepositoryBean |
||||
|
||||
/** |
||||
* Extension of [CoCrudRepository] to provide additional methods to retrieve entities using the sorting |
||||
* abstraction. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 2.3 |
||||
* @see Flow |
||||
* @see Sort |
||||
*/ |
||||
@NoRepositoryBean |
||||
interface CoSortingRepository<T, ID> : CoCrudRepository<T, ID> { |
||||
|
||||
/** |
||||
* Returns all entities sorted by the given options. |
||||
* |
||||
* @param sort must not be null. |
||||
* @return all entities sorted by the given options. |
||||
* @throws IllegalArgumentException in case the given [Sort] is null. |
||||
*/ |
||||
fun findAll(sort: Sort): Flow<T> |
||||
} |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
/** |
||||
* Support for Kotlin Coroutines repositories. |
||||
*/ |
||||
@org.springframework.lang.NonNullApi |
||||
package org.springframework.data.repository.kotlin; |
||||
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
/* |
||||
* Copyright 2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.core.support; |
||||
|
||||
import static org.mockito.Mockito.*; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.Optional; |
||||
|
||||
import org.mockito.Mockito; |
||||
|
||||
import org.springframework.data.projection.ProjectionFactory; |
||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor; |
||||
import org.springframework.data.repository.core.EntityInformation; |
||||
import org.springframework.data.repository.core.NamedQueries; |
||||
import org.springframework.data.repository.core.RepositoryInformation; |
||||
import org.springframework.data.repository.core.RepositoryMetadata; |
||||
import org.springframework.data.repository.query.QueryLookupStrategy; |
||||
import org.springframework.data.repository.query.QueryLookupStrategy.Key; |
||||
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; |
||||
import org.springframework.data.repository.query.RepositoryQuery; |
||||
|
||||
/** |
||||
* @author Mark Paluch |
||||
*/ |
||||
public class DummyReactiveRepositoryFactory extends ReactiveRepositoryFactorySupport { |
||||
|
||||
public final DummyRepositoryFactory.MyRepositoryQuery queryOne = mock(DummyRepositoryFactory.MyRepositoryQuery.class); |
||||
public final RepositoryQuery queryTwo = mock(RepositoryQuery.class); |
||||
public final QueryLookupStrategy strategy = mock(QueryLookupStrategy.class); |
||||
|
||||
@SuppressWarnings("unchecked") private final QuerydslPredicateExecutor<Object> querydsl = mock( |
||||
QuerydslPredicateExecutor.class); |
||||
private final Object repository; |
||||
|
||||
public DummyReactiveRepositoryFactory(Object repository) { |
||||
|
||||
this.repository = repository; |
||||
|
||||
when(strategy.resolveQuery(Mockito.any(Method.class), Mockito.any(RepositoryMetadata.class), |
||||
Mockito.any(ProjectionFactory.class), Mockito.any(NamedQueries.class))).thenReturn(queryOne); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class) |
||||
*/ |
||||
@Override |
||||
@SuppressWarnings("unchecked") |
||||
public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) { |
||||
return mock(EntityInformation.class); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryMetadata) |
||||
*/ |
||||
@Override |
||||
protected Object getTargetRepository(RepositoryInformation information) { |
||||
return repository; |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata) |
||||
*/ |
||||
@Override |
||||
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) { |
||||
return repository.getClass(); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) |
||||
*/ |
||||
@Override |
||||
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(Key key, |
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) { |
||||
return Optional.of(strategy); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryFragments(org.springframework.data.repository.core.RepositoryMetadata) |
||||
*/ |
||||
@Override |
||||
protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { |
||||
|
||||
RepositoryComposition.RepositoryFragments fragments = super.getRepositoryFragments(metadata); |
||||
|
||||
return QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()) //
|
||||
? fragments.append(RepositoryComposition.RepositoryFragments.just(querydsl)) //
|
||||
: fragments; |
||||
} |
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* |
||||
* Copyright 2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.kotlin |
||||
|
||||
import io.mockk.mockk |
||||
import kotlinx.coroutines.runBlocking |
||||
import org.assertj.core.api.Assertions.assertThat |
||||
import org.junit.Before |
||||
import org.junit.Test |
||||
import org.springframework.data.repository.core.RepositoryMetadata |
||||
import org.springframework.data.repository.core.support.DummyReactiveRepositoryFactory |
||||
import org.springframework.data.repository.core.support.RepositoryComposition |
||||
import org.springframework.data.repository.core.support.RepositoryFragment |
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository |
||||
import org.springframework.data.repository.sample.User |
||||
|
||||
/** |
||||
* Unit tests for Coroutine repositories. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class CoCrudRepositoryCustomImplementationUnitTests { |
||||
|
||||
val backingRepository = mockk<ReactiveCrudRepository<User, String>>() |
||||
lateinit var factory: DummyReactiveRepositoryFactory; |
||||
lateinit var coRepository: MyCoRepository; |
||||
|
||||
@Before |
||||
fun before() { |
||||
factory = CustomDummyReactiveRepositoryFactory(backingRepository) |
||||
coRepository = factory.getRepository(MyCoRepository::class.java) |
||||
} |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun shouldInvokeFindAll() { |
||||
|
||||
val result = runBlocking { |
||||
coRepository.findOne("foo") |
||||
} |
||||
|
||||
assertThat(result).isNotNull() |
||||
} |
||||
|
||||
class CustomDummyReactiveRepositoryFactory(repository: Any) : DummyReactiveRepositoryFactory(repository) { |
||||
|
||||
override fun getRepositoryFragments(metadata: RepositoryMetadata): RepositoryComposition.RepositoryFragments { |
||||
return super.getRepositoryFragments(metadata).append(RepositoryFragment.implemented(MyCustomCoRepository::class.java, MyCustomCoRepositoryImpl())) |
||||
} |
||||
} |
||||
|
||||
interface MyCoRepository : CoCrudRepository<User, String>, MyCustomCoRepository |
||||
|
||||
interface MyCustomCoRepository { |
||||
|
||||
suspend fun findOne(id: String): User |
||||
} |
||||
|
||||
class MyCustomCoRepositoryImpl : MyCustomCoRepository { |
||||
|
||||
override suspend fun findOne(id: String): User { |
||||
return User() |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
/* |
||||
* Copyright 2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.kotlin |
||||
|
||||
import io.mockk.every |
||||
import io.mockk.mockk |
||||
import kotlinx.coroutines.flow.Flow |
||||
import kotlinx.coroutines.flow.toList |
||||
import kotlinx.coroutines.runBlocking |
||||
import org.assertj.core.api.Assertions.assertThat |
||||
import org.junit.Before |
||||
import org.junit.Test |
||||
import org.mockito.Mockito |
||||
import org.springframework.data.repository.core.support.DummyReactiveRepositoryFactory |
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository |
||||
import org.springframework.data.repository.sample.User |
||||
import reactor.core.publisher.Flux |
||||
import reactor.core.publisher.Mono |
||||
import rx.Observable |
||||
import rx.Single |
||||
|
||||
/** |
||||
* Unit tests for Coroutine repositories. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class CoCrudRepositoryUnitTests { |
||||
|
||||
val backingRepository = mockk<ReactiveCrudRepository<User, String>>() |
||||
lateinit var factory: DummyReactiveRepositoryFactory; |
||||
lateinit var coRepository: MyCoRepository; |
||||
|
||||
@Before |
||||
fun before() { |
||||
factory = DummyReactiveRepositoryFactory(backingRepository) |
||||
coRepository = factory.getRepository(MyCoRepository::class.java) |
||||
} |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun shouldInvokeFindAll() { |
||||
|
||||
val sample = User() |
||||
|
||||
every { backingRepository.findAll() }.returns(Flux.just(sample)) |
||||
|
||||
val result = runBlocking { |
||||
coRepository.findAll().toList() |
||||
} |
||||
|
||||
assertThat(result).hasSize(1).containsOnly(sample) |
||||
} |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun shouldInvokeFindById() { |
||||
|
||||
val sample = User() |
||||
|
||||
every { backingRepository.findById("foo") }.returns(Mono.just(sample)) |
||||
|
||||
val result = runBlocking { |
||||
coRepository.findById("foo") |
||||
} |
||||
|
||||
assertThat(result).isNotNull().isEqualTo(sample) |
||||
} |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun shouldBridgeQueryMethod() { |
||||
|
||||
val sample = User() |
||||
|
||||
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null))).thenReturn(Mono.just(sample)) |
||||
|
||||
val result = runBlocking { |
||||
coRepository.findOne("foo") |
||||
} |
||||
|
||||
assertThat(result).isNotNull().isEqualTo(sample) |
||||
} |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun shouldBridgeRxJavaQueryMethod() { |
||||
|
||||
val sample = User() |
||||
|
||||
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null))).thenReturn(Single.just(sample)) |
||||
|
||||
val result = runBlocking { |
||||
coRepository.findOne("foo") |
||||
} |
||||
|
||||
assertThat(result).isNotNull().isEqualTo(sample) |
||||
} |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun shouldBridgeFlowMethod() { |
||||
|
||||
val sample = User() |
||||
|
||||
Mockito.`when`(factory.queryOne.execute(arrayOf("foo"))).thenReturn(Flux.just(sample), Flux.empty<User>()) |
||||
|
||||
val result = runBlocking { |
||||
coRepository.findMultiple("foo").toList() |
||||
} |
||||
|
||||
assertThat(result).hasSize(1).containsOnly(sample) |
||||
|
||||
val emptyResult = runBlocking { |
||||
coRepository.findMultiple("foo").toList() |
||||
} |
||||
|
||||
assertThat(emptyResult).isEmpty() |
||||
} |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun shouldBridgeRxJavaToFlowMethod() { |
||||
|
||||
val sample = User() |
||||
|
||||
Mockito.`when`(factory.queryOne.execute(arrayOf("foo"))).thenReturn(Observable.just(sample)) |
||||
|
||||
val result = runBlocking { |
||||
coRepository.findMultiple("foo").toList() |
||||
} |
||||
|
||||
assertThat(result).hasSize(1).containsOnly(sample) |
||||
} |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun shouldBridgeSuspendedFlowMethod() { |
||||
|
||||
val sample = User() |
||||
|
||||
Mockito.`when`(factory.queryOne.execute(arrayOf("foo", null))).thenReturn(Flux.just(sample), Flux.empty<User>()) |
||||
|
||||
val result = runBlocking { |
||||
coRepository.findSuspendedMultiple("foo").toList() |
||||
} |
||||
|
||||
assertThat(result).hasSize(1).containsOnly(sample) |
||||
|
||||
val emptyResult = runBlocking { |
||||
coRepository.findSuspendedMultiple("foo").toList() |
||||
} |
||||
|
||||
assertThat(emptyResult).isEmpty() |
||||
} |
||||
|
||||
interface MyCoRepository : CoCrudRepository<User, String> { |
||||
|
||||
suspend fun findOne(id: String): User |
||||
|
||||
fun findMultiple(id: String): Flow<User> |
||||
|
||||
suspend fun findSuspendedMultiple(id: String): Flow<User> |
||||
} |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright 2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.repository.query |
||||
|
||||
import org.assertj.core.api.Assertions.assertThat |
||||
import org.junit.Test |
||||
import org.springframework.core.DefaultParameterNameDiscoverer |
||||
import org.springframework.core.MethodParameter |
||||
import org.springframework.core.ParameterNameDiscoverer |
||||
import kotlin.reflect.jvm.javaMethod |
||||
|
||||
/** |
||||
* Unit tests for [Parameter]. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
class ParameterUnitTests { |
||||
|
||||
@Test // DATACMNS-1508 |
||||
fun `should consider Continuation a special parameter`() { |
||||
|
||||
val methodParameter = MethodParameter(MyCoroutineRepository::hello.javaMethod, 0) |
||||
methodParameter.initParameterNameDiscovery(DefaultParameterNameDiscoverer()) |
||||
val parameter = Parameter(methodParameter) |
||||
|
||||
assertThat(parameter.name).isEmpty() |
||||
assertThat(parameter.isBindable).isFalse() |
||||
} |
||||
|
||||
interface MyCoroutineRepository { |
||||
suspend fun hello(): Any |
||||
} |
||||
} |
||||
Loading…
Reference in new issue