Browse Source

#2 - Add R2dbcRepositoryFactory and simple query subsystem.

pull/1188/head
Mark Paluch 8 years ago
parent
commit
f794bfc3ab
  1. 9
      src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java
  2. 27
      src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java
  3. 147
      src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java
  4. 36
      src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java
  5. 108
      src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java
  6. 33
      src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java
  7. 41
      src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java
  8. 31
      src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java
  9. 80
      src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java
  10. 52
      src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java
  11. 99
      src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java
  12. 87
      src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java
  13. 227
      src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java
  14. 62
      src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java
  15. 110
      src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java
  16. 111
      src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java
  17. 159
      src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java
  18. 56
      src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java
  19. 67
      src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java
  20. 157
      src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java
  21. 140
      src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java
  22. 105
      src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java
  23. 73
      src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java
  24. 8
      src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java

9
src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java

@ -37,9 +37,10 @@ import org.springframework.util.ClassUtils; @@ -37,9 +37,10 @@ import org.springframework.util.ClassUtils;
*/
public class MappingR2dbcConverter {
private final MappingContext<JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext;
private final MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext;
public MappingR2dbcConverter(MappingContext<JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext) {
public MappingR2dbcConverter(
MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
}
@ -97,4 +98,8 @@ public class MappingR2dbcConverter { @@ -97,4 +98,8 @@ public class MappingR2dbcConverter {
return object;
};
}
public MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> getMappingContext() {
return mappingContext;
}
}

27
src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
/**
* R2DBC specific {@link org.springframework.data.repository.Repository} interface with reactive support.
*
* @author Mark Paluch
*/
@NoRepositoryBean
public interface R2dbcRepository<T, ID> extends ReactiveCrudRepository<T, ID> {}

147
src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.reactivestreams.Publisher;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec;
import org.springframework.data.jdbc.core.function.FetchSpec;
import org.springframework.data.jdbc.core.function.MappingR2dbcConverter;
import org.springframework.data.jdbc.repository.query.R2dbcQueryExecution.ResultProcessingConverter;
import org.springframework.data.jdbc.repository.query.R2dbcQueryExecution.ResultProcessingExecution;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.util.Assert;
/**
* Base class for reactive {@link RepositoryQuery} implementations for R2DBC.
*
* @author Mark Paluch
*/
public abstract class AbstractR2dbcQuery implements RepositoryQuery {
private final R2dbcQueryMethod method;
private final DatabaseClient databaseClient;
private final MappingR2dbcConverter converter;
private final EntityInstantiators instantiators;
/**
* Creates a new {@link AbstractR2dbcQuery} from the given {@link R2dbcQueryMethod} and {@link DatabaseClient}.
*
* @param method must not be {@literal null}.
* @param databaseClient must not be {@literal null}.
* @param converter must not be {@literal null}.
*/
public AbstractR2dbcQuery(R2dbcQueryMethod method, DatabaseClient databaseClient, MappingR2dbcConverter converter) {
Assert.notNull(method, "R2dbcQueryMethod must not be null!");
Assert.notNull(databaseClient, "DatabaseClient must not be null!");
Assert.notNull(converter, "MappingR2dbcConverter must not be null!");
this.method = method;
this.databaseClient = databaseClient;
this.converter = converter;
this.instantiators = new EntityInstantiators();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
*/
public R2dbcQueryMethod getQueryMethod() {
return method;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
*/
public Object execute(Object[] parameters) {
return method.hasReactiveWrapperParameter() ? executeDeferred(parameters)
: execute(new JdbcParametersParameterAccessor(method, parameters));
}
@SuppressWarnings("unchecked")
private Object executeDeferred(Object[] parameters) {
R2dbcParameterAccessor parameterAccessor = new R2dbcParameterAccessor(method, parameters);
if (getQueryMethod().isCollectionQuery()) {
return Flux.defer(() -> (Publisher<Object>) execute(parameterAccessor));
}
return Mono.defer(() -> (Mono<Object>) execute(parameterAccessor));
}
private Object execute(JdbcParameterAccessor parameterAccessor) {
// TODO: ConvertingParameterAccessor
BindableQuery query = createQuery(parameterAccessor);
ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor);
GenericExecuteSpec boundQuery = query.bind(databaseClient.execute().sql(query));
FetchSpec<?> fetchSpec = boundQuery.as(resolveResultType(processor)).fetch();
String tableName = method.getEntityInformation().getTableName();
R2dbcQueryExecution execution = getExecution(
new ResultProcessingConverter(processor, converter.getMappingContext(), instantiators));
return execution.execute(fetchSpec, processor.getReturnedType().getDomainType(), tableName);
}
private Class<?> resolveResultType(ResultProcessor resultProcessor) {
ReturnedType returnedType = resultProcessor.getReturnedType();
return returnedType.isProjecting() ? returnedType.getDomainType() : returnedType.getReturnedType();
}
/**
* Returns the execution instance to use.
*
* @param resultProcessing must not be {@literal null}.
* @return
*/
private R2dbcQueryExecution getExecution(Converter<Object, Object> resultProcessing) {
return new ResultProcessingExecution(getExecutionToWrap(), resultProcessing);
}
private R2dbcQueryExecution getExecutionToWrap() {
if (method.isCollectionQuery()) {
return (q, t, c) -> q.all();
}
return (q, t, c) -> q.one();
}
/**
* Creates a {@link BindableQuery} instance using the given {@link ParameterAccessor}
*
* @param accessor must not be {@literal null}.
* @return
*/
protected abstract BindableQuery createQuery(JdbcParameterAccessor accessor);
}

36
src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import java.util.function.Supplier;
import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec;
/**
* Interface declaring a query that supplies SQL and can bind parameters to a {@link BindSpec}.
*
* @author Mark Paluch
*/
public interface BindableQuery extends Supplier<String> {
/**
* Bind parameters to the {@link BindSpec query}.
*
* @param bindSpec must not be {@literal null}.
* @return the bound query object.
*/
<T extends BindSpec<T>> T bind(T bindSpec);
}

108
src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.util.Assert;
/**
* {@link Converter} to instantiate DTOs from fully equipped domain objects.
*
* @author Mark Paluch
*/
class DtoInstantiatingConverter implements Converter<Object, Object> {
private final Class<?> targetType;
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context;
private final EntityInstantiator instantiator;
/**
* Creates a new {@link Converter} to instantiate DTOs.
*
* @param dtoType must not be {@literal null}.
* @param context must not be {@literal null}.
* @param instantiators must not be {@literal null}.
*/
public DtoInstantiatingConverter(Class<?> dtoType,
MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> context,
EntityInstantiators instantiator) {
Assert.notNull(dtoType, "DTO type must not be null!");
Assert.notNull(context, "MappingContext must not be null!");
Assert.notNull(instantiator, "EntityInstantiators must not be null!");
this.targetType = dtoType;
this.context = context;
this.instantiator = instantiator.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType));
}
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public Object convert(Object source) {
if (targetType.isInterface()) {
return source;
}
final PersistentEntity<?, ?> sourceEntity = context.getRequiredPersistentEntity(source.getClass());
final PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source);
final PersistentEntity<?, ?> targetEntity = context.getRequiredPersistentEntity(targetType);
final PreferredConstructor<?, ? extends PersistentProperty<?>> constructor = targetEntity
.getPersistenceConstructor();
@SuppressWarnings({ "rawtypes", "unchecked" })
Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() {
@Override
public Object getParameterValue(Parameter parameter) {
return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName()));
}
});
final PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto);
targetEntity.doWithProperties(new SimplePropertyHandler() {
@Override
public void doWithPersistentProperty(PersistentProperty<?> property) {
if (constructor.isConstructorParameter(property)) {
return;
}
dtoAccessor.setProperty(property,
sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName())));
}
});
return dto;
}
}

