diff --git a/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java index 81a90f45b..7ab046f37 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java @@ -37,9 +37,10 @@ import org.springframework.util.ClassUtils; */ public class MappingR2dbcConverter { - private final MappingContext, JdbcPersistentProperty> mappingContext; + private final MappingContext, JdbcPersistentProperty> mappingContext; - public MappingR2dbcConverter(MappingContext, JdbcPersistentProperty> mappingContext) { + public MappingR2dbcConverter( + MappingContext, JdbcPersistentProperty> mappingContext) { this.mappingContext = mappingContext; } @@ -97,4 +98,8 @@ public class MappingR2dbcConverter { return object; }; } + + public MappingContext, JdbcPersistentProperty> getMappingContext() { + return mappingContext; + } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java new file mode 100644 index 000000000..d797a64b5 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java @@ -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 extends ReactiveCrudRepository {} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java b/src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java new file mode 100644 index 000000000..2e64068c3 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java @@ -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) execute(parameterAccessor)); + } + + return Mono.defer(() -> (Mono) 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 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); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java b/src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java new file mode 100644 index 000000000..d8361d7e7 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java @@ -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 { + + /** + * Bind parameters to the {@link BindSpec query}. + * + * @param bindSpec must not be {@literal null}. + * @return the bound query object. + */ + > T bind(T bindSpec); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java new file mode 100644 index 000000000..ceedda036 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java @@ -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 { + + private final Class targetType; + private final MappingContext, ? 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, 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> 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; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java new file mode 100644 index 000000000..6ce86dae0 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java @@ -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 extends EntityInformation { + + /** + * Returns the name of the table the entity shall be persisted to. + * + * @return + */ + String getTableName(); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java new file mode 100644 index 000000000..278cc0003 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java @@ -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 extends EntityMetadata { + + /** + * 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(); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java new file mode 100644 index 000000000..b7c57f1f9 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java @@ -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(); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java new file mode 100644 index 000000000..d199225f6 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java @@ -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 { + + /** + * 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 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 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); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java new file mode 100644 index 000000000..52c8c58d6 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java @@ -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 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(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java new file mode 100644 index 000000000..8a1c83c1f --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java @@ -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> 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 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()); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java new file mode 100644 index 000000000..eba2ab216 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java @@ -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 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 { + + private final @NonNull ResultProcessor processor; + private final @NonNull MappingContext, 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 converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + mappingContext, instantiators); + + return processor.processResult(source, converter); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java new file mode 100644 index 000000000..1e223decf --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java @@ -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_TYPE = ClassTypeInformation.from(Page.class); + private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class); + + private final Method method; + private final MappingContext, JdbcPersistentProperty> mappingContext; + private final Optional 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, 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) 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) 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 getQueryAnnotation() { + return this.query; + } + + /** + * @return {@literal true} if the {@link Method} is annotated with {@link Query}. + */ + public boolean hasAnnotatedQuery() { + return getQueryAnnotation().isPresent(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java b/src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java new file mode 100644 index 000000000..ea4dc5ed7 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java @@ -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 implements JdbcEntityMetadata { + + private final Class 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 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 getJavaType() { + return type; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.repository.query.JdbcEntityMetadata#getTableName() + */ + public String getTableName() { + return tableEntity.getTableName(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java new file mode 100644 index 000000000..319b25f1e --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java @@ -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. + *

+ * 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 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; + } + }; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java b/src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java new file mode 100644 index 000000000..b590ad77d --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java @@ -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 extends PersistentEntityInformation + implements JdbcEntityInformation { + + private final JdbcPersistentEntity entityMetadata; + private final @Nullable String customTableName; + private final Class fallbackIdType; + + /** + * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity}. + * + * @param entity must not be {@literal null}. + */ + public MappingJdbcEntityInformation(JdbcPersistentEntity 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 entity, @Nullable Class 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 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 entity, @Nullable String customTableName, + @Nullable Class idType) { + + super(entity); + + this.entityMetadata = entity; + this.customTableName = customTableName; + this.fallbackIdType = idType != null ? idType : (Class) 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 getIdType() { + + if (this.entityMetadata.hasIdProperty()) { + return super.getIdType(); + } + + return fallbackIdType; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java new file mode 100644 index 000000000..6fba57a3d --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java @@ -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, 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, 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 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 JdbcEntityInformation getEntityInformation(Class domainClass) { + return getEntityInformation(domainClass, null); + } + + @SuppressWarnings("unchecked") + private JdbcEntityInformation getEntityInformation(Class domainClass, + @Nullable RepositoryInformation information) { + + JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + + return new MappingJdbcEntityInformation<>((JdbcPersistentEntity) 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!"); + + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java index 955087ee6..66c09d87c 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java @@ -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; 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; * * @author Mark Paluch */ +@RequiredArgsConstructor public class SimpleR2dbcRepository implements ReactiveCrudRepository { - private final DatabaseClient databaseClient; - private final MappingR2dbcConverter converter; - private final JdbcPersistentEntity 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 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 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 implements ReactiveCrudRepository 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> fields = converter.getFieldsToUpdate(objectToSave); @@ -105,7 +87,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository implements ReactiveCrudRepository implements ReactiveCrudRepository 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 implements ReactiveCrudRepository implements ReactiveCrudRepository implements ReactiveCrudRepository implements ReactiveCrudRepository 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 implements ReactiveCrudRepository() { + @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()); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java new file mode 100644 index 000000000..34b2ff71d --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java @@ -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 { + + @Query("SELECT * FROM repo_legoset WHERE name like $1") + Flux findByNameContains(String name); + + @Query("SELECT * FROM repo_legoset") + Flux findAsProjection(); + + @Query("SELECT * FROM repo_legoset WHERE manual = $1") + Mono findByManual(int manual); + } + + @Data + @Table("repo_legoset") + @AllArgsConstructor + @NoArgsConstructor + static class LegoSet { + @Id Integer id; + String name; + Integer manual; + } + + interface Named { + String getName(); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java new file mode 100644 index 000000000..487cf7f02 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java @@ -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 { + + Mono findMonoByLastname(String lastname, Pageable pageRequest); + + Mono> findMonoPageByLastname(String lastname, Pageable pageRequest); + + Mono> findMonoSliceByLastname(String lastname, Pageable pageRequest); + + void deleteByUserName(String userName); + } + + interface SampleRepository extends Repository { + + List method(); + + List

differentTable(); + + Customer methodReturningAnInterface(); + } + + interface Customer {} + + static class Contact {} + + static class Address {} +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java new file mode 100644 index 000000000..a32de2aca --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -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 { + + @Query("SELECT * FROM person WHERE lastname = $1") + Person findByLastname(String lastname); + } + + static class Person { + + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java new file mode 100644 index 000000000..d040daad5 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -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 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 {} + + static class Person {} +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index ffe4f8ca7..7e358ac92 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -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 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 entityInformation = new MappingJdbcEntityInformation<>( (JdbcPersistentEntity) 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"