DATAJDBC-290 - Allow specification of resultsetExtractorClass.

A @Query annotation may now specify a class implementing ResultSetExtractor to be useed for extracting objects from ResultSets.

Original pull request: #101.
This commit is contained in:
Evgeni Dimitrov
2018-11-06 23:20:18 +02:00
committed by Jens Schauder
parent f032e6cf46
commit 4e7a109393
29 changed files with 732 additions and 205 deletions

View File

@@ -0,0 +1,27 @@
package org.springframework.data.jdbc.repository;
import org.springframework.data.jdbc.support.RowMapperResultsetExtractorEither;
import org.springframework.jdbc.core.ResultSetExtractor;
/**
* A map from a type to a {@link ResultSetExtractor} to be used for extracting that type from {@link java.sql.ResultSet}s.
*
* @author Jens Schauder
* @author Evgeni Dimitrov
*/
public interface QueryMappingConfiguration {
<T> RowMapperResultsetExtractorEither<?> getMapper(Class<T> type);
/**
* An immutable empty instance that will return {@literal null} for all arguments.
*/
QueryMappingConfiguration EMPTY = new QueryMappingConfiguration() {
@Override
public <T> RowMapperResultsetExtractorEither<?> getMapper(Class<T> type) {
return null;
}
};
}

View File

@@ -1,44 +0,0 @@
/*
* 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.jdbc.core.RowMapper;
import org.springframework.lang.Nullable;
/**
* A map from a type to a {@link RowMapper} to be used for extracting that type from {@link java.sql.ResultSet}s.
*
* @author Jens Schauder
*/
public interface RowMapperMap {
/**
* An immutable empty instance that will return {@literal null} for all arguments.
*/
RowMapperMap EMPTY = new RowMapperMap() {
/*
* (non-Javadoc)
* @see org.springframework.data.jdbc.repository.RowMapperMap#rowMapperFor(java.lang.Class)
*/
public <T> RowMapper<? extends T> rowMapperFor(Class<T> type) {
return null;
}
};
@Nullable
<T> RowMapper<? extends T> rowMapperFor(Class<T> type);
}

View File

@@ -1,75 +0,0 @@
/*
* 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.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A {@link RowMapperMap} that allows for registration of {@link RowMapper}s via a fluent Api.
*
* @author Jens Schauder
*/
public class ConfigurableRowMapperMap implements RowMapperMap {
private Map<Class<?>, RowMapper<?>> rowMappers = new LinkedHashMap<>();
/**
* Registers a the given {@link RowMapper} as to be used for the given type.
*
* @return this instance, so this can be used as a fluent interface.
*/
public <T> ConfigurableRowMapperMap register(Class<T> type, RowMapper<? extends T> rowMapper) {
rowMappers.put(type, rowMapper);
return this;
}
/**
* Returs a {@link RowMapper} for the given type if such a {@link RowMapper} is present. If an exact match is found
* that is returned. If not a {@link RowMapper} is returned that produces subtypes of the requested type. If no such
* {@link RowMapper} is found the method returns {@code null}.
*
* @param type the type to be produced by the returned {@link RowMapper}. Must not be {@code null}.
* @param <T> the type to be produced by the returned {@link RowMapper}.
* @return Guaranteed to be not {@code null}.
*/
@SuppressWarnings("unchecked")
@Nullable
public <T> RowMapper<? extends T> rowMapperFor(Class<T> type) {
Assert.notNull(type, "Type must not be null");
RowMapper<? extends T> candidate = (RowMapper<? extends T>) rowMappers.get(type);
if (candidate == null) {
for (Map.Entry<Class<?>, RowMapper<?>> entry : rowMappers.entrySet()) {
if (type.isAssignableFrom(entry.getKey())) {
candidate = (RowMapper<? extends T>) entry.getValue();
}
}
}
return candidate;
}
}

View File

@@ -0,0 +1,57 @@
package org.springframework.data.jdbc.repository.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.support.RowMapperResultsetExtractorEither;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.util.Assert;
/**
* A {@link QueryMappingConfiguration} that allows for registration of {@link RowMapper}s and {@link ResultSetExtractor}s via a fluent Api.
*
* @author Jens Schauder
* @author Evgeni Dimitrov
*/
public class DefaultQueryMappingConfiguration implements QueryMappingConfiguration{
private Map<Class<?>, RowMapperResultsetExtractorEither<?>> mappers = new LinkedHashMap<>();
public <T> RowMapperResultsetExtractorEither<?> getMapper(Class<T> type) {
Assert.notNull(type, "Type must not be null");
RowMapperResultsetExtractorEither<?> candidate = mappers.get(type);
if (candidate == null) {
for (Map.Entry<Class<?>, RowMapperResultsetExtractorEither<?>> entry : mappers.entrySet()) {
if (type.isAssignableFrom(entry.getKey())) {
candidate = entry.getValue();
}
}
}
return candidate;
}
/**
* Registers a the given {@link RowMapper} as to be used for the given type.
*
* @return this instance, so this can be used as a fluent interface.
*/
public <T> DefaultQueryMappingConfiguration registerRowMapper(Class<T> type, RowMapper<? extends T> rowMapper) {
mappers.put(type, RowMapperResultsetExtractorEither.of(rowMapper));
return this;
}
/**
* Registers a the given {@link ResultSetExtractor} as to be used for the given type.
*
* @return this instance, so this can be used as a fluent interface.
*/
public <T> DefaultQueryMappingConfiguration registerResultSetExtractor(Class<T> type, ResultSetExtractor resultSetExtractor) {
mappers.put(type, RowMapperResultsetExtractorEither.of(resultSetExtractor));
return this;
}
}

View File