33
src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import org.springframework.data.repository.core.EntityInformation;
/**
* JDBC specific {@link EntityInformation}.
*
* @author Mark Paluch
*/
public interface JdbcEntityInformation<T, ID> extends EntityInformation<T, ID> {
/**
* Returns the name of the table the entity shall be persisted to.
*
* @return
*/
String getTableName();
}

41
src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.repository.core.EntityMetadata;
/**
* Extension of {@link EntityMetadata} to additionally expose the collection name an entity shall be persisted to.
*
* @author Mark Paluch
*/
public interface JdbcEntityMetadata<T> extends EntityMetadata<T> {
/**
* Returns the name of the table the entity shall be persisted to.
*
* @return
*/
String getTableName();
/**
* Returns the {@link JdbcPersistentEntity} that supposed to determine the table to be queried.
*
* @return
*/
JdbcPersistentEntity<?> getTableEntity();
}

31
src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import org.springframework.data.repository.query.ParameterAccessor;
/**
* JDBC-specific {@link ParameterAccessor}.
*
* @author Mark Paluch
*/
public interface JdbcParameterAccessor extends ParameterAccessor {
/**
* Returns the raw parameter values of the underlying query method.
*/
Object[] getValues();
}

80
src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import java.lang.reflect.Method;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
/**
* Custom extension of {@link Parameters}.
*
* @author Mark Paluch
*/
public class JdbcParameters extends Parameters<JdbcParameters, JdbcParameter> {
/**
* Creates a new {@link JdbcParameters} instance from the given {@link Method}.
*
* @param method must not be {@literal null}.
*/
public JdbcParameters(Method method) {
super(method);
}
private JdbcParameters(List<JdbcParameter> parameters) {
super(parameters);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.Parameters#createParameter(org.springframework.core.MethodParameter)
*/
@Override
protected JdbcParameter createParameter(MethodParameter parameter) {
return new JdbcParameter(parameter);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List)
*/
@Override
protected JdbcParameters createFrom(List<JdbcParameter> parameters) {
return new JdbcParameters(parameters);
}
/**
* Custom {@link Parameter} implementation.
*
* @author Mark Paluch
*/
class JdbcParameter extends Parameter {
/**
* Creates a new {@link JdbcParameter}.
*
* @param parameter must not be {@literal null}.
*/
JdbcParameter(MethodParameter parameter) {
super(parameter);
}
}
}

52
src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import java.util.Arrays;
import java.util.List;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.QueryMethod;
/**
* JDBC-specific {@link ParametersParameterAccessor}.
*
* @author Mark Paluch
*/
public class JdbcParametersParameterAccessor extends ParametersParameterAccessor implements JdbcParameterAccessor {
private final List<Object> values;
/**
* Creates a new {@link JdbcParametersParameterAccessor}.
*
* @param method must not be {@literal null}.
* @param values must not be {@literal null}.
*/
public JdbcParametersParameterAccessor(QueryMethod method, Object[] values) {
super(method.getParameters(), values);
this.values = Arrays.asList(values);
}
/* (non-Javadoc)
* @see org.springframework.data.jdbc.repository.query.JdbcParameterAccessor#getValues()
*/
@Override
public Object[] getValues() {
return values.toArray();
}
}

99
src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.repository.util.ReactiveWrappers;
/**
* Reactive {@link org.springframework.data.repository.query.ParametersParameterAccessor} implementation that subscribes
* to reactive parameter wrapper types upon creation. This class performs synchronization when accessing parameters.
*
* @author Mark Paluch
*/
class R2dbcParameterAccessor extends JdbcParametersParameterAccessor {
private final Object[] values;
private final List<MonoProcessor<?>> subscriptions;
/**
* Creates a new {@link R2dbcParameterAccessor}.
*/
public R2dbcParameterAccessor(R2dbcQueryMethod method, Object... values) {
super(method, values);
this.values = values;
this.subscriptions = new ArrayList<>(values.length);
for (int i = 0; i < values.length; i++) {
Object value = values[i];
if (value == null || !ReactiveWrappers.supports(value.getClass())) {
subscriptions.add(null);
continue;
}
if (ReactiveWrappers.isSingleValueType(value.getClass())) {
subscriptions.add(ReactiveWrapperConverters.toWrapper(value, Mono.class).toProcessor());
} else {
subscriptions.add(ReactiveWrapperConverters.toWrapper(value, Flux.class).collectList().toProcessor());
}
}
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.ParametersParameterAccessor#getValue(int)
*/
@SuppressWarnings("unchecked")
@Override
protected <T> T getValue(int index) {
if (subscriptions.get(index) != null) {
return (T) subscriptions.get(index).block();
}
return super.getValue(index);
}
/* (non-Javadoc)
* @see org.springframework.data.jdbc.repository.query.JdbcParametersParameterAccessor#getValues()
*/
@Override
public Object[] getValues() {
Object[] result = new Object[values.length];
for (int i = 0; i < result.length; i++) {
result[i] = getValue(i);
}
return result;
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.ParametersParameterAccessor#getBindableValue(int)
*/
public Object getBindableValue(int index) {
return getValue(getParameters().getBindableParameter(index).getIndex());
}
}

87
src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java

@ -0,0 +1,87 @@ @@ -0,0 +1,87 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.jdbc.core.function.FetchSpec;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.util.ClassUtils;
/**
* Set of classes to contain query execution strategies. Depending (mostly) on the return type of a
* {@link org.springframework.data.repository.query.QueryMethod}.
*
* @author Mark Paluch
*/
interface R2dbcQueryExecution {
Object execute(FetchSpec<?> query, Class<?> type, String tableName);
/**
* An {@link R2dbcQueryExecution} that wraps the results of the given delegate with the given result processing.
*/
@RequiredArgsConstructor
final class ResultProcessingExecution implements R2dbcQueryExecution {
private final @NonNull R2dbcQueryExecution delegate;
private final @NonNull Converter<Object, Object> converter;
/* (non-Javadoc)
* @see org.springframework.data.jdbc.repository.query.R2dbcQueryExecution#execute(org.springframework.data.jdbc.core.function.FetchSpec, java.lang.Class, java.lang.String)
*/
@Override
public Object execute(FetchSpec<?> query, Class<?> type, String tableName) {
return converter.convert(delegate.execute(query, type, tableName));
}
}
/**
* A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}.
*/
@RequiredArgsConstructor
final class ResultProcessingConverter implements Converter<Object, Object> {
private final @NonNull ResultProcessor processor;
private final @NonNull MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext;
private final @NonNull EntityInstantiators instantiators;
/* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public Object convert(Object source) {
ReturnedType returnedType = processor.getReturnedType();
if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) {
return source;
}
Converter<Object, Object> converter = new DtoInstantiatingConverter(returnedType.getReturnedType(),
mappingContext, instantiators);
return processor.processResult(source, converter);
}
}
}

227
src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java

@ -0,0 +1,227 @@ @@ -0,0 +1,227 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import static org.springframework.data.repository.util.ClassUtils.*;
import java.lang.reflect.Method;
import java.util.Optional;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty;
import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.repository.util.ReactiveWrappers;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Reactive specific implementation of {@link QueryMethod}.
*
* @author Mark Paluch
*/
public class R2dbcQueryMethod extends QueryMethod {
private static final ClassTypeInformation<Page> PAGE_TYPE = ClassTypeInformation.from(Page.class);
private static final ClassTypeInformation<Slice> SLICE_TYPE = ClassTypeInformation.from(Slice.class);
private final Method method;
private final MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext;
private final Optional<Query> query;
private @Nullable JdbcEntityMetadata<?> metadata;
/**
* Creates a new {@link R2dbcQueryMethod} from the given {@link Method}.
*
* @param method must not be {@literal null}.
* @param metadata must not be {@literal null}.
* @param projectionFactory must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
*/
public R2dbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory,
MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext) {
super(method, metadata, projectionFactory);
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.mappingContext = mappingContext;
if (hasParameterOfType(method, Pageable.class)) {
TypeInformation<?> returnType = ClassTypeInformation.fromReturnTypeOf(method);
boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType());
boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType())
&& (PAGE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())
|| SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()));
if (singleWrapperWithWrappedPageableResult) {
throw new InvalidDataAccessApiUsageException(
String.format("'%s.%s' must not use sliced or paged execution. Please use Flux.buffer(size, skip).",
ClassUtils.getShortName(method.getDeclaringClass()), method.getName()));
}
if (!multiWrapper) {
throw new IllegalStateException(String.format(
"Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: %s",
method.toString()));
}
if (hasParameterOfType(method, Sort.class)) {
throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. "
+ "Use sorting capabilities on Pageble instead! Offending method: %s", method.toString()));
}
}
this.method = method;
this.query = Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class));
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method)
*/
@Override
protected JdbcParameters createParameters(Method method) {
return new JdbcParameters(method);
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#isCollectionQuery()
*/
@Override
public boolean isCollectionQuery() {
return !(isPageQuery() || isSliceQuery()) && ReactiveWrappers.isMultiValueType(method.getReturnType());
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#isModifyingQuery()
*/
@Override
public boolean isModifyingQuery() {
return super.isModifyingQuery();
}
/*
* All reactive query methods are streaming queries.
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#isStreamQuery()
*/
@Override
public boolean isStreamQuery() {
return true;
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#getEntityInformation()
*/
@Override
@SuppressWarnings("unchecked")
public JdbcEntityMetadata<?> getEntityInformation() {
if (metadata == null) {
Class<?> returnedObjectType = getReturnedObjectType();
Class<?> domainClass = getDomainClass();
if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType)) {
this.metadata = new SimpleJdbcEntityMetadata<>((Class<Object>) domainClass,
mappingContext.getRequiredPersistentEntity(domainClass));
} else {
JdbcPersistentEntity<?> returnedEntity = mappingContext.getPersistentEntity(returnedObjectType);
JdbcPersistentEntity<?> managedEntity = mappingContext.getRequiredPersistentEntity(domainClass);
returnedEntity = returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity
: returnedEntity;
JdbcPersistentEntity<?> tableEntity = domainClass.isAssignableFrom(returnedObjectType) ? returnedEntity
: managedEntity;
this.metadata = new SimpleJdbcEntityMetadata<>((Class<Object>) returnedEntity.getType(), tableEntity);
}
}
return this.metadata;
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#getParameters()
*/
@Override
public JdbcParameters getParameters() {
return (JdbcParameters) super.getParameters();
}
/**
* Check if the given {@link org.springframework.data.repository.query.QueryMethod} receives a reactive parameter
* wrapper as one of its parameters.
*
* @return {@literal true} if the given {@link org.springframework.data.repository.query.QueryMethod} receives a
* reactive parameter wrapper as one of its parameters.
*/
public boolean hasReactiveWrapperParameter() {
for (JdbcParameter parameter : getParameters()) {
if (ReactiveWrapperConverters.supports(parameter.getType())) {
return true;
}
}
return false;
}
/**
* Returns the required query string declared in a {@link Query} annotation or throws {@link IllegalStateException} if
* neither the annotation found nor the attribute was specified.
*
* @return the query string.
* @throws IllegalStateException in case query method has no annotated query.
*/
public String getRequiredAnnotatedQuery() {
return this.query.map(Query::value)
.orElseThrow(() -> new IllegalStateException("Query method " + this + " has no annotated query"));
}
/**
* Returns the {@link Query} annotation that is applied to the method or {@literal null} if none available.
*
* @return the optional query annotation.
*/
Optional<Query> getQueryAnnotation() {
return this.query;
}
/**
* @return {@literal true} if the {@link Method} is annotated with {@link Query}.
*/
public boolean hasAnnotatedQuery() {
return getQueryAnnotation().isPresent();
}
}

62
src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import lombok.Getter;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.util.Assert;
/**
* Default implementation of {@link JdbcEntityMetadata}.
*
* @author Mark Paluch
*/
class SimpleJdbcEntityMetadata<T> implements JdbcEntityMetadata<T> {
private final Class<T> type;
private final @Getter JdbcPersistentEntity<?> tableEntity;
/**
* Creates a new {@link SimpleJdbcEntityMetadata} using the given type and {@link JdbcPersistentEntity} to use for
* table lookups.
*
* @param type must not be {@literal null}.
* @param tableEntity must not be {@literal null}.
*/
SimpleJdbcEntityMetadata(Class<T> type, JdbcPersistentEntity<?> tableEntity) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(tableEntity, "Table entity must not be null!");
this.type = type;
this.tableEntity = tableEntity;
}
/* (non-Javadoc)
* @see org.springframework.data.repository.core.EntityMetadata#getJavaType()
*/
public Class<T> getJavaType() {
return type;
}
/* (non-Javadoc)
* @see org.springframework.data.jdbc.repository.query.JdbcEntityMetadata#getTableName()
*/
public String getTableName() {
return tableEntity.getTableName();
}
}

110
src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec;
import org.springframework.data.jdbc.core.function.MappingR2dbcConverter;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
/**
* String-based {@link StringBasedR2dbcQuery} implementation.
* <p>
* A {@link StringBasedR2dbcQuery} expects a query method to be annotated with {@link Query} with a SQL query.
*
* @author Mark Paluch
*/
public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
private final String sql;
/**
* Creates a new {@link StringBasedR2dbcQuery} for the given {@link StringBasedR2dbcQuery}, {@link DatabaseClient},
* {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}.
*
* @param queryMethod must not be {@literal null}.
* @param databaseClient must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
*/
public StringBasedR2dbcQuery(R2dbcQueryMethod queryMethod, DatabaseClient databaseClient,
MappingR2dbcConverter converter, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, databaseClient, converter, expressionParser,
evaluationContextProvider);
}
/**
* Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod},
* {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}.
*
* @param method must not be {@literal null}.
* @param databaseClient must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
*/
public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, DatabaseClient databaseClient,
MappingR2dbcConverter converter, SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
super(method, databaseClient, converter);
Assert.hasText(query, "Query must not be empty");
this.sql = query;
}
/* (non-Javadoc)
* @see org.springframework.data.jdbc.repository.query.AbstractR2dbcQuery#createQuery(org.springframework.data.jdbc.repository.query.JdbcParameterAccessor)
*/
@Override
protected BindableQuery createQuery(JdbcParameterAccessor accessor) {
return new BindableQuery() {
@Override
public <T extends BindSpec<T>> T bind(T bindSpec) {
T bindSpecToUse = bindSpec;
// TODO: Encapsulate PostgreSQL-specific bindings
int index = 1;
for (Object value : accessor.getValues()) {
if (value == null) {
if (accessor.hasBindableNullValue()) {
bindSpecToUse = bindSpecToUse.bindNull("$" + (index++));
}
} else {
bindSpecToUse = bindSpecToUse.bind("$" + (index++), value);
}
}
return bindSpecToUse;
}
@Override
public String get() {
return sql;
}
};
}
}