@@ -22,6 +22,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.data.annotation.QueryAnnotation;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
/**
@@ -44,6 +45,13 @@ public @interface Query {
/**
* Optional {@link RowMapper} to use to convert the result of the query to domain class instances.
* Cannot be used along with {@link #resultSetExtractorClass()} only one of the two can be set.
*/
Class<? extends RowMapper> rowMapperClass() default RowMapper.class;
/**
* Optional {@link ResultSetExtractor} to use to convert the result of the query to domain class instances.
* Cannot be used along with {@link #rowMapperClass()} only one of the two can be set.
*/
Class<? extends ResultSetExtractor> resultSetExtractorClass() default ResultSetExtractor.class;
}

View File

@@ -0,0 +1,19 @@
package org.springframework.data.jdbc.repository.support;
import org.springframework.data.jdbc.repository.query.Query;
/**
* Exception thrown when both {@link Query#resultSetExtractorClass()} and {@link Query#rowMapperClass()} are used in one {@link Query}.
*
* @author Evgeni Dimitrov
*/
public class InvalidQueryConfiguration extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 6604189906427682546L;
public InvalidQueryConfiguration(String message) {
super(message);
}
}

View File

@@ -20,7 +20,8 @@ import java.lang.reflect.Method;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.EntityRowMapper;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.support.RowMapperResultsetExtractorEither;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
@@ -29,6 +30,7 @@ import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@@ -49,12 +51,12 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {
private final RelationalMappingContext context;
private final RelationalConverter converter;
private final DataAccessStrategy accessStrategy;
private final RowMapperMap rowMapperMap;
private final QueryMappingConfiguration mapperMap;
private final NamedParameterJdbcOperations operations;
/**
* Creates a new {@link JdbcQueryLookupStrategy} for the given {@link RelationalMappingContext},
* {@link DataAccessStrategy} and {@link RowMapperMap}.
* {@link DataAccessStrategy} and {@link QueryMappingConfiguration}.
*
* @param publisher must not be {@literal null}.
* @param context must not be {@literal null}.
@@ -63,7 +65,7 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {
* @param rowMapperMap must not be {@literal null}.
*/
JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, RelationalMappingContext context, RelationalConverter converter,
DataAccessStrategy accessStrategy, RowMapperMap rowMapperMap, NamedParameterJdbcOperations operations) {
DataAccessStrategy accessStrategy, QueryMappingConfiguration rowMapperMap, NamedParameterJdbcOperations operations) {
Assert.notNull(publisher, "Publisher must not be null!");
Assert.notNull(context, "RelationalMappingContext must not be null!");
@@ -75,7 +77,7 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {
this.context = context;
this.converter = converter;
this.accessStrategy = accessStrategy;
this.rowMapperMap = rowMapperMap;
this.mapperMap = rowMapperMap;
this.operations = operations;
}
@@ -89,36 +91,35 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {
JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory);
RowMapper<?> rowMapper = queryMethod.isModifyingQuery() ? null : createRowMapper(queryMethod);
RowMapperResultsetExtractorEither<?> mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod);
return new JdbcRepositoryQuery(publisher, context, queryMethod, operations, rowMapper);
return new JdbcRepositoryQuery(publisher, context, queryMethod, operations, mapper);
}
private RowMapper<?> createRowMapper(JdbcQueryMethod queryMethod) {
private RowMapperResultsetExtractorEither<?> createMapper(JdbcQueryMethod queryMethod) {
Class<?> returnedObjectType = queryMethod.getReturnedObjectType();
RelationalPersistentEntity<?> persistentEntity = context.getPersistentEntity(returnedObjectType);
if (persistentEntity == null) {
return SingleColumnRowMapper.newInstance(returnedObjectType, converter.getConversionService());
return RowMapperResultsetExtractorEither.of(
SingleColumnRowMapper.newInstance(returnedObjectType, converter.getConversionService()));
}
return determineDefaultRowMapper(queryMethod);
return determineDefaultMapper(queryMethod);
}
private RowMapper<?> determineDefaultRowMapper(JdbcQueryMethod queryMethod) {
private RowMapperResultsetExtractorEither<?> determineDefaultMapper(JdbcQueryMethod queryMethod) {
Class<?> domainType = queryMethod.getReturnedObjectType();
RowMapper<?> typeMappedRowMapper = rowMapperMap.rowMapperFor(domainType);
return typeMappedRowMapper == null //
? new EntityRowMapper<>( //
context.getRequiredPersistentEntity(domainType), //
context, //
converter, //
accessStrategy) //
: typeMappedRowMapper;
RowMapperResultsetExtractorEither<?> configuredQueryMapper = mapperMap.getMapper(domainType);
if(configuredQueryMapper != null) return configuredQueryMapper;
EntityRowMapper<?> defaultEntityRowMapper = new EntityRowMapper<>( //
context.getRequiredPersistentEntity(domainType), //
context, //
converter, //
accessStrategy);
return RowMapperResultsetExtractorEither.of(defaultEntityRowMapper);
}
}

View File

@@ -63,6 +63,16 @@ public class JdbcQueryMethod extends QueryMethod {
public Class<?> getRowMapperClass() {
return getMergedAnnotationAttribute("rowMapperClass");
}
/**
* Returns the class to be used as {@link org.springframework.jdbc.core.ResultSetExtractor}
*
* @return May be {@code null}.
*/
@Nullable
public Class<?> getResultSetExtractorClass() {
return getMergedAnnotationAttribute("resultSetExtractorClass");
}
/**
* Returns whether the query method is a modifying one.

View File

@@ -20,7 +20,7 @@ import java.util.Optional;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@@ -51,7 +51,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
private final DataAccessStrategy accessStrategy;
private final NamedParameterJdbcOperations operations;
private RowMapperMap rowMapperMap = RowMapperMap.EMPTY;
private QueryMappingConfiguration mapperMap = QueryMappingConfiguration.EMPTY;
/**
* Creates a new {@link JdbcRepositoryFactory} for the given {@link DataAccessStrategy},
@@ -81,11 +81,11 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
/**
* @param rowMapperMap must not be {@literal null} consider {@link RowMapperMap#EMPTY} instead.
*/
public void setRowMapperMap(RowMapperMap rowMapperMap) {
public void setRowMapperMap(QueryMappingConfiguration rowMapperMap) {
Assert.notNull(rowMapperMap, "RowMapperMap must not be null!");
this.rowMapperMap = rowMapperMap;
this.mapperMap = rowMapperMap;
}
@SuppressWarnings("unchecked")
@@ -133,6 +133,6 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
}
return Optional.of(new JdbcQueryLookupStrategy(publisher, context, converter, accessStrategy, rowMapperMap, operations));
return Optional.of(new JdbcQueryLookupStrategy(publisher, context, converter, accessStrategy, mapperMap, operations));
}
}