111
src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java

@ -0,0 +1,111 @@ @@ -0,0 +1,111 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.support;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.repository.query.JdbcEntityInformation;
import org.springframework.data.repository.core.support.PersistentEntityInformation;
import org.springframework.lang.Nullable;
import com.sun.corba.se.spi.ior.ObjectId;
/**
* {@link JdbcEntityInformation} implementation using a {@link JdbcPersistentEntity} instance to lookup the necessary
* information. Can be configured with a custom table name.
*
* @author Mark Paluch
*/
public class MappingJdbcEntityInformation<T, ID> extends PersistentEntityInformation<T, ID>
implements JdbcEntityInformation<T, ID> {
private final JdbcPersistentEntity<T> entityMetadata;
private final @Nullable String customTableName;
private final Class<ID> fallbackIdType;
/**
* Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity}.
*
* @param entity must not be {@literal null}.
*/
public MappingJdbcEntityInformation(JdbcPersistentEntity<T> entity) {
this(entity, null, null);
}
/**
* Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity} and fallback
* identifier type.
*
* @param entity must not be {@literal null}.
* @param fallbackIdType can be {@literal null}.
*/
public MappingJdbcEntityInformation(JdbcPersistentEntity<T> entity, @Nullable Class<ID> fallbackIdType) {
this(entity, null, fallbackIdType);
}
/**
* Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity} and custom table
* name.
*
* @param entity must not be {@literal null}.
* @param customTableName can be {@literal null}.
*/
public MappingJdbcEntityInformation(JdbcPersistentEntity<T> entity, String customTableName) {
this(entity, customTableName, null);
}
/**
* Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity}, collection name and
* identifier type.
*
* @param entity must not be {@literal null}.
* @param customTableName can be {@literal null}.
* @param idType can be {@literal null}.
*/
@SuppressWarnings("unchecked")
private MappingJdbcEntityInformation(JdbcPersistentEntity<T> entity, @Nullable String customTableName,
@Nullable Class<ID> idType) {
super(entity);
this.entityMetadata = entity;
this.customTableName = customTableName;
this.fallbackIdType = idType != null ? idType : (Class<ID>) ObjectId.class;
}
/* (non-Javadoc)
* @see org.springframework.data.jdbc.repository.query.JdbcEntityInformation#getTableName()
*/
public String getTableName() {
return customTableName == null ? entityMetadata.getTableName() : customTableName;
}
public String getIdAttribute() {
return entityMetadata.getRequiredIdProperty().getName();
}
/* (non-Javadoc)
* @see org.springframework.data.repository.core.support.PersistentEntityInformation#getIdType()
*/
@Override
public Class<ID> getIdType() {
if (this.entityMetadata.hasIdProperty()) {
return super.getIdType();
}
return fallbackIdType;
}
}

159
src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.support;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import java.util.Optional;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.function.MappingR2dbcConverter;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty;
import org.springframework.data.jdbc.repository.query.JdbcEntityInformation;
import org.springframework.data.jdbc.repository.query.R2dbcQueryMethod;
import org.springframework.data.jdbc.repository.query.StringBasedR2dbcQuery;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
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.core.support.ReactiveRepositoryFactorySupport;
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;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Factory to create {@link org.springframework.data.jdbc.repository.R2dbcRepository} instances.
*
* @author Mark Paluch
*/
public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final DatabaseClient databaseClient;
private final MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext;
private final MappingR2dbcConverter converter;
/**
* Creates a new {@link R2dbcRepositoryFactory} given {@link DatabaseClient} and {@link MappingContext}.
*
* @param databaseClient must not be {@literal null}.
* @param mappingContext must not be {@literal null}.
*/
public R2dbcRepositoryFactory(DatabaseClient databaseClient,
MappingContext<? extends JdbcPersistentEntity<?>, JdbcPersistentProperty> mappingContext) {
Assert.notNull(databaseClient, "DatabaseClient must not be null!");
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.databaseClient = databaseClient;
this.mappingContext = mappingContext;
this.converter = new MappingR2dbcConverter(mappingContext);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata)
*/
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return SimpleR2dbcRepository.class;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryInformation)
*/
@Override
protected Object getTargetRepository(RepositoryInformation information) {
JdbcEntityInformation<?, ?> entityInformation = getEntityInformation(information.getDomainType(), information);
return getTargetRepositoryViaReflection(information, entityInformation, databaseClient, converter);
}
/*
* (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(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new R2dbcQueryLookupStrategy(databaseClient, evaluationContextProvider, converter));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class)
*/
public <T, ID> JdbcEntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
return getEntityInformation(domainClass, null);
}
@SuppressWarnings("unchecked")
private <T, ID> JdbcEntityInformation<T, ID> getEntityInformation(Class<T> domainClass,
@Nullable RepositoryInformation information) {
JdbcPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(domainClass);
return new MappingJdbcEntityInformation<>((JdbcPersistentEntity<T>) entity);
}
/**
* {@link QueryLookupStrategy} to create R2DBC queries..
*
* @author Mark Paluch
*/
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy {
private final DatabaseClient databaseClient;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final MappingR2dbcConverter converter;
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries)
*/
@Override
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory, converter.getMappingContext());
String namedQueryName = queryMethod.getNamedQueryName();
if (namedQueries.hasQuery(namedQueryName)) {
String namedQuery = namedQueries.getQuery(namedQueryName);
return new StringBasedR2dbcQuery(namedQuery, queryMethod, databaseClient, converter, EXPRESSION_PARSER,
evaluationContextProvider);
} else if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedR2dbcQuery(queryMethod, databaseClient, converter, EXPRESSION_PARSER,
evaluationContextProvider);
}
throw new UnsupportedOperationException("Query derivation not yet supported!");
}
}
}