View File

@@ -23,7 +23,7 @@ import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.DefaultDataAccessStrategy;
import org.springframework.data.jdbc.core.SqlGeneratorSource;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.repository.Repository;
@@ -49,7 +49,7 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extend
private RelationalMappingContext mappingContext;
private RelationalConverter converter;
private DataAccessStrategy dataAccessStrategy;
private RowMapperMap rowMapperMap = RowMapperMap.EMPTY;
private QueryMappingConfiguration mapperMap = QueryMappingConfiguration.EMPTY;
private NamedParameterJdbcOperations operations;
/**
@@ -81,7 +81,7 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extend
JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(dataAccessStrategy, mappingContext,
converter, publisher, operations);
jdbcRepositoryFactory.setRowMapperMap(rowMapperMap);
jdbcRepositoryFactory.setRowMapperMap(mapperMap);
return jdbcRepositoryFactory;
}
@@ -106,8 +106,8 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extend
* {@literal null}.
*/
@Autowired(required = false)
public void setRowMapperMap(RowMapperMap rowMapperMap) {
this.rowMapperMap = rowMapperMap;
public void setRowMapperMap(QueryMappingConfiguration rowMapperMap) {
this.mapperMap = rowMapperMap;
}
@Autowired
@@ -137,8 +137,8 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extend
operations);
}
if (rowMapperMap == null) {
this.rowMapperMap = RowMapperMap.EMPTY;
if (mapperMap == null) {
this.mapperMap = QueryMappingConfiguration.EMPTY;
}
super.afterPropertiesSet();

View File

@@ -18,11 +18,13 @@ package org.springframework.data.jdbc.repository.support;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.jdbc.support.RowMapperResultsetExtractorEither;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
import org.springframework.data.relational.core.mapping.event.Identifier;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
@@ -49,7 +51,7 @@ class JdbcRepositoryQuery implements RepositoryQuery {
private final RelationalMappingContext context;
private final JdbcQueryMethod queryMethod;
private final NamedParameterJdbcOperations operations;
private final RowMapper<?> rowMapper;
private final RowMapperResultsetExtractorEither<?> mapper;
/**
* Creates a new {@link JdbcRepositoryQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
@@ -62,7 +64,7 @@ class JdbcRepositoryQuery implements RepositoryQuery {
* @param defaultRowMapper can be {@literal null} (only in case of a modifying query).
*/
JdbcRepositoryQuery(ApplicationEventPublisher publisher, RelationalMappingContext context, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
@Nullable RowMapper<?> defaultRowMapper) {
@Nullable RowMapperResultsetExtractorEither<?> defaultMapper) {
Assert.notNull(publisher, "Publisher must not be null!");
Assert.notNull(context, "Context must not be null!");
@@ -70,14 +72,14 @@ class JdbcRepositoryQuery implements RepositoryQuery {
Assert.notNull(operations, "NamedParameterJdbcOperations must not be null!");
if (!queryMethod.isModifyingQuery()) {
Assert.notNull(defaultRowMapper, "RowMapper must not be null!");
Assert.notNull(defaultMapper, "Mapper must not be null!");
}
this.publisher = publisher;
this.context = context;
this.queryMethod = queryMethod;
this.operations = operations;
this.rowMapper = createRowMapper(queryMethod, defaultRowMapper);
this.mapper = determineMapper(defaultMapper);
}
/*
@@ -99,15 +101,23 @@ class JdbcRepositoryQuery implements RepositoryQuery {
}
if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) {
List<?> result = operations.query(query, parameters, rowMapper);
List<?> result = null;
if(this.mapper.isResultSetExtractor()) {
result = (List<?>) operations.query(query, parameters, this.mapper.resultSetExtractor());
} else {
result = operations.query(query, parameters, this.mapper.rowMapper());
}
publishAfterLoad(result);
return result;
}
try {
Object result = operations.queryForObject(query, parameters, rowMapper);
Object result = null;
if(this.mapper.isResultSetExtractor()) {
result = operations.query(query,parameters, this.mapper.resultSetExtractor());
} else {
result = operations.queryForObject(query, parameters, this.mapper.rowMapper());
}
publishAfterLoad(result);
return result;
} catch (EmptyResultDataAccessException e) {
@@ -148,14 +158,30 @@ class JdbcRepositoryQuery implements RepositoryQuery {
return parameters;
}
private RowMapperResultsetExtractorEither<?> determineMapper(RowMapperResultsetExtractorEither<?> defaultMapper) {
RowMapperResultsetExtractorEither<?> configuredMapper = getConfiguredMapper(queryMethod);
if(configuredMapper != null) return configuredMapper;
return defaultMapper;
}
@Nullable
private static RowMapper<?> createRowMapper(JdbcQueryMethod queryMethod, @Nullable RowMapper<?> defaultRowMapper) {
private static RowMapperResultsetExtractorEither<?> getConfiguredMapper(JdbcQueryMethod queryMethod) {
Class<?> rowMapperClass = queryMethod.getRowMapperClass();
Class<?> resultSetExtractorClass = queryMethod.getResultSetExtractorClass();
if(isConfigured(rowMapperClass, RowMapper.class) && isConfigured(resultSetExtractorClass, ResultSetExtractor.class))
throw new InvalidQueryConfiguration("Cannot use both rowMapperClass and resultSetExtractorClass on @Query annotation. Query method: [" + queryMethod.getName() + "] query: [" + queryMethod.getAnnotatedQuery() + "]");
if(!isConfigured(rowMapperClass, RowMapper.class) && !isConfigured(resultSetExtractorClass, ResultSetExtractor.class))
return null;
if(isConfigured(rowMapperClass, RowMapper.class)) {
return RowMapperResultsetExtractorEither.of((RowMapper<?>) BeanUtils.instantiateClass(rowMapperClass));
} else {
return RowMapperResultsetExtractorEither.of((ResultSetExtractor<?>) BeanUtils.instantiateClass(resultSetExtractorClass));
}
}
return rowMapperClass == null || rowMapperClass == RowMapper.class //
? defaultRowMapper //
: (RowMapper<?>) BeanUtils.instantiateClass(rowMapperClass);
private static boolean isConfigured(Class<?> rowMapperClass, Class<?> defaultClass) {
return rowMapperClass != null && rowMapperClass != defaultClass;
}
private <T> void publishAfterLoad(Iterable<T> all) {

View File

@@ -0,0 +1,76 @@
package org.springframework.data.jdbc.support;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
/**
* Represents either a RowMapper or a ResultSetExtractor
*
* @author Evgeni Dimitrov
*/
public class RowMapperResultsetExtractorEither<T> {
private final RowMapper<T> rowMapper;
private final ResultSetExtractor<T> resultSetExtractor;
private RowMapperResultsetExtractorEither(RowMapper<T> rowMapper, ResultSetExtractor<T> resultSetExtractor) {
this.rowMapper = rowMapper;
this.resultSetExtractor = resultSetExtractor;
}
public static RowMapperResultsetExtractorEither<?> of(RowMapper<?> rowMapper) {
return new RowMapperResultsetExtractorEither<>(rowMapper, null);
}
public boolean isRowMapper() {
return this.rowMapper != null;
}
public RowMapper<?> rowMapper() {
return this.rowMapper;
}
public static RowMapperResultsetExtractorEither<?> of(ResultSetExtractor<?> resultSetExtractor) {
return new RowMapperResultsetExtractorEither<>(null, resultSetExtractor);
}
public boolean isResultSetExtractor() {
return this.resultSetExtractor != null;
}
public ResultSetExtractor<?> resultSetExtractor() {
return this.resultSetExtractor;
}
@Override
public String toString() {
return String.format("RowMapperResultsetExtractorEither[%s]", this.rowMapper != null ? this.rowMapper : this.resultSetExtractor);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((resultSetExtractor == null) ? 0 : resultSetExtractor.hashCode());
result = prime * result + ((rowMapper == null) ? 0 : rowMapper.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
RowMapperResultsetExtractorEither other = (RowMapperResultsetExtractorEither) obj;
if (resultSetExtractor == null) {
if (other.resultSetExtractor != null) return false;
} else {
if (!resultSetExtractor.equals(other.resultSetExtractor)) return false;
}
if (rowMapper == null) {
if (other.rowMapper != null) return false;
} else {
if (!rowMapper.equals(other.rowMapper)) return false;
}
return true;
}
}

View File

@@ -0,0 +1,110 @@
/*
* 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.repository;
import static org.assertj.core.api.Assertions.assertThat;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.dao.DataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
import org.springframework.transaction.annotation.Transactional;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Very simple use cases for creation and usage of {@link ResultSetExtractor}s in JdbcRepository.
*
* @author Evgeni Dimitrov
*/
@ContextConfiguration
@Transactional
public class JdbcRepositoryMapperMapResultSetExtractorIntegrationTests {
private static String CAR_MODEL = "ResultSetExtracotr Car";
@Configuration
@Import(TestConfiguration.class)
@EnableJdbcRepositories(considerNestedRepositories = true)
static class Config {
@Bean
Class<?> testClass() {
return JdbcRepositoryMapperMapResultSetExtractorIntegrationTests.class;
}
@Bean
QueryMappingConfiguration mappers() {
return new DefaultQueryMappingConfiguration()
.registerResultSetExtractor(Car.class, new CarResultSetExtractor());
}
}
@ClassRule public static final SpringClassRule classRule = new SpringClassRule();
@Rule public SpringMethodRule methodRule = new SpringMethodRule();
@Autowired NamedParameterJdbcTemplate template;
@Autowired CarRepository carRepository;
@Test // DATAJDBC-290
public void customFindAllCarsPicksResultSetExtractorFromMapperMap() {
carRepository.save(new Car(null, "Some model"));
Iterable<Car> cars = carRepository.customFindAll();
assertThat(cars).hasSize(1);
assertThat(cars).allMatch(car -> CAR_MODEL.equals(car.getModel()));
}
interface CarRepository extends CrudRepository<Car, Long> {
@Query("select * from car")
public List<Car> customFindAll();
}
@Data
@AllArgsConstructor
static class Car {
@Id
private Long id;
private String model;
}
static class CarResultSetExtractor implements ResultSetExtractor<List<Car>> {
@Override
public List<Car> extractData(ResultSet rs) throws SQLException, DataAccessException {
return Arrays.asList(new Car(1L, CAR_MODEL));
}
}
}

View File

@@ -0,0 +1,171 @@
/*
* 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.repository;
import static org.assertj.core.api.Assertions.assertThat;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.RecoverableDataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.TestConfiguration;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
import org.springframework.transaction.annotation.Transactional;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Very simple use cases for creation and usage of {@link ResultSetExtractor}s in JdbcRepository.
*
* @author Evgeni Dimitrov
*/
@ContextConfiguration
@Transactional
public class JdbcRepositoryResultSetExtractorIntegrationTests {
@Configuration
@Import(TestConfiguration.class)
static class Config {
@Autowired JdbcRepositoryFactory factory;
@Bean
Class<?> testClass() {
return JdbcRepositoryResultSetExtractorIntegrationTests.class;
}
@Bean
PersonRepository personEntityRepository() {
return factory.getRepository(PersonRepository.class);
}
}
@ClassRule public static final SpringClassRule classRule = new SpringClassRule();
@Rule public SpringMethodRule methodRule = new SpringMethodRule();
@Autowired NamedParameterJdbcTemplate template;
@Autowired PersonRepository personRepository;
@Test // DATAJDBC-290
public void findAllPeopleWithAdressesReturnsEmptyWhenNoneFound() {
// NOT saving anything, so DB is empty
assertThat(personRepository.findAllPeopleWithAdresses()).isEmpty();
}
@Test // DATAJDBC-290
public void findAllPeopleWithAdressesReturnsOnePersonWithoutAdresses() {
personRepository.save(new Person(null, "Joe", null));
assertThat(personRepository.findAllPeopleWithAdresses()).hasSize(1);
}
@Test // DATAJDBC-290
public void findAllPeopleWithAdressesReturnsOnePersonWithAdresses() {
final String personName = "Joe";
Person savedPerson = personRepository.save(new Person(null, personName, null));
String street1 = "Klokotnitsa";
MapSqlParameterSource paramsAddress1 = buildAddressParameters(savedPerson.getId(), street1);
template.update("insert into address (street, person_id) values (:street, :personId)",paramsAddress1);
String street2 = "bul. Hristo Botev";
MapSqlParameterSource paramsAddress2 = buildAddressParameters(savedPerson.getId(), street2);
template.update("insert into address (street, person_id) values (:street, :personId)",paramsAddress2);
List<Person> people = personRepository.findAllPeopleWithAdresses();
assertThat(people).hasSize(1);
Person person = people.get(0);
assertThat(person.getName()).isEqualTo(personName);
assertThat(person.getAdresses()).hasSize(2);
assertThat(person.getAdresses()).extracting(a -> a.getStreet()).containsExactlyInAnyOrder(street1, street2);
}
private MapSqlParameterSource buildAddressParameters(Long id, String streetName) {
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("street", streetName, Types.VARCHAR);
params.addValue("personId", id, Types.NUMERIC);
return params;
}
interface PersonRepository extends CrudRepository<Person, Long> {
@Query(value="select p.id, p.name, a.id addrId, a.street from person p left join address a on(p.id = a.person_id)",
resultSetExtractorClass=PersonResultSetExtractor.class)
public List<Person> findAllPeopleWithAdresses();
}
@Data
@AllArgsConstructor
static class Person {
@Id
private Long id;
private String name;
private List<Address> adresses;
}
@Data
@AllArgsConstructor
static class Address {
@Id
private Long id;
private String street;
}
static class PersonResultSetExtractor implements ResultSetExtractor<List<Person>> {
@Override
public List<Person> extractData(ResultSet rs) throws SQLException, DataAccessException {
Map<Long, Person> peopleById = new HashMap<>();
while(rs.next()) {
long personId = rs.getLong("id");
Person currentPerson = peopleById.computeIfAbsent(personId, t -> {
try {
return new Person(personId, rs.getString("name"), new ArrayList<>());
} catch (SQLException e) {
throw new RecoverableDataAccessException("Error mapping Person", e);
}
});
if(currentPerson.getAdresses() == null) currentPerson.setAdresses(new ArrayList<>());
long addrId = rs.getLong("addrId");
if(!rs.wasNull()) {
currentPerson.getAdresses().add(new Address(addrId, rs.getString("street")));
}
}
return new ArrayList<Person>(peopleById.values());
}
}
}

View File

@@ -19,7 +19,9 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.support.RowMapperResultsetExtractorEither;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
/**
@@ -32,9 +34,9 @@ public class ConfigurableRowMapperMapUnitTests {
@Test
public void freshInstanceReturnsNull() {
RowMapperMap map = new ConfigurableRowMapperMap();
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration();
assertThat(map.rowMapperFor(Object.class)).isNull();
assertThat(map.getMapper(Object.class)).isNull();
}
@Test
@@ -42,9 +44,19 @@ public class ConfigurableRowMapperMapUnitTests {
RowMapper rowMapper = mock(RowMapper.class);
RowMapperMap map = new ConfigurableRowMapperMap().register(Object.class, rowMapper);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration().registerRowMapper(Object.class, rowMapper);
assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper);
assertThat(map.getMapper(Object.class)).isEqualTo(RowMapperResultsetExtractorEither.of(rowMapper));
}
@Test
public void returnsConfiguredInstanceResultSetExtractorForClass() {
ResultSetExtractor resultSetExtractor = mock(ResultSetExtractor.class);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration().registerResultSetExtractor(Object.class, resultSetExtractor);
assertThat(map.getMapper(Object.class)).isEqualTo(RowMapperResultsetExtractorEither.of(resultSetExtractor));
}
@Test
@@ -52,10 +64,21 @@ public class ConfigurableRowMapperMapUnitTests {
RowMapper rowMapper = mock(RowMapper.class);
RowMapperMap map = new ConfigurableRowMapperMap().register(Number.class, rowMapper);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration().registerRowMapper(Number.class, rowMapper);
assertThat(map.rowMapperFor(Integer.class)).isNull();
assertThat(map.rowMapperFor(String.class)).isNull();
assertThat(map.getMapper(Integer.class)).isNull();
assertThat(map.getMapper(String.class)).isNull();
}
@Test
public void returnsNullResultSetExtractorForClassNotConfigured() {
ResultSetExtractor resultSetExtractor = mock(ResultSetExtractor.class);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration().registerResultSetExtractor(Number.class, resultSetExtractor);
assertThat(map.getMapper(Integer.class)).isNull();
assertThat(map.getMapper(String.class)).isNull();
}
@Test
@@ -63,9 +86,19 @@ public class ConfigurableRowMapperMapUnitTests {
RowMapper rowMapper = mock(RowMapper.class);
RowMapperMap map = new ConfigurableRowMapperMap().register(String.class, rowMapper);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration().registerRowMapper(String.class, rowMapper);
assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper);
assertThat(map.getMapper(Object.class)).isEqualTo(RowMapperResultsetExtractorEither.of(rowMapper));
}
@Test
public void returnsInstanceOfResultSetExtractorRegisteredForSubClass() {
ResultSetExtractor resultSetExtractor = mock(ResultSetExtractor.class);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration().registerResultSetExtractor(String.class, resultSetExtractor);
assertThat(map.getMapper(Object.class)).isEqualTo(RowMapperResultsetExtractorEither.of(resultSetExtractor));
}
@Test
@@ -73,12 +106,25 @@ public class ConfigurableRowMapperMapUnitTests {
RowMapper rowMapper = mock(RowMapper.class);
RowMapperMap map = new ConfigurableRowMapperMap() //
.register(Object.class, mock(RowMapper.class)) //
.register(Integer.class, rowMapper) //
.register(Number.class, mock(RowMapper.class));
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration() //
.registerRowMapper(Object.class, mock(RowMapper.class)) //
.registerRowMapper(Integer.class, rowMapper) //
.registerRowMapper(Number.class, mock(RowMapper.class));
assertThat(map.rowMapperFor(Integer.class)).isEqualTo(rowMapper);
assertThat(map.getMapper(Integer.class)).isEqualTo(RowMapperResultsetExtractorEither.of(rowMapper));
}
@Test
public void prefersExactResultSetExtractorTypeMatchClass() {
ResultSetExtractor resultSetExtractor = mock(ResultSetExtractor.class);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration() //
.registerResultSetExtractor(Object.class, mock(ResultSetExtractor.class)) //
.registerResultSetExtractor(Integer.class, resultSetExtractor) //
.registerResultSetExtractor(Number.class, mock(ResultSetExtractor.class));
assertThat(map.getMapper(Integer.class)).isEqualTo(RowMapperResultsetExtractorEither.of(resultSetExtractor));
}
@Test
@@ -86,10 +132,22 @@ public class ConfigurableRowMapperMapUnitTests {
RowMapper rowMapper = mock(RowMapper.class);
RowMapperMap map = new ConfigurableRowMapperMap() //
.register(Integer.class, mock(RowMapper.class)) //
.register(Number.class, rowMapper);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration() //
.registerRowMapper(Integer.class, mock(RowMapper.class)) //
.registerRowMapper(Number.class, rowMapper);
assertThat(map.rowMapperFor(Object.class)).isEqualTo(rowMapper);
assertThat(map.getMapper(Object.class)).isEqualTo(RowMapperResultsetExtractorEither.of(rowMapper));
}
@Test
public void prefersLatestRegistrationOfResultSetExtractorForSuperTypeMatch() {
ResultSetExtractor resultSetExtractor = mock(ResultSetExtractor.class);
QueryMappingConfiguration map = new DefaultQueryMappingConfiguration() //
.registerResultSetExtractor(Integer.class, mock(ResultSetExtractor.class)) //
.registerResultSetExtractor(Number.class, resultSetExtractor);
assertThat(map.getMapper(Object.class)).isEqualTo(RowMapperResultsetExtractorEither.of(resultSetExtractor));
}
}

View File

@@ -30,10 +30,12 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositoriesIntegrationTests.TestConfiguration;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean;
import org.springframework.data.jdbc.support.RowMapperResultsetExtractorEither;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -49,16 +51,17 @@ import org.springframework.util.ReflectionUtils;
@ContextConfiguration(classes = TestConfiguration.class)
public class EnableJdbcRepositoriesIntegrationTests {
static final Field ROW_MAPPER_MAP = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "rowMapperMap");
static final Field MAPPER_MAP = ReflectionUtils.findField(JdbcRepositoryFactoryBean.class, "mapperMap");
public static final RowMapper DUMMY_ENTITY_ROW_MAPPER = mock(RowMapper.class);
public static final RowMapper STRING_ROW_MAPPER = mock(RowMapper.class);
public static final ResultSetExtractor<Integer> INTEGER_RESULT_SET_EXTRACTOR = mock(ResultSetExtractor.class);
@Autowired JdbcRepositoryFactoryBean factoryBean;
@Autowired DummyRepository repository;
@BeforeClass
public static void setup() {
ROW_MAPPER_MAP.setAccessible(true);
MAPPER_MAP.setAccessible(true);
}
@Test // DATAJDBC-100
@@ -71,13 +74,25 @@ public class EnableJdbcRepositoriesIntegrationTests {
assertThat(all).isNotNull();
}
@Test // DATAJDBC-290
public void customResultSetExtractorConfigurationGetsPickedUp() {
QueryMappingConfiguration mapping = (QueryMappingConfiguration) ReflectionUtils.getField(MAPPER_MAP, factoryBean);
assertThat(mapping.getMapper(Integer.class)).isEqualTo(RowMapperResultsetExtractorEither.of(INTEGER_RESULT_SET_EXTRACTOR));
}
@Test // DATAJDBC-290
public void customResultSetExtractorConfigurationIsNotPickedUpIfRowMapperIsRegisteredForTheSameType() {
QueryMappingConfiguration mapping = (QueryMappingConfiguration) ReflectionUtils.getField(MAPPER_MAP, factoryBean);
assertThat(mapping.getMapper(String.class).isResultSetExtractor()).isFalse();
}
@Test // DATAJDBC-166
public void customRowMapperConfigurationGetsPickedUp() {
RowMapperMap mapping = (RowMapperMap) ReflectionUtils.getField(ROW_MAPPER_MAP, factoryBean);
QueryMappingConfiguration mapping = (QueryMappingConfiguration) ReflectionUtils.getField(MAPPER_MAP, factoryBean);
assertThat(mapping.rowMapperFor(String.class)).isEqualTo(STRING_ROW_MAPPER);
assertThat(mapping.rowMapperFor(DummyEntity.class)).isEqualTo(DUMMY_ENTITY_ROW_MAPPER);
assertThat(mapping.getMapper(String.class)).isEqualTo(RowMapperResultsetExtractorEither.of(STRING_ROW_MAPPER));
assertThat(mapping.getMapper(DummyEntity.class)).isEqualTo(RowMapperResultsetExtractorEither.of(DUMMY_ENTITY_ROW_MAPPER));
}
interface DummyRepository extends CrudRepository<DummyEntity, Long> {
@@ -100,10 +115,11 @@ public class EnableJdbcRepositoriesIntegrationTests {
}
@Bean
RowMapperMap rowMappers() {
return new ConfigurableRowMapperMap() //
.register(DummyEntity.class, DUMMY_ENTITY_ROW_MAPPER) //
.register(String.class, STRING_ROW_MAPPER);
QueryMappingConfiguration rowMappers() {
return new DefaultQueryMappingConfiguration() //
.registerRowMapper(DummyEntity.class, DUMMY_ENTITY_ROW_MAPPER) //
.registerRowMapper(String.class, STRING_ROW_MAPPER)
.registerResultSetExtractor(Integer.class, INTEGER_RESULT_SET_EXTRACTOR);
}
}

View File

@@ -27,8 +27,8 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.jdbc.repository.config.ConfigurableRowMapperMap;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
@@ -37,6 +37,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
@@ -74,7 +75,7 @@ public class JdbcQueryLookupStrategyUnitTests {
public void typeBasedRowMapperGetsUsedForQuery() {
RowMapper<? extends NumberFormat> numberFormatMapper = mock(RowMapper.class);
RowMapperMap rowMapperMap = new ConfigurableRowMapperMap().register(NumberFormat.class, numberFormatMapper);
QueryMappingConfiguration rowMapperMap = new DefaultQueryMappingConfiguration().registerRowMapper(NumberFormat.class, numberFormatMapper);
RepositoryQuery repositoryQuery = getRepositoryQuery("returningNumberFormat", rowMapperMap);
@@ -82,8 +83,22 @@ public class JdbcQueryLookupStrategyUnitTests {
verify(operations).queryForObject(anyString(), any(SqlParameterSource.class), eq(numberFormatMapper));
}
@Test // DATAJDBC-290
@SuppressWarnings("unchecked")
public void typeBasedResultSetExtractorGetsUsedForQuery() {
private RepositoryQuery getRepositoryQuery(String name, RowMapperMap rowMapperMap) {
ResultSetExtractor<? extends NumberFormat> numberFormatMapper = mock(ResultSetExtractor.class);
QueryMappingConfiguration rowMapperMap = new DefaultQueryMappingConfiguration().registerResultSetExtractor(NumberFormat.class, numberFormatMapper);
RepositoryQuery repositoryQuery = getRepositoryQuery("returningNumberFormat", rowMapperMap);
repositoryQuery.execute(new Object[] {});
verify(operations).query(anyString(), any(SqlParameterSource.class), eq(numberFormatMapper));
}
private RepositoryQuery getRepositoryQuery(String name, QueryMappingConfiguration rowMapperMap) {
JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(publisher, mappingContext, converter, accessStrategy,
rowMapperMap, operations);

View File

@@ -29,7 +29,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.DataAccessStrategy;
import org.springframework.data.jdbc.core.DefaultDataAccessStrategy;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.repository.RowMapperMap;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.repository.CrudRepository;
@@ -100,7 +100,7 @@ public class JdbcRepositoryFactoryBeanUnitTests {
assertThat(factoryBean.getObject()).isNotNull();
assertThat(ReflectionTestUtils.getField(factoryBean, "dataAccessStrategy"))
.isInstanceOf(DefaultDataAccessStrategy.class);
assertThat(ReflectionTestUtils.getField(factoryBean, "rowMapperMap")).isEqualTo(RowMapperMap.EMPTY);
assertThat(ReflectionTestUtils.getField(factoryBean, "mapperMap")).isEqualTo(QueryMappingConfiguration.EMPTY);
}
private static class DummyEntity {

View File

@@ -22,16 +22,20 @@ import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DataAccessException;
import org.springframework.data.jdbc.support.RowMapperResultsetExtractorEither;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
import org.springframework.data.repository.query.DefaultParameters;
import org.springframework.data.repository.query.Parameters;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
@@ -48,6 +52,7 @@ public class JdbcRepositoryQueryUnitTests {
JdbcQueryMethod queryMethod;
RowMapper<?> defaultRowMapper;
ResultSetExtractor<?> defaultResultSetExtractor;
JdbcRepositoryQuery query;
NamedParameterJdbcOperations operations;
ApplicationEventPublisher publisher;
@@ -67,7 +72,7 @@ public class JdbcRepositoryQueryUnitTests {
this.publisher = mock(ApplicationEventPublisher.class);
this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS);
this.query = new JdbcRepositoryQuery(publisher, context, queryMethod, operations, defaultRowMapper);
this.query = new JdbcRepositoryQuery(publisher, context, queryMethod, operations, RowMapperResultsetExtractorEither.of(defaultRowMapper));
}
@Test // DATAJDBC-165
@@ -106,11 +111,24 @@ public class JdbcRepositoryQueryUnitTests {
doReturn("some sql statement").when(queryMethod).getAnnotatedQuery();
doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass();
new JdbcRepositoryQuery(publisher, context, queryMethod, operations, defaultRowMapper).execute(new Object[] {});
new JdbcRepositoryQuery(publisher, context, queryMethod, operations, RowMapperResultsetExtractorEither.of(defaultRowMapper)).execute(new Object[] {});
verify(operations) //
.queryForObject(anyString(), any(SqlParameterSource.class), isA(CustomRowMapper.class));
}
@Test // DATAJDBC-290
public void customResultSetExtractorIsUsedWhenSpecified() {
doReturn("some sql statement").when(queryMethod).getAnnotatedQuery();
doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass();
new JdbcRepositoryQuery(publisher, context, queryMethod, operations, RowMapperResultsetExtractorEither.of(defaultRowMapper)).execute(new Object[] {});
verify(operations) //
.query(anyString(), any(SqlParameterSource.class), isA(CustomResultSetExtractor.class));
}
@Test // DATAJDBC-263
public void publishesSingleEventWhenQueryReturnsSingleAggregate() {
@@ -121,7 +139,7 @@ public class JdbcRepositoryQueryUnitTests {
doReturn(true).when(context).hasPersistentEntityFor(DummyEntity.class);
when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getRequiredIdentifier()).thenReturn("some identifier");
new JdbcRepositoryQuery(publisher, context, queryMethod, operations, defaultRowMapper).execute(new Object[] {});
new JdbcRepositoryQuery(publisher, context, queryMethod, operations, RowMapperResultsetExtractorEither.of(defaultRowMapper)).execute(new Object[] {});
verify(publisher).publishEvent(any(AfterLoadEvent.class));
}
@@ -135,7 +153,7 @@ public class JdbcRepositoryQueryUnitTests {
doReturn(true).when(context).hasPersistentEntityFor(DummyEntity.class);
when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getRequiredIdentifier()).thenReturn("some identifier");
new JdbcRepositoryQuery(publisher, context, queryMethod, operations, defaultRowMapper).execute(new Object[] {});
new JdbcRepositoryQuery(publisher, context, queryMethod, operations, RowMapperResultsetExtractorEither.of(defaultRowMapper)).execute(new Object[] {});
verify(publisher, times(2)).publishEvent(any(AfterLoadEvent.class));
}
@@ -153,6 +171,14 @@ public class JdbcRepositoryQueryUnitTests {
return null;
}
}
private static class CustomResultSetExtractor implements ResultSetExtractor<Object> {
@Override
public Object extractData(ResultSet rs) throws SQLException, DataAccessException {
return null;
}
}
private static class DummyEntity {
private Long id;

View File

@@ -0,0 +1 @@
CREATE TABLE car ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, model VARCHAR(100));

View File

@@ -0,0 +1 @@
CREATE TABLE car ( id INT NOT NULL AUTO_INCREMENT, model VARCHAR(100), PRIMARY KEY (id));

View File

@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS car;
CREATE TABLE car ( id int IDENTITY(1,1) PRIMARY KEY, model VARCHAR(100));

View File

@@ -0,0 +1 @@
CREATE TABLE car ( id INT NOT NULL AUTO_INCREMENT, model VARCHAR(100), PRIMARY KEY (id));

View File

@@ -0,0 +1,2 @@
DROP TABLE car;
CREATE TABLE car ( id SERIAL PRIMARY KEY, model VARCHAR(100));

View File

@@ -0,0 +1,3 @@
CREATE TABLE person ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, name VARCHAR(100));
CREATE TABLE address ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, street VARCHAR(100), person_id BIGINT);
ALTER TABLE address ADD FOREIGN KEY (person_id) REFERENCES person(id);

View File

@@ -0,0 +1,3 @@
CREATE TABLE person ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100), PRIMARY KEY (id));
CREATE TABLE address ( id INT NOT NULL AUTO_INCREMENT, street VARCHAR(100), person_id INT, PRIMARY KEY (id));
ALTER TABLE address ADD FOREIGN KEY (person_id) REFERENCES person(id);

View File

@@ -0,0 +1,5 @@
DROP TABLE IF EXISTS person;
DROP TABLE IF EXISTS address;
CREATE TABLE person ( id int IDENTITY(1,1) PRIMARY KEY, name VARCHAR(100));
CREATE TABLE address ( id int IDENTITY(1,1) PRIMARY KEY, street VARCHAR(100), person_id INT);
ALTER TABLE address ADD FOREIGN KEY (person_id) REFERENCES person(id);

View File

@@ -0,0 +1,3 @@
CREATE TABLE person ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100), PRIMARY KEY (id));
CREATE TABLE address ( id INT NOT NULL AUTO_INCREMENT, street VARCHAR(100), person_id INT, PRIMARY KEY (id));
ALTER TABLE address ADD FOREIGN KEY (person_id) REFERENCES person(id);

View File

@@ -0,0 +1,5 @@
DROP TABLE person;
DROP TABLE address;
CREATE TABLE person ( id SERIAL PRIMARY KEY, name VARCHAR(100));
CREATE TABLE address ( id SERIAL PRIMARY KEY, street VARCHAR(100), person_id INT);
ALTER TABLE address ADD FOREIGN KEY (person_id) REFERENCES person(id);