56
src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java

@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
*/
package org.springframework.data.jdbc.repository.support;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -30,8 +32,7 @@ import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; @@ -30,8 +32,7 @@ import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec;
import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec;
import org.springframework.data.jdbc.core.function.FetchSpec;
import org.springframework.data.jdbc.core.function.MappingR2dbcConverter;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.jdbc.repository.query.JdbcEntityInformation;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.util.Assert;
@ -40,30 +41,12 @@ import org.springframework.util.Assert; @@ -40,30 +41,12 @@ import org.springframework.util.Assert;
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, ID> {
private final DatabaseClient databaseClient;
private final MappingR2dbcConverter converter;
private final JdbcPersistentEntity<T> entity;
/**
* Create a new {@link SimpleR2dbcRepository} given {@link DatabaseClient} and {@link JdbcPersistentEntity}.
*
* @param databaseClient must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param entity must not be {@literal null}.
*/
public SimpleR2dbcRepository(DatabaseClient databaseClient, MappingR2dbcConverter converter,
JdbcPersistentEntity<T> entity) {
this.converter = converter;
Assert.notNull(databaseClient, "DatabaseClient must not be null!");
Assert.notNull(converter, "MappingR2dbcConverter must not be null!");
Assert.notNull(entity, "PersistentEntity must not be null!");
this.databaseClient = databaseClient;
this.entity = entity;
}
private final @NonNull JdbcEntityInformation<T, ID> entity;
private final @NonNull DatabaseClient databaseClient;
private final @NonNull MappingR2dbcConverter converter;
/* (non-Javadoc)
* @see org.springframework.data.repository.reactive.ReactiveCrudRepository#save(S)
@ -76,15 +59,14 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -76,15 +59,14 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
if (entity.isNew(objectToSave)) {
return databaseClient.insert() //
.into(entity.getType()) //
.into(entity.getJavaType()) //
.using(objectToSave) //
.exchange() //
.flatMap(it -> it.extract(converter.populateIdIfNecessary(objectToSave)).one());
}
// TODO: Extract in some kind of SQL generator
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(objectToSave);
Object id = identifierAccessor.getRequiredIdentifier();
Object id = entity.getRequiredId(objectToSave);
Map<String, Optional<Object>> fields = converter.getFieldsToUpdate(objectToSave);
@ -105,7 +87,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -105,7 +87,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
}
}
return exec.as(entity.getType()) //
return exec.as(entity.getJavaType()) //
.exchange() //
.flatMap(FetchSpec::rowsUpdated) //
.thenReturn(objectToSave);
@ -162,7 +144,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -162,7 +144,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
return databaseClient.execute()
.sql(String.format("SELECT * FROM %s WHERE %s = $1", entity.getTableName(), getIdColumnName())) //
.bind("$1", id) //
.as(entity.getType()) //
.as(entity.getJavaType()) //
.fetch() //
.one();
@ -206,7 +188,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -206,7 +188,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
*/
@Override
public Flux<T> findAll() {
return databaseClient.select().from(entity.getType()).fetch().all();
return databaseClient.select().from(entity.getJavaType()).fetch().all();
}
/* (non-Javadoc)
@ -235,7 +217,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -235,7 +217,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
GenericExecuteSpec exec = databaseClient.execute()
.sql(String.format("SELECT * FROM %s WHERE %s IN (%s)", entity.getTableName(), getIdColumnName(), bindings));
return bind(ids, exec).as(entity.getType()).fetch().all();
return bind(ids, exec).as(entity.getJavaType()).fetch().all();
});
}
@ -284,7 +266,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -284,7 +266,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
GenericExecuteSpec exec = databaseClient.execute()
.sql(String.format("DELETE FROM %s WHERE %s IN (%s)", entity.getTableName(), getIdColumnName(), bindings));
return bind(ids, exec).as(entity.getType()).fetch().rowsUpdated();
return bind(ids, exec).as(entity.getJavaType()).fetch().rowsUpdated();
}).then();
}
@ -297,9 +279,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -297,9 +279,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
Assert.notNull(objectToDelete, "Object to delete must not be null!");
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(objectToDelete);
return deleteById((ID) identifierAccessor.getRequiredIdentifier());
return deleteById(entity.getRequiredId(objectToDelete));
}
/* (non-Javadoc)
@ -323,8 +303,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -323,8 +303,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
Assert.notNull(objectPublisher, "The Object Publisher must not be null!");
Flux<ID> idPublisher = Flux.from(objectPublisher) //
.map(entity::getIdentifierAccessor) //
.map(identifierAccessor -> (ID) identifierAccessor.getRequiredIdentifier());
.map(entity::getRequiredId);
return deleteById(idPublisher);
}
@ -355,6 +334,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I @@ -355,6 +334,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
}
private String getIdColumnName() {
return entity.getRequiredIdProperty().getColumnName();
return converter.getMappingContext().getRequiredPersistentEntity(entity.getJavaType()).getRequiredIdProperty()
.getColumnName();
}
}

67
src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
/*
* Copyright 2017-2018 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
*
* http://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.jdbc.degraph;
import static de.schauderhaft.degraph.check.JCheck.*;
import static org.junit.Assert.*;
import de.schauderhaft.degraph.check.JCheck;
import scala.runtime.AbstractFunction1;
import org.junit.Ignore;
import org.junit.Test;
/**
* Test package dependencies for violations.
*
* @author Jens Schauder
*/
public class DependencyTests {
@Test // DATAJDBC-114
public void cycleFree() {
assertThat( //
classpath() //
.noJars() //
.including("org.springframework.data.jdbc.**") //
.filterClasspath("*target/classes") // exclude test code
.printOnFailure("degraph.graphml"),
JCheck.violationFree());
}
@Test // DATAJDBC-220
@Ignore("I don't understand why this fails after adding reactive repos - mp911de")
public void acrossModules() {
assertThat( //
classpath() //
// include only Spring Data related classes (for example no JDK code)
.including("org.springframework.data.**") //
.filterClasspath(new AbstractFunction1<String, Object>() {
@Override
public Object apply(String s) { //
// only the current module + commons
return s.endsWith("target/classes") || s.contains("spring-data-commons");
}
}) // exclude test code
.withSlicing("sub-modules", // sub-modules are defined by any of the following pattern.
"org.springframework.data.jdbc.(**).*", //
"org.springframework.data.(**).*") //
.printTo("degraph-across-modules.graphml"), // writes a graphml to this location
JCheck.violationFree());
}
}

157
src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository;
import static org.assertj.core.api.Assertions.*;
import io.r2dbc.spi.ConnectionFactory;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.function.DefaultReactiveDataAccessStrategy;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.Table;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.jdbc.repository.support.R2dbcRepositoryFactory;
import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory}.
*
* @author Mark Paluch
*/
public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport {
private static JdbcMappingContext mappingContext = new JdbcMappingContext();
private ConnectionFactory connectionFactory;
private DatabaseClient databaseClient;
private LegoSetRepository repository;
private JdbcTemplate jdbc;
@Before
public void before() {
Hooks.onOperatorDebug();
this.connectionFactory = createConnectionFactory();
this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory)
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build();
this.jdbc = createJdbcTemplate(createDataSource());
String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n"
+ " name varchar(255) NOT NULL,\n" + " manual integer NULL\n" + ");";
this.jdbc.execute("DROP TABLE IF EXISTS repo_legoset");
this.jdbc.execute(tableToCreate);
this.repository = new R2dbcRepositoryFactory(databaseClient, mappingContext).getRepository(LegoSetRepository.class);
}
@Test
public void shouldInsertNewItems() {
LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12);
LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13);
repository.saveAll(Arrays.asList(legoSet1, legoSet2)) //
.as(StepVerifier::create) //
.expectNextCount(2) //
.verifyComplete();
}
@Test
public void shouldFindItemsByManual() {
shouldInsertNewItems();
repository.findByManual(13) //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual.getName()).isEqualTo("FORSCHUNGSSCHIFF");
}) //
.verifyComplete();
}
@Test
public void shouldFindItemsByNameLike() {
shouldInsertNewItems();
repository.findByNameContains("%F%") //
.map(LegoSet::getName) //
.collectList() //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF");
}).verifyComplete();
}
@Test
public void shouldFindApplyingProjection() {
shouldInsertNewItems();
repository.findAsProjection() //
.map(Named::getName) //
.collectList() //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF");
}).verifyComplete();
}
interface LegoSetRepository extends ReactiveCrudRepository<LegoSet, Integer> {
@Query("SELECT * FROM repo_legoset WHERE name like $1")
Flux<LegoSet> findByNameContains(String name);
@Query("SELECT * FROM repo_legoset")
Flux<Named> findAsProjection();
@Query("SELECT * FROM repo_legoset WHERE manual = $1")
Mono<LegoSet> findByManual(int manual);
}
@Data
@Table("repo_legoset")
@AllArgsConstructor
@NoArgsConstructor
static class LegoSet {
@Id Integer id;
String name;
Integer manual;
}
interface Named {
String getName();
}
}

140
src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java

@ -0,0 +1,140 @@ @@ -0,0 +1,140 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
/**
* Unit test for {@link R2dbcQueryMethod}.
*
* @author Mark Paluch
*/
public class R2dbcQueryMethodUnitTests {
JdbcMappingContext context;
@Before
public void setUp() {
context = new JdbcMappingContext();
}
@Test
public void detectsCollectionFromReturnTypeIfReturnTypeAssignable() throws Exception {
R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "method");
JdbcEntityMetadata<?> metadata = queryMethod.getEntityInformation();
assertThat(metadata.getJavaType()).isAssignableFrom(Contact.class);
assertThat(metadata.getTableName()).isEqualTo("contact");
}
@Test
public void detectsTableNameFromRepoTypeIfReturnTypeNotAssignable() throws Exception {
R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "differentTable");
JdbcEntityMetadata<?> metadata = queryMethod.getEntityInformation();
assertThat(metadata.getJavaType()).isAssignableFrom(Address.class);
assertThat(metadata.getTableName()).isEqualTo("contact");
}
@Test(expected = IllegalArgumentException.class)
public void rejectsNullMappingContext() throws Exception {
Method method = PersonRepository.class.getMethod("findMonoByLastname", String.class, Pageable.class);
new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class),
new SpelAwareProxyProjectionFactory(), null);
}
@Test(expected = IllegalStateException.class)
public void rejectsMonoPageableResult() throws Exception {
queryMethod(PersonRepository.class, "findMonoByLastname", String.class, Pageable.class);
}
@Test
public void createsQueryMethodObjectForMethodReturningAnInterface() throws Exception {
queryMethod(SampleRepository.class, "methodReturningAnInterface");
}
@Test(expected = InvalidDataAccessApiUsageException.class)
public void throwsExceptionOnWrappedPage() throws Exception {
queryMethod(PersonRepository.class, "findMonoPageByLastname", String.class, Pageable.class);
}
@Test(expected = InvalidDataAccessApiUsageException.class)
public void throwsExceptionOnWrappedSlice() throws Exception {
queryMethod(PersonRepository.class, "findMonoSliceByLastname", String.class, Pageable.class);
}
@Test
public void fallsBackToRepositoryDomainTypeIfMethodDoesNotReturnADomainType() throws Exception {
R2dbcQueryMethod method = queryMethod(PersonRepository.class, "deleteByUserName", String.class);
assertThat(method.getEntityInformation().getJavaType()).isAssignableFrom(Contact.class);
}
private R2dbcQueryMethod queryMethod(Class<?> repository, String name, Class<?>... parameters) throws Exception {
Method method = repository.getMethod(name, parameters);
ProjectionFactory factory = new SpelAwareProxyProjectionFactory();
return new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context);
}
interface PersonRepository extends Repository<Contact, Long> {
Mono<Contact> findMonoByLastname(String lastname, Pageable pageRequest);
Mono<Page<Contact>> findMonoPageByLastname(String lastname, Pageable pageRequest);
Mono<Slice<Contact>> findMonoSliceByLastname(String lastname, Pageable pageRequest);
void deleteByUserName(String userName);
}
interface SampleRepository extends Repository<Contact, Long> {
List<Contact> method();
List<Address> differentTable();
Customer methodReturningAnInterface();
}
interface Customer {}
static class Contact {}
static class Address {}
}

105
src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java

@ -0,0 +1,105 @@ @@ -0,0 +1,105 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec;
import org.springframework.data.jdbc.core.function.MappingR2dbcConverter;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.ReflectionUtils;
/**
* Unit tests for {@link StringBasedR2dbcQuery}.
*
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class StringBasedR2dbcQueryUnitTests {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
@Mock private DatabaseClient databaseClient;
@Mock private GenericExecuteSpec bindSpec;
private JdbcMappingContext mappingContext;
private MappingR2dbcConverter converter;
private ProjectionFactory factory;
private RepositoryMetadata metadata;
@Before
@SuppressWarnings("unchecked")
public void setUp() {
this.mappingContext = new JdbcMappingContext();
this.converter = new MappingR2dbcConverter(this.mappingContext);
this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class);
this.factory = new SpelAwareProxyProjectionFactory();
when(bindSpec.bind(anyString(), any())).thenReturn(bindSpec);
}
@Test
public void bindsSimplePropertyCorrectly() {
StringBasedR2dbcQuery query = getQueryMethod("findByLastname", String.class);
R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod(), "White");
BindableQuery stringQuery = query.createQuery(accessor);
assertThat(stringQuery.get()).isEqualTo("SELECT * FROM person WHERE lastname = $1");
assertThat(stringQuery.bind(bindSpec)).isNotNull();
verify(bindSpec).bind("$1", "White");
}
private StringBasedR2dbcQuery getQueryMethod(String name, Class<?>... args) {
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory, converter.getMappingContext());
return new StringBasedR2dbcQuery(queryMethod, databaseClient, converter, PARSER,
ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT);
}
@SuppressWarnings("unused")
private interface SampleRepository extends Repository<Person, String> {
@Query("SELECT * FROM person WHERE lastname = $1")
Person findByLastname(String lastname);
}
static class Person {
}
}

73
src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
/*
* Copyright 2018 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
*
* http://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.jdbc.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.jdbc.core.function.DatabaseClient;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.repository.query.JdbcEntityInformation;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.Repository;
/**
* Unit test for {@link R2dbcRepositoryFactory}.
*
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class R2dbcRepositoryFactoryUnitTests {
@Mock DatabaseClient databaseClient;
@Mock @SuppressWarnings("rawtypes") MappingContext mappingContext;
@Mock @SuppressWarnings("rawtypes") JdbcPersistentEntity entity;
@Before
@SuppressWarnings("unchecked")
public void before() {
when(mappingContext.getRequiredPersistentEntity(Person.class)).thenReturn(entity);
}
@Test
@SuppressWarnings("unchecked")
public void usesMappingJdbcEntityInformationIfMappingContextSet() {
R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext);
JdbcEntityInformation<Person, Long> entityInformation = factory.getEntityInformation(Person.class);
assertThat(entityInformation).isInstanceOf(MappingJdbcEntityInformation.class);
}
@Test
@SuppressWarnings("unchecked")
public void createsRepositoryWithIdTypeLong() {
R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext);
MyPersonRepository repository = factory.getRepository(MyPersonRepository.class);
assertThat(repository).isNotNull();
}
interface MyPersonRepository extends Repository<Person, Long> {}
static class Person {}
}

8
src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java

@ -40,6 +40,7 @@ import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; @@ -40,6 +40,7 @@ import org.springframework.data.jdbc.core.function.MappingR2dbcConverter;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.core.mapping.Table;
import org.springframework.data.jdbc.repository.query.JdbcEntityInformation;
import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.jdbc.core.JdbcTemplate;
@ -65,10 +66,13 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS @@ -65,10 +66,13 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS
this.connectionFactory = createConnectionFactory();
this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory)
.dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build();
this.repository = new SimpleR2dbcRepository<>(databaseClient,
new MappingR2dbcConverter(mappingContext),
JdbcEntityInformation<LegoSet, Integer> entityInformation = new MappingJdbcEntityInformation<>(
(JdbcPersistentEntity<LegoSet>) mappingContext.getRequiredPersistentEntity(LegoSet.class));
this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient,
new MappingR2dbcConverter(mappingContext));
this.jdbc = createJdbcTemplate(createDataSource());
String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n"

Loading…
Cancel
Save