DATAJDBC-318 - Initial support for query derivation.

Move JdbcRepositoryQuery into repository.query package. Split JdbcRepositoryQuery into AbstractJdbcQuery and StringBasedJdbcQuery.
Add QueryMapper for mapping of Criteria.
Initial support for query derivation.

Emit events and issue entity callbacks only for default RowMapper.
Custom RowMapper/ResultSetExtractor are in full control of the mapping and can issue events/callbacks themselves.

Update reference documentation.

Original pull request: #209.
This commit is contained in:
Mark Paluch
2020-04-20 14:01:28 +02:00
committed by Jens Schauder
parent 2f3f00bd71
commit 999bf29321
40 changed files with 3263 additions and 839 deletions

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import java.util.List;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.RowMapperResultSetExtractor;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A query to be executed based on a repository method, it's annotated SQL query and the arguments provided to the
* method.
*
* @author Jens Schauder
* @author Kazuki Shimizu
* @author Oliver Gierke
* @author Maciej Walkowiak
* @author Mark Paluch
* @since 2.0
*/
public abstract class AbstractJdbcQuery implements RepositoryQuery {
private final JdbcQueryMethod queryMethod;
private final NamedParameterJdbcOperations operations;
/**
* Creates a new {@link AbstractJdbcQuery} for the given {@link JdbcQueryMethod}, {@link NamedParameterJdbcOperations}
* and {@link RowMapper}.
*
* @param queryMethod must not be {@literal null}.
* @param operations must not be {@literal null}.
* @param defaultRowMapper can be {@literal null} (only in case of a modifying query).
*/
AbstractJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
@Nullable RowMapper<?> defaultRowMapper) {
Assert.notNull(queryMethod, "Query method must not be null!");
Assert.notNull(operations, "NamedParameterJdbcOperations must not be null!");
if (!queryMethod.isModifyingQuery()) {
Assert.notNull(defaultRowMapper, "Mapper must not be null!");
}
this.queryMethod = queryMethod;
this.operations = operations;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
*/
@Override
public JdbcQueryMethod getQueryMethod() {
return queryMethod;
}
/**
* Creates a {@link JdbcQueryExecution} given {@link JdbcQueryMethod}, {@link ResultSetExtractor} an
* {@link RowMapper}. Prefers the given {@link ResultSetExtractor} over {@link RowMapper}.
*
* @param queryMethod must not be {@literal null}.
* @param extractor must not be {@literal null}.
* @param rowMapper must not be {@literal null}.
* @return
*/
protected JdbcQueryExecution<?> getQueryExecution(JdbcQueryMethod queryMethod,
@Nullable ResultSetExtractor<Object> extractor, RowMapper<Object> rowMapper) {
if (queryMethod.isModifyingQuery()) {
return createModifyingQueryExecutor();
}
if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) {
return extractor != null ? getQueryExecution(extractor) : collectionQuery(rowMapper);
}
return extractor != null ? getQueryExecution(extractor) : singleObjectQuery(rowMapper);
}
private JdbcQueryExecution<Object> createModifyingQueryExecutor() {
return (query, parameters) -> {
int updatedCount = operations.update(query, parameters);
Class<?> returnedObjectType = queryMethod.getReturnedObjectType();
return (returnedObjectType == boolean.class || returnedObjectType == Boolean.class) ? updatedCount != 0
: updatedCount;
};
}
private JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {
return (query, parameters) -> {
try {
return operations.queryForObject(query, parameters, rowMapper);
} catch (EmptyResultDataAccessException e) {
return null;
}
};
}
private <T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> rowMapper) {
return getQueryExecution(new RowMapperResultSetExtractor<>(rowMapper));
}
private <T> JdbcQueryExecution<T> getQueryExecution(ResultSetExtractor<T> resultSetExtractor) {
return (query, parameters) -> operations.query(query, parameters, resultSetExtractor);
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.dialect.RenderContextFactory;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.SelectBuilder;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.render.SqlRenderer;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.relational.repository.query.RelationalQueryCreator;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.util.Assert;
/**
* Implementation of {@link RelationalQueryCreator} that creates {@link ParametrizedQuery} from a {@link PartTree}.
*
* @author Mark Paluch
* @since 2.0
*/
class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
private final PartTree tree;
private final RelationalParameterAccessor accessor;
private final QueryMapper queryMapper;
private final MappingContext<RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
private final RelationalEntityMetadata<?> entityMetadata;
private final RenderContextFactory renderContextFactory;
/**
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
* {@link RelationalEntityMetadata} and {@link RelationalParameterAccessor}.
*
* @param tree part tree, must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param dialect must not be {@literal null}.
* @param entityMetadata relational entity metadata, must not be {@literal null}.
* @param accessor parameter metadata provider, must not be {@literal null}.
*/
public JdbcQueryCreator(PartTree tree, JdbcConverter converter, Dialect dialect,
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor) {
super(tree, accessor);
Assert.notNull(converter, "JdbcConverter must not be null");
Assert.notNull(dialect, "Dialect must not be null");
Assert.notNull(entityMetadata, "Relational entity metadata must not be null");
this.tree = tree;
this.accessor = accessor;
this.mappingContext = (MappingContext) converter.getMappingContext();
this.entityMetadata = entityMetadata;
this.queryMapper = new QueryMapper(dialect, converter);
this.renderContextFactory = new RenderContextFactory(dialect);
}
/**
* Creates {@link ParametrizedQuery} applying the given {@link Criteria} and {@link Sort} definition.
*
* @param criteria {@link Criteria} to be applied to query
* @param sort sort option to be applied to query, must not be {@literal null}.
* @return instance of {@link ParametrizedQuery}
*/
@Override
protected ParametrizedQuery complete(Criteria criteria, Sort sort) {
RelationalPersistentEntity<?> entity = entityMetadata.getTableEntity();
Table table = Table.create(entityMetadata.getTableName());
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
SelectBuilder.SelectFromAndJoin builder = Select.builder().select(table.columns(getSelectProjection())).from(table);
if (tree.isExistsProjection()) {
builder = builder.limit(1);
} else if (tree.isLimiting()) {
builder = builder.limit(tree.getMaxResults());
}
Pageable pageable = accessor.getPageable();
if (pageable.isPaged()) {
builder = builder.limit(pageable.getPageSize()).offset(pageable.getOffset());
}
if (criteria != null) {
builder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity));
}
if (sort.isSorted()) {
builder.orderBy(queryMapper.getMappedSort(table, sort, entity));
}
Select select = builder.build();
String sql = SqlRenderer.create(renderContextFactory.createRenderContext()).render(select);
return new ParametrizedQuery(sql, parameterSource);
}
private SqlIdentifier[] getSelectProjection() {
RelationalPersistentEntity<?> tableEntity = entityMetadata.getTableEntity();
if (tree.isExistsProjection()) {
return new SqlIdentifier[] { tableEntity.getIdColumn() };
}
Collection<SqlIdentifier> columnNames = unwrapColumnNames("", tableEntity);
return columnNames.toArray(new SqlIdentifier[0]);
}
private Collection<SqlIdentifier> unwrapColumnNames(String prefix, RelationalPersistentEntity<?> persistentEntity) {
Collection<SqlIdentifier> columnNames = new ArrayList<>();
for (RelationalPersistentProperty property : persistentEntity) {
if (property.isEmbedded()) {
columnNames.addAll(
unwrapColumnNames(prefix + property.getEmbeddedPrefix(), mappingContext.getPersistentEntity(property)));
}
else {
columnNames.add(property.getColumnName().transform(prefix::concat));
}
}
return columnNames;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.lang.Nullable;
/**
* Interface specifying a result execution strategy.
*
* @author Mark Paluch
* @since 2.0
*/
@FunctionalInterface
interface JdbcQueryExecution<T> {
/**
* Execute the given {@code query}.
*
* @param query
* @param parameter
* @return
*/
@Nullable
T execute(String query, SqlParameterSource parameter);
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Optional;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
import org.springframework.data.relational.repository.query.RelationalParameters;
import org.springframework.data.relational.repository.query.SimpleRelationalEntityMetadata;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;
/**
* {@link QueryMethod} implementation that implements a method by executing the query from a {@link Query} annotation on
* that method. Binds method arguments to named parameters in the SQL statement.
*
* @author Jens Schauder
* @author Kazuki Shimizu
* @author Moises Cisneros
*/
public class JdbcQueryMethod extends QueryMethod {
private final Method method;
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
private final Map<Class<? extends Annotation>, Optional<Annotation>> annotationCache;
private final NamedQueries namedQueries;
private @Nullable RelationalEntityMetadata<?> metadata;
// TODO: Remove NamedQueries and put it into JdbcQueryLookupStrategy
public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries,
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext) {
super(method, metadata, factory);
this.namedQueries = namedQueries;
this.method = method;
this.mappingContext = mappingContext;
this.annotationCache = new ConcurrentReferenceHashMap<>();
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method)
*/
@Override
protected RelationalParameters createParameters(Method method) {
return new RelationalParameters(method);
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#getEntityInformation()
*/
@Override
@SuppressWarnings("unchecked")
public RelationalEntityMetadata<?> getEntityInformation() {
if (metadata == null) {
Class<?> returnedObjectType = getReturnedObjectType();
Class<?> domainClass = getDomainClass();
if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType)) {
this.metadata = new SimpleRelationalEntityMetadata<>((Class<Object>) domainClass,
mappingContext.getRequiredPersistentEntity(domainClass));
} else {
RelationalPersistentEntity<?> returnedEntity = mappingContext.getPersistentEntity(returnedObjectType);
RelationalPersistentEntity<?> managedEntity = mappingContext.getRequiredPersistentEntity(domainClass);
returnedEntity = returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity
: returnedEntity;
RelationalPersistentEntity<?> tableEntity = domainClass.isAssignableFrom(returnedObjectType) ? returnedEntity
: managedEntity;
this.metadata = new SimpleRelationalEntityMetadata<>((Class<Object>) returnedEntity.getType(), tableEntity);
}
}
return this.metadata;
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.QueryMethod#getParameters()
*/
@Override
public RelationalParameters getParameters() {
return (RelationalParameters) super.getParameters();
}
/**
* Returns the annotated query if it exists.
*
* @return May be {@code null}.
*/
@Nullable
String getDeclaredQuery() {
String annotatedValue = getQueryValue();
return StringUtils.hasText(annotatedValue) ? annotatedValue : getNamedQuery();
}
/**
* Returns the annotated query if it exists.
*
* @return May be {@code null}.
*/
@Nullable
private String getQueryValue() {
return getMergedAnnotationAttribute("value");
}
/**
* Returns the named query for this method if it exists.
*
* @return May be {@code null}.
*/
@Nullable
private String getNamedQuery() {
String name = getQueryName();
return this.namedQueries.hasQuery(name) ? this.namedQueries.getQuery(name) : null;
}
/**
* Returns the annotated query name.
*
* @return May be {@code null}.
*/
private String getQueryName() {
String annotatedName = getMergedAnnotationAttribute("name");
return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName();
}
/*
* Returns the class to be used as {@link org.springframework.jdbc.core.RowMapper}
*
* @return May be {@code null}.
*/
@Nullable
Class<? extends RowMapper> getRowMapperClass() {
return getMergedAnnotationAttribute("rowMapperClass");
}
/**
* Returns the class to be used as {@link org.springframework.jdbc.core.ResultSetExtractor}
*
* @return May be {@code null}.
*/
@Nullable
Class<? extends ResultSetExtractor> getResultSetExtractorClass() {
return getMergedAnnotationAttribute("resultSetExtractorClass");
}
/**
* Returns whether the query method is a modifying one.
*
* @return if it's a modifying query, return {@code true}.
*/
@Override
public boolean isModifyingQuery() {
return AnnotationUtils.findAnnotation(method, Modifying.class) != null;
}
@SuppressWarnings("unchecked")
@Nullable
private <T> T getMergedAnnotationAttribute(String attribute) {
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
return (T) AnnotationUtils.getValue(queryAnnotation, attribute);
}
/**
* Returns whether the method has an annotated query.
*
* @return
*/
public boolean hasAnnotatedQuery() {
return findAnnotatedQuery().isPresent();
}
/**
* Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found
* nor the attribute was specified.
*
* @return
*/
@Nullable
String getAnnotatedQuery() {
return findAnnotatedQuery().orElse(null);
}
private Optional<String> findAnnotatedQuery() {
return lookupQueryAnnotation() //
.map(Query::value) //
.filter(StringUtils::hasText);
}
Optional<Query> lookupQueryAnnotation() {
return doFindAnnotation(Query.class);
}
@SuppressWarnings("unchecked")
private <A extends Annotation> Optional<A> doFindAnnotation(Class<A> annotationType) {
return (Optional<A>) this.annotationCache.computeIfAbsent(annotationType,
it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it)));
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
/**
* Value object encapsulating a parametrized query containing named parameters and {@link SqlParameterSource}.
*
* @author Mark Paluch
* @since 2.0
*/
class ParametrizedQuery {
private final String query;
private final SqlParameterSource parameterSource;
public ParametrizedQuery(String query, SqlParameterSource parameterSource) {
this.query = query;
this.parameterSource = parameterSource;
}
public String getQuery() {
return query;
}
public SqlParameterSource getParameterSource() {
return parameterSource;
}
@Override
public String toString() {
return this.query;
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.util.Assert;
/**
* An {@link AbstractJdbcQuery} implementation based on a {@link PartTree}.
*
* @author Mark Paluch
* @since 2.0
*/
public class PartTreeJdbcQuery extends AbstractJdbcQuery {
private final Parameters<?, ?> parameters;
private final Dialect dialect;
private final JdbcConverter converter;
private final PartTree tree;
private final JdbcQueryExecution<?> execution;
/**
* Creates a new {@link PartTreeJdbcQuery}.
*
* @param queryMethod must not be {@literal null}.
* @param dialect must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param operations must not be {@literal null}.
* @param rowMapper must not be {@literal null}.
*/
public PartTreeJdbcQuery(JdbcQueryMethod queryMethod, Dialect dialect, JdbcConverter converter,
NamedParameterJdbcOperations operations, RowMapper<Object> rowMapper) {
super(queryMethod, operations, rowMapper);
Assert.notNull(queryMethod, "JdbcQueryMethod must not be null");
Assert.notNull(dialect, "Dialect must not be null");
Assert.notNull(converter, "JdbcConverter must not be null");
this.parameters = queryMethod.getParameters();
this.dialect = dialect;
this.converter = converter;
try {
this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType());
JdbcQueryCreator.validate(this.tree, this.parameters);
} catch (RuntimeException e) {
throw new IllegalArgumentException(
String.format("Failed to create query for method %s! %s", queryMethod, e.getMessage()), e);
}
this.execution = getQueryExecution(queryMethod, null, rowMapper);
}
private Sort getDynamicSort(RelationalParameterAccessor accessor) {
return parameters.potentiallySortsDynamically() ? accessor.getSort() : Sort.unsorted();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
*/
@Override
public Object execute(Object[] values) {
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
values);
ParametrizedQuery query = createQuery(accessor);
return this.execution.execute(query.getQuery(), query.getParameterSource());
}
protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor) {
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
JdbcQueryCreator queryCreator = new JdbcQueryCreator(tree, converter, dialect, entityMetadata, accessor);
return queryCreator.createQuery(getDynamicSort(accessor));
}
}

View File

@@ -0,0 +1,748 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcValue;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.dialect.Escaper;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.query.CriteriaDefinition;
import org.springframework.data.relational.core.query.CriteriaDefinition.Comparator;
import org.springframework.data.relational.core.query.ValueFunction;
import org.springframework.data.relational.core.sql.*;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Pair;
import org.springframework.data.util.TypeInformation;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Maps {@link CriteriaDefinition} and {@link Sort} objects considering mapping metadata and dialect-specific
* conversion.
*
* @author Mark Paluch
* @since 2.0
*/
class QueryMapper {
private final JdbcConverter converter;
private final Dialect dialect;
private final MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
/**
* Creates a new {@link QueryMapper} with the given {@link JdbcConverter}.
*
* @param dialect must not be {@literal null}.
* @param converter must not be {@literal null}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public QueryMapper(Dialect dialect, JdbcConverter converter) {
Assert.notNull(dialect, "Dialect must not be null!");
Assert.notNull(converter, "JdbcConverter must not be null!");
this.converter = converter;
this.dialect = dialect;
this.mappingContext = (MappingContext) converter.getMappingContext();
}
/**
* Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}.
*
* @param sort must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return
*/
public List<OrderByField> getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity<?> entity) {
List<OrderByField> mappedOrder = new ArrayList<>();
for (Sort.Order order : sort) {
Field field = createPropertyField(entity, SqlIdentifier.unquoted(order.getProperty()), this.mappingContext);
OrderByField orderBy = OrderByField.from(table.column(field.getMappedColumnName()))
.withNullHandling(order.getNullHandling());
mappedOrder.add(order.isAscending() ? orderBy.asc() : orderBy.desc());
}
return mappedOrder;
}
/**
* Map the {@link Expression} object to apply field name mapping using {@link Class the type to read}.
*
* @param expression must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return the mapped {@link Expression}.
*/
public Expression getMappedObject(Expression expression, @Nullable RelationalPersistentEntity<?> entity) {
if (entity == null || expression instanceof AsteriskFromTable) {
return expression;
}
if (expression instanceof Column) {
Column column = (Column) expression;
Field field = createPropertyField(entity, column.getName());
Table table = column.getTable();
Column columnFromTable = table.column(field.getMappedColumnName());
return column instanceof Aliased ? columnFromTable.as(((Aliased) column).getAlias()) : columnFromTable;
}
if (expression instanceof SimpleFunction) {
SimpleFunction function = (SimpleFunction) expression;
List<Expression> arguments = function.getExpressions();
List<Expression> mappedArguments = new ArrayList<>(arguments.size());
for (Expression argument : arguments) {
mappedArguments.add(getMappedObject(argument, entity));
}
SimpleFunction mappedFunction = SimpleFunction.create(function.getFunctionName(), mappedArguments);
return function instanceof Aliased ? mappedFunction.as(((Aliased) function).getAlias()) : mappedFunction;
}
throw new IllegalArgumentException(String.format("Cannot map %s", expression));
}
/**
* Map a {@link CriteriaDefinition} object into {@link Condition} and consider value/{@code NULL} {@link Bindings}.
*
* @param parameterSource bind parameterSource object, must not be {@literal null}.
* @param criteria criteria definition to map, must not be {@literal null}.
* @param table must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return the mapped {@link Condition}.
*/
public Condition getMappedObject(MapSqlParameterSource parameterSource, CriteriaDefinition criteria, Table table,
@Nullable RelationalPersistentEntity<?> entity) {
Assert.notNull(parameterSource, "MapSqlParameterSource must not be null!");
Assert.notNull(criteria, "CriteriaDefinition must not be null!");
Assert.notNull(table, "Table must not be null!");
if (criteria.isEmpty()) {
throw new IllegalArgumentException("Cannot map empty Criteria");
}
return unroll(criteria, table, entity, parameterSource);
}
private Condition unroll(CriteriaDefinition criteria, Table table, @Nullable RelationalPersistentEntity<?> entity,
MapSqlParameterSource parameterSource) {
CriteriaDefinition current = criteria;
// reverse unroll criteria chain
Map<CriteriaDefinition, CriteriaDefinition> forwardChain = new HashMap<>();
while (current.hasPrevious()) {
forwardChain.put(current.getPrevious(), current);
current = current.getPrevious();
}
// perform the actual mapping
Condition mapped = getCondition(current, parameterSource, table, entity);
while (forwardChain.containsKey(current)) {
CriteriaDefinition criterion = forwardChain.get(current);
Condition result = null;
Condition condition = getCondition(criterion, parameterSource, table, entity);
if (condition != null) {
result = combine(criterion, mapped, criterion.getCombinator(), condition);
}
if (result != null) {
mapped = result;
}
current = criterion;
}
if (mapped == null) {
throw new IllegalStateException("Cannot map empty Criteria");
}
return mapped;
}
@Nullable
private Condition unrollGroup(List<? extends CriteriaDefinition> criteria, Table table,
CriteriaDefinition.Combinator combinator, @Nullable RelationalPersistentEntity<?> entity,
MapSqlParameterSource parameterSource) {
Condition mapped = null;
for (CriteriaDefinition criterion : criteria) {
if (criterion.isEmpty()) {
continue;
}
Condition condition = unroll(criterion, table, entity, parameterSource);
mapped = combine(criterion, mapped, combinator, condition);
}
return mapped;
}
@Nullable
private Condition getCondition(CriteriaDefinition criteria, MapSqlParameterSource parameterSource, Table table,
@Nullable RelationalPersistentEntity<?> entity) {
if (criteria.isEmpty()) {
return null;
}
if (criteria.isGroup()) {
Condition condition = unrollGroup(criteria.getGroup(), table, criteria.getCombinator(), entity, parameterSource);
return condition == null ? null : Conditions.nest(condition);
}
return mapCondition(criteria, parameterSource, table, entity);
}
private Condition combine(CriteriaDefinition criteria, @Nullable Condition currentCondition,
CriteriaDefinition.Combinator combinator, Condition nextCondition) {
if (currentCondition == null) {
currentCondition = nextCondition;
} else if (combinator == CriteriaDefinition.Combinator.AND) {
currentCondition = currentCondition.and(nextCondition);
} else if (combinator == CriteriaDefinition.Combinator.OR) {
currentCondition = currentCondition.or(nextCondition);
} else {
throw new IllegalStateException("Combinator " + criteria.getCombinator() + " not supported");
}
return currentCondition;
}
private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSource parameterSource, Table table,
@Nullable RelationalPersistentEntity<?> entity) {
Field propertyField = createPropertyField(entity, criteria.getColumn(), this.mappingContext);
// Single embedded entity
if (propertyField.isEmbedded()) {
return mapEmbeddedObjectCondition(criteria, parameterSource, table,
((MetadataBackedField) propertyField).getPath().getLeafProperty());
}
TypeInformation<?> actualType = propertyField.getTypeHint().getRequiredActualType();
Column column = table.column(propertyField.getMappedColumnName());
Object mappedValue;
int sqlType;
if (criteria.getValue() instanceof JdbcValue) {
JdbcValue settableValue = (JdbcValue) criteria.getValue();
mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint());
sqlType = getTypeHint(mappedValue, actualType.getType(), settableValue);
} else if (criteria.getValue() instanceof ValueFunction) {
ValueFunction<Object> valueFunction = (ValueFunction<Object>) criteria.getValue();
Object value = valueFunction.apply(getEscaper(criteria.getComparator()));
mappedValue = convertValue(value, propertyField.getTypeHint());
sqlType = propertyField.getSqlType();
} else {
mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint());
sqlType = propertyField.getSqlType();
}
return createCondition(column, mappedValue, sqlType, parameterSource, criteria.getComparator(),
criteria.isIgnoreCase());
}
private Condition mapEmbeddedObjectCondition(CriteriaDefinition criteria, MapSqlParameterSource parameterSource,
Table table, RelationalPersistentProperty embeddedProperty) {
RelationalPersistentEntity<?> persistentEntity = this.mappingContext.getRequiredPersistentEntity(embeddedProperty);
Assert.isInstanceOf(persistentEntity.getType(), criteria.getValue(),
() -> "Value must be of type " + persistentEntity.getType().getName() + " for embedded entity matching");
PersistentPropertyAccessor<Object> embeddedAccessor = persistentEntity.getPropertyAccessor(criteria.getValue());
String prefix = embeddedProperty.getEmbeddedPrefix();
Condition condition = null;
for (RelationalPersistentProperty nestedProperty : persistentEntity) {
SqlIdentifier sqlIdentifier = nestedProperty.getColumnName().transform(prefix::concat);
Object mappedNestedValue = convertValue(embeddedAccessor.getProperty(nestedProperty),
nestedProperty.getTypeInformation());
int sqlType = converter.getSqlType(nestedProperty);
Condition mappedCondition = createCondition(table.column(sqlIdentifier), mappedNestedValue, sqlType,
parameterSource, criteria.getComparator(), criteria.isIgnoreCase());
if (condition != null) {
condition = condition.and(mappedCondition);
} else {
condition = mappedCondition;
}
}
return Conditions.nest(condition);
}
private Escaper getEscaper(Comparator comparator) {
if (comparator == Comparator.LIKE || comparator == Comparator.NOT_LIKE) {
return dialect.getLikeEscaper();
}
return Escaper.DEFAULT;
}
@Nullable
protected Object convertValue(@Nullable Object value, TypeInformation<?> typeInformation) {
if (value == null) {
return null;
}
if (value instanceof Pair) {
Pair<Object, Object> pair = (Pair<Object, Object>) value;
Object first = convertValue(pair.getFirst(),
typeInformation.getActualType() != null ? typeInformation.getRequiredActualType()
: ClassTypeInformation.OBJECT);
Object second = convertValue(pair.getSecond(),
typeInformation.getActualType() != null ? typeInformation.getRequiredActualType()
: ClassTypeInformation.OBJECT);
return Pair.of(first, second);
}
if (value instanceof Iterable) {
List<Object> mapped = new ArrayList<>();
for (Object o : (Iterable<?>) value) {
mapped.add(convertValue(o, typeInformation.getActualType() != null ? typeInformation.getRequiredActualType()
: ClassTypeInformation.OBJECT));
}
return mapped;
}
if (value.getClass().isArray()
&& (ClassTypeInformation.OBJECT.equals(typeInformation) || typeInformation.isCollectionLike())) {
return value;
}
return this.converter.writeValue(value, typeInformation);
}
protected MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> getMappingContext() {
return this.mappingContext;
}
private Condition createCondition(Column column, @Nullable Object mappedValue, int sqlType,
MapSqlParameterSource parameterSource, Comparator comparator, boolean ignoreCase) {
if (comparator.equals(Comparator.IS_NULL)) {
return column.isNull();
}
if (comparator.equals(Comparator.IS_NOT_NULL)) {
return column.isNotNull();
}
if (comparator == Comparator.IS_TRUE) {
return column.isEqualTo(SQL.literalOf(true));
}
if (comparator == Comparator.IS_FALSE) {
return column.isEqualTo(SQL.literalOf(false));
}
Expression columnExpression = column;
if (ignoreCase && (sqlType == Types.VARCHAR || sqlType == Types.NVARCHAR)) {
columnExpression = Functions.upper(column);
}
if (comparator == Comparator.NOT_IN || comparator == Comparator.IN) {
Condition condition;
if (mappedValue instanceof Iterable) {
List<Expression> expressions = new ArrayList<>(
mappedValue instanceof Collection ? ((Collection<?>) mappedValue).size() : 10);
for (Object o : (Iterable<?>) mappedValue) {
expressions.add(bind(o, sqlType, parameterSource, column.getName().getReference()));
}
condition = Conditions.in(columnExpression, expressions.toArray(new Expression[0]));
} else {
Expression expression = bind(mappedValue, sqlType, parameterSource, column.getName().getReference());
condition = Conditions.in(columnExpression, expression);
}
if (comparator == Comparator.NOT_IN) {
condition = condition.not();
}
return condition;
}
if (comparator == Comparator.BETWEEN || comparator == Comparator.NOT_BETWEEN) {
Pair<Object, Object> pair = (Pair<Object, Object>) mappedValue;
Expression begin = bind(pair.getFirst(), sqlType, parameterSource, column.getName().getReference(), ignoreCase);
Expression end = bind(pair.getSecond(), sqlType, parameterSource, column.getName().getReference(), ignoreCase);
return comparator == Comparator.BETWEEN ? Conditions.between(columnExpression, begin, end)
: Conditions.notBetween(columnExpression, begin, end);
}
String refName = column.getName().getReference();
switch (comparator) {
case EQ: {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase);
return Conditions.isEqual(columnExpression, expression);
}
case NEQ: {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase);
return Conditions.isEqual(columnExpression, expression).not();
}
case LT: {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName);
return column.isLess(expression);
}
case LTE: {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName);
return column.isLessOrEqualTo(expression);
}
case GT: {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName);
return column.isGreater(expression);
}
case GTE: {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName);
return column.isGreaterOrEqualTo(expression);
}
case LIKE: {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase);
return Conditions.like(columnExpression, expression);
}
case NOT_LIKE: {
Expression expression = bind(mappedValue, sqlType, parameterSource, refName, ignoreCase);
return Conditions.notLike(columnExpression, expression);
}
default:
throw new UnsupportedOperationException("Comparator " + comparator + " not supported");
}
}
Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, SqlIdentifier key) {
return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext, converter);
}
Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, SqlIdentifier key,
MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext) {
return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext, converter);
}
Class<?> getTypeHint(@Nullable Object mappedValue, Class<?> propertyType) {
return propertyType;
}
int getTypeHint(@Nullable Object mappedValue, Class<?> propertyType, JdbcValue settableValue) {
if (mappedValue == null || propertyType.equals(Object.class)) {
return JdbcUtils.TYPE_UNKNOWN;
}
if (mappedValue.getClass().equals(settableValue.getValue().getClass())) {
return JdbcUtils.TYPE_UNKNOWN;
}
return settableValue.getJdbcType().getVendorTypeNumber();
}
private Expression bind(@Nullable Object mappedValue, int sqlType, MapSqlParameterSource parameterSource,
String name) {
return bind(mappedValue, sqlType, parameterSource, name, false);
}
private Expression bind(@Nullable Object mappedValue, int sqlType, MapSqlParameterSource parameterSource, String name,
boolean ignoreCase) {
String uniqueName = getUniqueName(parameterSource, name);
parameterSource.addValue(uniqueName, mappedValue, sqlType);
return ignoreCase ? Functions.upper(SQL.bindMarker(":" + uniqueName)) : SQL.bindMarker(":" + uniqueName);
}
private static String getUniqueName(MapSqlParameterSource parameterSource, String name) {
Map<String, Object> values = parameterSource.getValues();
if (!values.containsKey(name)) {
return name;
}
int counter = 1;
String uniqueName;
do {
uniqueName = name + (counter++);
} while (values.containsKey(uniqueName));
return uniqueName;
}
/**
* Value object to represent a field and its meta-information.
*/
protected static class Field {
protected final SqlIdentifier name;
/**
* Creates a new {@link Field} without meta-information but the given name.
*
* @param name must not be {@literal null} or empty.
*/
public Field(SqlIdentifier name) {
Assert.notNull(name, "Name must not be null!");
this.name = name;
}
public boolean isEmbedded() {
return false;
}
/**
* Returns the key to be used in the mapped document eventually.
*
* @return
*/
public SqlIdentifier getMappedColumnName() {
return this.name;
}
public TypeInformation<?> getTypeHint() {
return ClassTypeInformation.OBJECT;
}
public int getSqlType() {
return JdbcUtils.TYPE_UNKNOWN;
}
}
/**
* Extension of {@link Field} to be backed with mapping metadata.
*/
protected static class MetadataBackedField extends Field {
private final RelationalPersistentEntity<?> entity;
private final MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
private final RelationalPersistentProperty property;
private final @Nullable PersistentPropertyPath<RelationalPersistentProperty> path;
private final boolean embedded;
private final int sqlType;
/**
* Creates a new {@link MetadataBackedField} with the given name, {@link RelationalPersistentEntity} and
* {@link MappingContext}.
*
* @param name must not be {@literal null} or empty.
* @param entity must not be {@literal null}.
* @param context must not be {@literal null}.
* @param converter must not be {@literal null}.
*/
protected MetadataBackedField(SqlIdentifier name, RelationalPersistentEntity<?> entity,
MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> context,
JdbcConverter converter) {
this(name, entity, context, null, converter);
}
/**
* Creates a new {@link MetadataBackedField} with the given name, {@link RelationalPersistentEntity} and
* {@link MappingContext} with the given {@link RelationalPersistentProperty}.
*
* @param name must not be {@literal null} or empty.
* @param entity must not be {@literal null}.
* @param context must not be {@literal null}.
* @param property may be {@literal null}.
* @param converter may be {@literal null}.
*/
protected MetadataBackedField(SqlIdentifier name, RelationalPersistentEntity<?> entity,
MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> context,
@Nullable RelationalPersistentProperty property, JdbcConverter converter) {
super(name);
Assert.notNull(entity, "MongoPersistentEntity must not be null!");
this.entity = entity;
this.mappingContext = context;
this.path = getPath(name.getReference());
this.property = this.path == null ? property : this.path.getLeafProperty();
this.sqlType = this.property != null ? converter.getSqlType(this.property) : JdbcUtils.TYPE_UNKNOWN;
if (this.property != null) {
this.embedded = this.property.isEmbedded();
} else {
this.embedded = false;
}
}
@Override
public SqlIdentifier getMappedColumnName() {
if (isEmbedded()) {
throw new IllegalStateException("Cannot obtain a single column name for embedded property");
}
if (this.property != null && this.path != null) {
RelationalPersistentProperty owner = this.path.getParentPath().getLeafProperty();
if (owner != null && owner.isEmbedded()) {
return this.property.getColumnName()
.transform(it -> Objects.requireNonNull(owner.getEmbeddedPrefix()).concat(it));
}
}
return this.path == null || this.path.getLeafProperty() == null ? super.getMappedColumnName()
: this.path.getLeafProperty().getColumnName();
}
/**
* Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}.
*
* @param pathExpression
* @return
*/
@Nullable
private PersistentPropertyPath<RelationalPersistentProperty> getPath(String pathExpression) {
try {
PropertyPath path = PropertyPath.from(pathExpression, this.entity.getTypeInformation());
if (isPathToJavaLangClassProperty(path)) {
return null;
}
return this.mappingContext.getPersistentPropertyPath(path);
} catch (PropertyReferenceException | InvalidPersistentPropertyPath e) {
return null;
}
}
private boolean isPathToJavaLangClassProperty(PropertyPath path) {
return path.getType().equals(Class.class) && path.getLeafProperty().getOwningType().getType().equals(Class.class);
}
@Nullable
public PersistentPropertyPath<RelationalPersistentProperty> getPath() {
return path;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isEmbedded()
*/
@Override
public boolean isEmbedded() {
return this.embedded;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getTypeHint()
*/
@Override
public TypeInformation<?> getTypeHint() {
if (this.property == null) {
return super.getTypeHint();
}
if (this.property.getType().isPrimitive()) {
return ClassTypeInformation.from(ClassUtils.resolvePrimitiveIfNecessary(this.property.getType()));
}
if (this.property.getType().isArray()) {
return this.property.getTypeInformation();
}
if (this.property.getType().isInterface()
|| (java.lang.reflect.Modifier.isAbstract(this.property.getType().getModifiers()))) {
return ClassTypeInformation.OBJECT;
}
return this.property.getTypeInformation();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getSqlType()
*/
@Override
public int getSqlType() {
return this.sqlType;
}
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import java.lang.reflect.Constructor;
import java.sql.JDBCType;
import org.springframework.beans.BeanUtils;
import org.springframework.data.jdbc.core.convert.JdbcColumnTypes;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcValue;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.repository.query.Parameter;
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;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* A query to be executed based on a repository method, it's annotated SQL query and the arguments provided to the
* method.
*
* @author Jens Schauder
* @author Kazuki Shimizu
* @author Oliver Gierke
* @author Maciej Walkowiak
* @author Mark Paluch
* @since 2.0
*/
public class StringBasedJdbcQuery extends AbstractJdbcQuery {
private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters. Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters.";
private final JdbcQueryMethod queryMethod;
private final JdbcQueryExecution<?> executor;
private final JdbcConverter converter;
/**
* Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
* and {@link RowMapper}.
*
* @param queryMethod must not be {@literal null}.
* @param operations must not be {@literal null}.
* @param defaultRowMapper can be {@literal null} (only in case of a modifying query).
*/
public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
@Nullable RowMapper<?> defaultRowMapper, JdbcConverter converter) {
super(queryMethod, operations, defaultRowMapper);
this.queryMethod = queryMethod;
this.converter = converter;
RowMapper<Object> rowMapper = determineRowMapper(defaultRowMapper);
executor = getQueryExecution( //
queryMethod, //
determineResultSetExtractor(rowMapper != defaultRowMapper ? rowMapper : null), //
rowMapper //
);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
*/
@Override
public Object execute(Object[] objects) {
return executor.execute(determineQuery(), this.bindParameters(objects));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
*/
@Override
public JdbcQueryMethod getQueryMethod() {
return queryMethod;
}
MapSqlParameterSource bindParameters(Object[] objects) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
queryMethod.getParameters().getBindableParameters()
.forEach(p -> convertAndAddParameter(parameters, p, objects[p.getIndex()]));
return parameters;
}
private void convertAndAddParameter(MapSqlParameterSource parameters, Parameter p, Object value) {
String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED));
Class<?> parameterType = queryMethod.getParameters().getParameter(p.getIndex()).getType();
Class<?> conversionTargetType = JdbcColumnTypes.INSTANCE.resolvePrimitiveType(parameterType);
JdbcValue jdbcValue = converter.writeJdbcValue(value, conversionTargetType,
JdbcUtil.sqlTypeFor(conversionTargetType));
JDBCType jdbcType = jdbcValue.getJdbcType();
if (jdbcType == null) {
parameters.addValue(parameterName, jdbcValue.getValue());
} else {
parameters.addValue(parameterName, jdbcValue.getValue(), jdbcType.getVendorTypeNumber());
}
}
private String determineQuery() {
String query = queryMethod.getDeclaredQuery();
if (StringUtils.isEmpty(query)) {
throw new IllegalStateException(String.format("No query specified on %s", queryMethod.getName()));
}
return query;
}
@Nullable
@SuppressWarnings({ "rawtypes", "unchecked" })
ResultSetExtractor<Object> determineResultSetExtractor(@Nullable RowMapper<Object> rowMapper) {
Class<? extends ResultSetExtractor> resultSetExtractorClass = queryMethod.getResultSetExtractorClass();
if (isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class)) {
return null;
}
Constructor<? extends ResultSetExtractor> constructor = ClassUtils
.getConstructorIfAvailable(resultSetExtractorClass, RowMapper.class);
if (constructor != null) {
return BeanUtils.instantiateClass(constructor, rowMapper);
}
return BeanUtils.instantiateClass(resultSetExtractorClass);
}
@SuppressWarnings("unchecked")
RowMapper<Object> determineRowMapper(@Nullable RowMapper<?> defaultMapper) {
Class<?> rowMapperClass = queryMethod.getRowMapperClass();
if (isUnconfigured(rowMapperClass, RowMapper.class)) {
return (RowMapper<Object>) defaultMapper;
}
return (RowMapper<Object>) BeanUtils.instantiateClass(rowMapperClass);
}
private static boolean isUnconfigured(@Nullable Class<?> configuredClass, Class<?> defaultClass) {
return configuredClass == null || configuredClass == defaultClass;
}
}

View File

@@ -1,3 +1,6 @@
/**
* Query derivation mechanism for JDBC specific repositories.
*/
@NonNullApi
package org.springframework.data.jdbc.repository.query;

View File

@@ -15,18 +15,24 @@
*/
package org.springframework.data.jdbc.repository.support;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jdbc.core.convert.EntityRowMapper;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.jdbc.repository.query.JdbcQueryMethod;
import org.springframework.data.jdbc.repository.query.PartTreeJdbcQuery;
import org.springframework.data.jdbc.repository.query.StringBasedJdbcQuery;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.event.AfterLoadCallback;
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
@@ -34,9 +40,11 @@ import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link QueryLookupStrategy} for JDBC repositories. Currently only supports annotated queries.
* {@link QueryLookupStrategy} for JDBC repositories.
*
* @author Jens Schauder
* @author Kazuki Shimizu
@@ -45,16 +53,36 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
* @author Maciej Walkowiak
* @author Moises Cisneros
*/
@RequiredArgsConstructor
class JdbcQueryLookupStrategy implements QueryLookupStrategy {
private final ApplicationEventPublisher publisher;
private final EntityCallbacks callbacks;
private final @Nullable EntityCallbacks callbacks;
private final RelationalMappingContext context;
private final JdbcConverter converter;
private final Dialect dialect;
private final QueryMappingConfiguration queryMappingConfiguration;
private final NamedParameterJdbcOperations operations;
public JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
RelationalMappingContext context, JdbcConverter converter, Dialect dialect,
QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations) {
Assert.notNull(publisher, "ApplicationEventPublisher must not be null");
Assert.notNull(context, "RelationalMappingContextPublisher must not be null");
Assert.notNull(converter, "JdbcConverter must not be null");
Assert.notNull(dialect, "Dialect must not be null");
Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null");
Assert.notNull(operations, "NamedParameterJdbcOperations must not be null");
this.publisher = publisher;
this.callbacks = callbacks;
this.context = context;
this.converter = converter;
this.dialect = dialect;
this.queryMappingConfiguration = queryMappingConfiguration;
this.operations = operations;
}
/*
* (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)
@@ -63,24 +91,34 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata,
ProjectionFactory projectionFactory, NamedQueries namedQueries) {
JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries);
JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries,
context);
RowMapper<?> mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod);
if (namedQueries.hasQuery(queryMethod.getNamedQueryName())) {
return new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, mapper, converter);
RowMapper<?> mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod);
return new StringBasedJdbcQuery(queryMethod, operations, mapper, converter);
} else if (queryMethod.hasAnnotatedQuery()) {
RowMapper<?> mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod);
return new StringBasedJdbcQuery(queryMethod, operations, mapper, converter);
} else {
return new PartTreeJdbcQuery(queryMethod, dialect, converter, operations, createMapper(queryMethod));
}
}
private RowMapper<?> createMapper(JdbcQueryMethod queryMethod) {
@SuppressWarnings("unchecked")
private RowMapper<Object> createMapper(JdbcQueryMethod queryMethod) {
Class<?> returnedObjectType = queryMethod.getReturnedObjectType();
RelationalPersistentEntity<?> persistentEntity = context.getPersistentEntity(returnedObjectType);
if (persistentEntity == null) {
return SingleColumnRowMapper.newInstance(returnedObjectType, converter.getConversionService());
return (RowMapper) SingleColumnRowMapper.newInstance(returnedObjectType, converter.getConversionService());
}
return determineDefaultMapper(queryMethod);
return (RowMapper) determineDefaultMapper(queryMethod);
}
private RowMapper<?> determineDefaultMapper(JdbcQueryMethod queryMethod) {
@@ -96,6 +134,32 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy {
converter //
);
return defaultEntityRowMapper;
return new PostProcessingRowMapper<>(defaultEntityRowMapper);
}
class PostProcessingRowMapper<T> implements RowMapper<T> {
private final RowMapper<T> delegate;
PostProcessingRowMapper(RowMapper<T> delegate) {
this.delegate = delegate;
}
@Override
public T mapRow(ResultSet rs, int rowNum) throws SQLException {
T entity = delegate.mapRow(rs, rowNum);
if (entity != null) {
publisher.publishEvent(new AfterLoadEvent<>(entity));
if (callbacks != null) {
return callbacks.callback(AfterLoadCallback.class, entity);
}
}
return entity;
}
}
}

View File

@@ -1,138 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.support;
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* {@link QueryMethod} implementation that implements a method by executing the query from a {@link Query} annotation on
* that method. Binds method arguments to named parameters in the SQL statement.
*
* @author Jens Schauder
* @author Kazuki Shimizu
* @author Moises Cisneros
*/
class JdbcQueryMethod extends QueryMethod {
private final Method method;
private final NamedQueries namedQueries;
public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
NamedQueries namedQueries) {
super(method, metadata, factory);
this.namedQueries = namedQueries;
this.method = method;
}
/**
* Returns the annotated query if it exists.
*
* @return May be {@code null}.
*/
@Nullable
String getDeclaredQuery() {
String annotatedValue = getQueryValue();
return StringUtils.hasText(annotatedValue) ? annotatedValue : getNamedQuery();
}
/**
* Returns the annotated query if it exists.
*
* @return May be {@code null}.
*/
@Nullable
private String getQueryValue() {
return getMergedAnnotationAttribute("value");
}
/**
* Returns the named query for this method if it exists.
*
* @return May be {@code null}.
*/
@Nullable
private String getNamedQuery() {
String name = getQueryName();
return this.namedQueries.hasQuery(name) ? this.namedQueries.getQuery(name) : null;
}
/**
* Returns the annotated query name.
*
* @return May be {@code null}.
*/
private String getQueryName() {
String annotatedName = getMergedAnnotationAttribute("name");
return StringUtils.hasText(annotatedName) ? annotatedName : getNamedQueryName();
}
/*
* Returns the class to be used as {@link org.springframework.jdbc.core.RowMapper}
*
* @return May be {@code null}.
*/
@Nullable
Class<? extends RowMapper> getRowMapperClass() {
return getMergedAnnotationAttribute("rowMapperClass");
}
/**
* Returns the class to be used as {@link org.springframework.jdbc.core.ResultSetExtractor}
*
* @return May be {@code null}.
*/
@Nullable
Class<? extends ResultSetExtractor> getResultSetExtractorClass() {
return getMergedAnnotationAttribute("resultSetExtractorClass");
}
/**
* Returns whether the query method is a modifying one.
*
* @return if it's a modifying query, return {@code true}.
*/
@Override
public boolean isModifyingQuery() {
return AnnotationUtils.findAnnotation(method, Modifying.class) != null;
}
@SuppressWarnings("unchecked")
@Nullable
private <T> T getMergedAnnotationAttribute(String attribute) {
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
return (T) AnnotationUtils.getValue(queryAnnotation, attribute);
}
}

View File

@@ -23,6 +23,7 @@ import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.repository.core.EntityInformation;
@@ -51,6 +52,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
private final ApplicationEventPublisher publisher;
private final DataAccessStrategy accessStrategy;
private final NamedParameterJdbcOperations operations;
private final Dialect dialect;
private QueryMappingConfiguration queryMappingConfiguration = QueryMappingConfiguration.EMPTY;
private EntityCallbacks entityCallbacks;
@@ -62,20 +64,24 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
* @param dataAccessStrategy must not be {@literal null}.
* @param context must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param dialect must not be {@literal null}.
* @param publisher must not be {@literal null}.
* @param operations must not be {@literal null}.
*/
public JdbcRepositoryFactory(DataAccessStrategy dataAccessStrategy, RelationalMappingContext context,
JdbcConverter converter, ApplicationEventPublisher publisher, NamedParameterJdbcOperations operations) {
JdbcConverter converter, Dialect dialect, ApplicationEventPublisher publisher,
NamedParameterJdbcOperations operations) {
Assert.notNull(dataAccessStrategy, "DataAccessStrategy must not be null!");
Assert.notNull(context, "RelationalMappingContext must not be null!");
Assert.notNull(converter, "RelationalConverter must not be null!");
Assert.notNull(dialect, "Dialect must not be null!");
Assert.notNull(publisher, "ApplicationEventPublisher must not be null!");
this.publisher = publisher;
this.context = context;
this.converter = converter;
this.dialect = dialect;
this.accessStrategy = dataAccessStrategy;
this.operations = operations;
}
@@ -136,15 +142,8 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport {
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable QueryLookupStrategy.Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
if (key == null || key == QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND
|| key == QueryLookupStrategy.Key.USE_DECLARED_QUERY) {
JdbcQueryLookupStrategy strategy = new JdbcQueryLookupStrategy(publisher, entityCallbacks, context, converter,
queryMappingConfiguration, operations);
return Optional.of(strategy);
}
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
return Optional.of(new JdbcQueryLookupStrategy(publisher, entityCallbacks, context, converter, dialect,
queryMappingConfiguration, operations));
}
/**

View File

@@ -86,7 +86,7 @@ public class JdbcRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extend
protected RepositoryFactorySupport doCreateRepositoryFactory() {
JdbcRepositoryFactory jdbcRepositoryFactory = new JdbcRepositoryFactory(dataAccessStrategy, mappingContext,
converter, publisher, operations);
converter, dialect, publisher, operations);
jdbcRepositoryFactory.setQueryMappingConfiguration(queryMappingConfiguration);
jdbcRepositoryFactory.setEntityCallbacks(entityCallbacks);

View File

@@ -1,302 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.support;
import java.lang.reflect.Constructor;
import java.sql.JDBCType;
import java.util.List;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.jdbc.core.convert.JdbcColumnTypes;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcValue;
import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.event.AfterLoadCallback;
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
import org.springframework.data.repository.query.Parameter;
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;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* A query to be executed based on a repository method, it's annotated SQL query and the arguments provided to the
* method.
*
* @author Jens Schauder
* @author Kazuki Shimizu
* @author Oliver Gierke
* @author Maciej Walkowiak
*/
class JdbcRepositoryQuery implements RepositoryQuery {
private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters. Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters.";
private final ApplicationEventPublisher publisher;
private final EntityCallbacks callbacks;
private final RelationalMappingContext context;
private final JdbcQueryMethod queryMethod;
private final NamedParameterJdbcOperations operations;
private final QueryExecutor<Object> executor;
private final JdbcConverter converter;
/**
* Creates a new {@link JdbcRepositoryQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext}
* and {@link RowMapper}.
*
* @param publisher must not be {@literal null}.
* @param context must not be {@literal null}.
* @param queryMethod must not be {@literal null}.
* @param operations must not be {@literal null}.
* @param defaultRowMapper can be {@literal null} (only in case of a modifying query).
*/
JdbcRepositoryQuery(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
RelationalMappingContext context, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations,
RowMapper<?> defaultRowMapper, JdbcConverter converter) {
Assert.notNull(publisher, "Publisher must not be null!");
Assert.notNull(context, "Context must not be null!");
Assert.notNull(queryMethod, "Query method must not be null!");
Assert.notNull(operations, "NamedParameterJdbcOperations must not be null!");
if (!queryMethod.isModifyingQuery()) {
Assert.notNull(defaultRowMapper, "Mapper must not be null!");
}
this.publisher = publisher;
this.callbacks = callbacks == null ? EntityCallbacks.create() : callbacks;
this.context = context;
this.queryMethod = queryMethod;
this.operations = operations;
RowMapper<Object> rowMapper = determineRowMapper(defaultRowMapper);
executor = createExecutor( //
queryMethod, //
determineResultSetExtractor(rowMapper != defaultRowMapper ? rowMapper : null), //
rowMapper //
);
this.converter = converter;
}
private QueryExecutor<Object> createExecutor(JdbcQueryMethod queryMethod,
@Nullable ResultSetExtractor<Object> extractor, RowMapper<Object> rowMapper) {
String query = determineQuery();
if (queryMethod.isModifyingQuery()) {
return createModifyingQueryExecutor(query);
}
if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) {
QueryExecutor<Object> innerExecutor = extractor != null ? createResultSetExtractorQueryExecutor(query, extractor)
: createListRowMapperQueryExecutor(query, rowMapper);
return createCollectionQueryExecutor(innerExecutor);
}
QueryExecutor<Object> innerExecutor = extractor != null ? createResultSetExtractorQueryExecutor(query, extractor)
: createObjectRowMapperQueryExecutor(query, rowMapper);
return createObjectQueryExecutor(innerExecutor);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[])
*/
@Override
public Object execute(Object[] objects) {
return executor.execute(bindParameters(objects));
}
private QueryExecutor<Object> createObjectQueryExecutor(QueryExecutor<Object> executor) {
return parameters -> {
try {
Object result = executor.execute(parameters);
publishAfterLoad(result);
return result;
} catch (EmptyResultDataAccessException e) {
return null;
}
};
}
private QueryExecutor<Object> createCollectionQueryExecutor(QueryExecutor<Object> executor) {
return parameters -> {
List<?> result = (List<?>) executor.execute(parameters);
Assert.notNull(result, "A collection valued result must never be null.");
publishAfterLoad(result);
return result;
};
}
private QueryExecutor<Object> createModifyingQueryExecutor(String query) {
return parameters -> {
int updatedCount = operations.update(query, parameters);
Class<?> returnedObjectType = queryMethod.getReturnedObjectType();
return (returnedObjectType == boolean.class || returnedObjectType == Boolean.class) ? updatedCount != 0
: updatedCount;
};
}
private QueryExecutor<Object> createListRowMapperQueryExecutor(String query, RowMapper<?> rowMapper) {
return parameters -> operations.query(query, parameters, rowMapper);
}
private QueryExecutor<Object> createObjectRowMapperQueryExecutor(String query, RowMapper<?> rowMapper) {
return parameters -> operations.queryForObject(query, parameters, rowMapper);
}
private QueryExecutor<Object> createResultSetExtractorQueryExecutor(String query,
ResultSetExtractor<?> resultSetExtractor) {
return parameters -> operations.query(query, parameters, resultSetExtractor);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod()
*/
@Override
public JdbcQueryMethod getQueryMethod() {
return queryMethod;
}
private String determineQuery() {
String query = queryMethod.getDeclaredQuery();
if (StringUtils.isEmpty(query)) {
throw new IllegalStateException(String.format("No query specified on %s", queryMethod.getName()));
}
return query;
}
private MapSqlParameterSource bindParameters(Object[] objects) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
queryMethod.getParameters().getBindableParameters()
.forEach(p -> convertAndAddParameter(parameters, p, objects[p.getIndex()]));
return parameters;
}
private void convertAndAddParameter(MapSqlParameterSource parameters, Parameter p, Object value) {
String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED));
Class<?> parameterType = queryMethod.getParameters().getParameter(p.getIndex()).getType();
Class<?> conversionTargetType = JdbcColumnTypes.INSTANCE.resolvePrimitiveType(parameterType);
JdbcValue jdbcValue = converter.writeJdbcValue(value, conversionTargetType,
JdbcUtil.sqlTypeFor(conversionTargetType));
JDBCType jdbcType = jdbcValue.getJdbcType();
if (jdbcType == null) {
parameters.addValue(parameterName, jdbcValue.getValue());
} else {
parameters.addValue(parameterName, jdbcValue.getValue(), jdbcType.getVendorTypeNumber());
}
}
@Nullable
@SuppressWarnings({ "rawtypes", "unchecked" })
private ResultSetExtractor<Object> determineResultSetExtractor(@Nullable RowMapper<Object> rowMapper) {
Class<? extends ResultSetExtractor> resultSetExtractorClass = queryMethod.getResultSetExtractorClass();
if (isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class)) {
return null;
}
Constructor<? extends ResultSetExtractor> constructor = ClassUtils
.getConstructorIfAvailable(resultSetExtractorClass, RowMapper.class);
if (constructor != null) {
return BeanUtils.instantiateClass(constructor, rowMapper);
}
return BeanUtils.instantiateClass(resultSetExtractorClass);
}
@SuppressWarnings("unchecked")
private RowMapper<Object> determineRowMapper(RowMapper<?> defaultMapper) {
Class<?> rowMapperClass = queryMethod.getRowMapperClass();
if (isUnconfigured(rowMapperClass, RowMapper.class)) {
return (RowMapper<Object>) defaultMapper;
}
return (RowMapper<Object>) BeanUtils.instantiateClass(rowMapperClass);
}
private static boolean isUnconfigured(@Nullable Class<?> configuredClass, Class<?> defaultClass) {
return configuredClass == null || configuredClass == defaultClass;
}
private <T> void publishAfterLoad(Iterable<T> all) {
for (T e : all) {
publishAfterLoad(e);
}
}
private <T> void publishAfterLoad(@Nullable T entity) {
if (entity != null && context.hasPersistentEntityFor(entity.getClass())) {
RelationalPersistentEntity<?> e = context.getRequiredPersistentEntity(entity.getClass());
Object identifier = e.getIdentifierAccessor(entity).getIdentifier();
if (identifier != null) {
publisher.publishEvent(new AfterLoadEvent(entity));
}
callbacks.callback(AfterLoadCallback.class, entity);
}
}
private interface QueryExecutor<T> {
@Nullable
T execute(MapSqlParameterSource parameter);
}
}

View File

@@ -21,16 +21,19 @@ import static org.assertj.core.api.Assertions.*;
import lombok.Data;
import java.io.IOException;
import java.util.List;
import java.sql.ResultSet;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -39,11 +42,14 @@ 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.relational.core.mapping.event.AbstractRelationalEvent;
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
import org.springframework.data.repository.query.Param;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.rules.SpringClassRule;
@@ -55,6 +61,7 @@ import org.springframework.transaction.annotation.Transactional;
* Very simple use cases for creation and usage of JdbcRepositories.
*
* @author Jens Schauder
* @author Mark Paluch
*/
@ContextConfiguration
@Transactional
@@ -83,7 +90,21 @@ public class JdbcRepositoryIntegrationTests {
properties.setLocation(new ClassPathResource("META-INF/jdbc-named-queries.properties"));
properties.afterPropertiesSet();
return new PropertiesBasedNamedQueries(properties.getObject());
}
@Bean
MyEventListener eventListener() {
return new MyEventListener();
}
}
static class MyEventListener implements ApplicationListener<AbstractRelationalEvent<?>> {
private List<AbstractRelationalEvent<?>> events = new ArrayList<>();
@Override
public void onApplicationEvent(AbstractRelationalEvent<?> event) {
events.add(event);
}
}
@@ -92,6 +113,12 @@ public class JdbcRepositoryIntegrationTests {
@Autowired NamedParameterJdbcTemplate template;
@Autowired DummyEntityRepository repository;
@Autowired MyEventListener eventListener;
@Before
public void before() {
eventListener.events.clear();
}
@Test // DATAJDBC-95
public void savesAnEntity() {
@@ -263,7 +290,7 @@ public class JdbcRepositoryIntegrationTests {
assertThat(repository.findById(-1L)).isEmpty();
}
@Test // DATAJDBC-464
@Test // DATAJDBC-464, DATAJDBC-318
public void executeQueryWithParameterRequiringConversion() {
Instant now = Instant.now();
@@ -281,6 +308,32 @@ public class JdbcRepositoryIntegrationTests {
assertThat(repository.after(now)) //
.extracting(DummyEntity::getName) //
.containsExactly("second");
assertThat(repository.findAllByPointInTimeAfter(now)) //
.extracting(DummyEntity::getName) //
.containsExactly("second");
}
@Test // DATAJDBC-318
public void queryMethodShouldEmitEvents() {
repository.save(createDummyEntity());
eventListener.events.clear();
repository.findAllWithSql();
assertThat(eventListener.events).hasSize(1).hasOnlyElementsOfType(AfterLoadEvent.class);
}
@Test // DATAJDBC-318
public void queryMethodWithCustomRowMapperDoesNotEmitEvents() {
repository.save(createDummyEntity());
eventListener.events.clear();
repository.findAllWithCustomMapper();
assertThat(eventListener.events).isEmpty();
}
@Test // DATAJDBC-234
@@ -314,12 +367,19 @@ public class JdbcRepositoryIntegrationTests {
List<DummyEntity> findAllByNamedQuery();
List<DummyEntity> findAllByPointInTimeAfter(Instant instant);
@Query("SELECT * FROM DUMMY_ENTITY")
List<DummyEntity> findAllWithSql();
@Query(value = "SELECT * FROM DUMMY_ENTITY", rowMapperClass = CustomRowMapper.class)
List<DummyEntity> findAllWithCustomMapper();
@Query("SELECT * FROM DUMMY_ENTITY WHERE POINT_IN_TIME > :threshhold")
List<DummyEntity> after(@Param("threshhold")Instant threshhold);
List<DummyEntity> after(@Param("threshhold") Instant threshhold);
@Query("SELECT id_Prop from dummy_entity where id_Prop = :id")
DummyEntity withMissingColumn(@Param("id")Long id);
DummyEntity withMissingColumn(@Param("id") Long id);
}
@Data
@@ -328,4 +388,12 @@ public class JdbcRepositoryIntegrationTests {
@Id private Long idProp;
Instant pointInTime;
}
static class CustomRowMapper implements RowMapper<DummyEntity> {
@Override
public DummyEntity mapRow(ResultSet rs, int rowNum) {
return new DummyEntity();
}
}
}

View File

@@ -48,6 +48,7 @@ import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.dialect.H2Dialect;
import org.springframework.data.relational.core.dialect.HsqlDbDialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.event.AfterDeleteEvent;
@@ -97,8 +98,8 @@ public class SimpleJdbcRepositoryEventsUnitTests {
delegatingDataAccessStrategy.setDelegate(dataAccessStrategy);
doReturn(true).when(dataAccessStrategy).update(any(), any());
JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, publisher,
operations);
JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter,
H2Dialect.INSTANCE, publisher, operations);
this.repository = factory.getRepository(DummyEntityRepository.class);
}

View File

@@ -53,7 +53,7 @@ import org.springframework.transaction.annotation.Transactional;
*/
@ContextConfiguration
@Transactional
public class JdbcRepositoryQueryMappingConfigurationIntegrationTests {
public class StringBasedJdbcQueryMappingConfigurationIntegrationTests {
private static String CAR_MODEL = "ResultSetExtractor Car";
@@ -64,7 +64,7 @@ public class JdbcRepositoryQueryMappingConfigurationIntegrationTests {
@Bean
Class<?> testClass() {
return JdbcRepositoryQueryMappingConfigurationIntegrationTests.class;
return StringBasedJdbcQueryMappingConfigurationIntegrationTests.class;
}
@Bean

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018-2020 the original author or authors.
* Copyright 2020 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.
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.support;
package org.springframework.data.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
@@ -26,7 +26,8 @@ import java.util.Properties;
import org.jetbrains.annotations.NotNull;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -47,6 +48,7 @@ public class JdbcQueryMethodUnitTests {
public static final String METHOD_WITHOUT_QUERY_ANNOTATION = "methodWithImplicitlyNamedQuery";
public static final String QUERY2 = "SELECT something NAME AND VALUE";
JdbcMappingContext mappingContext = new JdbcMappingContext();
NamedQueries namedQueries;
RepositoryMetadata metadata;
@@ -100,7 +102,7 @@ public class JdbcQueryMethodUnitTests {
private JdbcQueryMethod createJdbcQueryMethod(String methodName) throws NoSuchMethodException {
Method method = JdbcQueryMethodUnitTests.class.getDeclaredMethod(methodName);
return new JdbcQueryMethod(method, metadata, mock(ProjectionFactory.class), namedQueries);
return new JdbcQueryMethod(method, metadata, mock(ProjectionFactory.class), namedQueries, mappingContext);
}
@Test // DATAJDBC-234

View File

@@ -0,0 +1,644 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.RelationResolver;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.relational.core.dialect.H2Dialect;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
/**
* Unit tests for {@link PartTreeJdbcQuery}.
*
* @author Roman Chigvintsev
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.class)
public class PartTreeJdbcQueryUnitTests {
private static final String TABLE = "\"users\"";
private static final String ALL_FIELDS = "\"users\".\"ID\", \"users\".\"FIRST_NAME\", \"users\".\"LAST_NAME\", \"users\".\"DATE_OF_BIRTH\", \"users\".\"AGE\", \"users\".\"ACTIVE\", \"users\".\"USER_STREET\", \"users\".\"USER_CITY\"";
JdbcMappingContext mappingContext = new JdbcMappingContext();
JdbcConverter converter = new BasicJdbcConverter(mappingContext, mock(RelationResolver.class));
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttribute() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "John" }));
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name");
}
@Test // DATAJDBC-318
public void createsQueryWithIsNullCondition() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
ParametrizedQuery query = jdbcQuery.createQuery((getAccessor(queryMethod, new Object[] { null })));
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" IS NULL");
}
@Test // DATAJDBC-318
public void createsQueryWithLimitForExistsProjection() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("existsByFirstName", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
ParametrizedQuery query = jdbcQuery.createQuery((getAccessor(queryMethod, new Object[] { "John" })));
assertThat(query.getQuery()).isEqualTo(
"SELECT " + TABLE + ".\"ID\" FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name LIMIT 1");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByTwoStringAttributes() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameAndFirstName", String.class, String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "Doe", "John" }));
assertThat(query.getQuery()).isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE
+ ".\"LAST_NAME\" = :last_name AND (" + TABLE + ".\"FIRST_NAME\" = :first_name)");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByOneOfTwoStringAttributes() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameOrFirstName", String.class, String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "Doe", "John" }));
assertThat(query.getQuery()).isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE
+ ".\"LAST_NAME\" = :last_name OR (" + TABLE + ".\"FIRST_NAME\" = :first_name)");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByDateAttributeBetween() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthBetween", Date.class, Date.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
Date from = new Date();
Date to = new Date();
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { from, to });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE
+ ".\"DATE_OF_BIRTH\" BETWEEN :date_of_birth AND :date_of_birth1");
assertThat(query.getParameterSource().getValue("date_of_birth")).isEqualTo(from);
assertThat(query.getParameterSource().getValue("date_of_birth1")).isEqualTo(to);
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeLessThan() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeLessThan", Integer.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" < :age");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeLessThanEqual() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeLessThanEqual", Integer.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" <= :age");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeGreaterThan() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeGreaterThan", Integer.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" > :age");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeGreaterThanEqual() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeGreaterThanEqual", Integer.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" >= :age");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByDateAttributeAfter() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthAfter", Date.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Date() });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo(
"SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"DATE_OF_BIRTH\" > :date_of_birth");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByDateAttributeBefore() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthBefore", Date.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Date() });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo(
"SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"DATE_OF_BIRTH\" < :date_of_birth");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeIsNull() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeIsNull");
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]);
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" IS NULL");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeIsNotNull() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeIsNotNull");
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]);
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" IS NOT NULL");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttributeLike() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameLike", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "%John%" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttributeNotLike() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotLike", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "%John%" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo(
"SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" NOT LIKE :first_name");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttributeStartingWith() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameStartingWith", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Jo" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name");
}
@Test // DATAJDBC-318
public void appendsLikeOperatorParameterWithPercentSymbolForStartingWithQuery() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameStartingWith", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Jo" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name");
assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("Jo%");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttributeEndingWith() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameEndingWith", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "hn" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name");
}
@Test // DATAJDBC-318
public void prependsLikeOperatorParameterWithPercentSymbolForEndingWithQuery() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameEndingWith", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "hn" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name");
assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%hn");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttributeContaining() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameContaining", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name");
}
@Test // DATAJDBC-318
public void wrapsLikeOperatorParameterWithPercentSymbolsForContainingQuery() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameContaining", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name");
assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%oh%");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttributeNotContaining() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotContaining", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo(
"SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" NOT LIKE :first_name");
}
@Test // DATAJDBC-318
public void wrapsLikeOperatorParameterWithPercentSymbolsForNotContainingQuery() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotContaining", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo(
"SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" NOT LIKE :first_name");
assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%oh%");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeWithDescendingOrderingByStringAttribute()
throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeOrderByLastNameDesc", Integer.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 123 });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo(
"SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" = :age ORDER BY \"LAST_NAME\" DESC");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeWithAscendingOrderingByStringAttribute() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeOrderByLastNameAsc", Integer.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 123 });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo(
"SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" = :age ORDER BY \"LAST_NAME\" ASC");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttributeNot() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameNot", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Doe" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"LAST_NAME\" != :last_name");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeIn() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeIn", Collection.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod,
new Object[] { Collections.singleton(25) });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" IN (:age)");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByIntegerAttributeNotIn() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeNotIn", Collection.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod,
new Object[] { Collections.singleton(25) });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"AGE\" NOT IN (:age)");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByBooleanAttributeTrue() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByActiveTrue");
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]);
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"ACTIVE\" = TRUE");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByBooleanAttributeFalse() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByActiveFalse");
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]);
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery())
.isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".\"ACTIVE\" = FALSE");
}
@Test // DATAJDBC-318
public void createsQueryToFindAllEntitiesByStringAttributeIgnoringCase() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameIgnoreCase", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
assertThat(query.getQuery()).isEqualTo(
"SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE UPPER(" + TABLE + ".\"FIRST_NAME\") = UPPER(:first_name)");
}
@Test // DATAJDBC-318
public void throwsExceptionWhenIgnoringCaseIsImpossible() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findByIdIgnoringCase", Long.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
assertThatIllegalStateException()
.isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { 1L })));
}
@Test // DATAJDBC-318
public void throwsExceptionWhenConditionKeywordIsUnsupported() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByIdIsEmpty");
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
assertThatIllegalArgumentException()
.isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[0])));
}
@Test // DATAJDBC-318
public void throwsExceptionWhenInvalidNumberOfParameterIsGiven() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
assertThatIllegalArgumentException()
.isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[0])));
}
@Test // DATAJDBC-318
public void createsQueryWithLimitToFindEntitiesByStringAttribute() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findTop3ByFirstName", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE
+ ".\"FIRST_NAME\" = :first_name LIMIT 3";
assertThat(query.getQuery()).isEqualTo(expectedSql);
}
@Test // DATAJDBC-318
public void createsQueryToFindFirstEntityByStringAttribute() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findFirstByFirstName", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE
+ ".\"FIRST_NAME\" = :first_name LIMIT 1";
assertThat(query.getQuery()).isEqualTo(expectedSql);
}
@Test // DATAJDBC-318
public void createsQueryByEmbeddedObject() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findByAddress", Address.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod,
new Object[] { new Address("Hello", "World") });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE (" + TABLE
+ ".\"USER_STREET\" = :user_street AND " + TABLE + ".\"USER_CITY\" = :user_city)";
assertThat(query.getQuery()).isEqualTo(expectedSql);
assertThat(query.getParameterSource().getValue("user_street")).isEqualTo("Hello");
assertThat(query.getParameterSource().getValue("user_city")).isEqualTo("World");
}
@Test // DATAJDBC-318
public void createsQueryByEmbeddedProperty() throws Exception {
JdbcQueryMethod queryMethod = getQueryMethod("findByAddressStreet", String.class);
PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod);
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Hello" });
ParametrizedQuery query = jdbcQuery.createQuery(accessor);
String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE
+ ".\"USER_STREET\" = :user_street";
assertThat(query.getQuery()).isEqualTo(expectedSql);
assertThat(query.getParameterSource().getValue("user_street")).isEqualTo("Hello");
}
private PartTreeJdbcQuery createQuery(JdbcQueryMethod queryMethod) {
return new PartTreeJdbcQuery(queryMethod, H2Dialect.INSTANCE, converter, mock(NamedParameterJdbcOperations.class),
mock(RowMapper.class));
}
private JdbcQueryMethod getQueryMethod(String methodName, Class<?>... parameterTypes) throws Exception {
Method method = UserRepository.class.getMethod(methodName, parameterTypes);
return new JdbcQueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class),
new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), mappingContext);
}
private RelationalParametersParameterAccessor getAccessor(JdbcQueryMethod queryMethod, Object[] values) {
return new RelationalParametersParameterAccessor(queryMethod, values);
}
interface UserRepository extends Repository<User, Long> {
List<User> findAllByFirstName(String firstName);
List<User> findAllByLastNameAndFirstName(String lastName, String firstName);
List<User> findAllByLastNameOrFirstName(String lastName, String firstName);
Boolean existsByFirstName(String firstName);
List<User> findAllByDateOfBirthBetween(Date from, Date to);
List<User> findAllByAgeLessThan(Integer age);
List<User> findAllByAgeLessThanEqual(Integer age);
List<User> findAllByAgeGreaterThan(Integer age);
List<User> findAllByAgeGreaterThanEqual(Integer age);
List<User> findAllByDateOfBirthAfter(Date date);
List<User> findAllByDateOfBirthBefore(Date date);
List<User> findAllByAgeIsNull();
List<User> findAllByAgeIsNotNull();
List<User> findAllByFirstNameLike(String like);
List<User> findAllByFirstNameNotLike(String like);
List<User> findAllByFirstNameStartingWith(String starting);
List<User> findAllByFirstNameEndingWith(String ending);
List<User> findAllByFirstNameContaining(String containing);
List<User> findAllByFirstNameNotContaining(String notContaining);
List<User> findAllByAgeOrderByLastNameAsc(Integer age);
List<User> findAllByAgeOrderByLastNameDesc(Integer age);
List<User> findAllByLastNameNot(String lastName);
List<User> findAllByAgeIn(Collection<Integer> ages);
List<User> findAllByAgeNotIn(Collection<Integer> ages);
List<User> findAllByActiveTrue();
List<User> findAllByActiveFalse();
List<User> findAllByFirstNameIgnoreCase(String firstName);
User findByIdIgnoringCase(Long id);
List<User> findAllByIdIsEmpty();
List<User> findTop3ByFirstName(String firstName);
User findFirstByFirstName(String firstName);
User findByAddress(Address address);
User findByAddressStreet(String street);
}
@Table("users")
@Data
static class User {
@Id Long id;
String firstName;
String lastName;
Date dateOfBirth;
Integer age;
Boolean active;
@Embedded(prefix = "user_", onEmpty = Embedded.OnEmpty.USE_NULL) Address address;
}
@Data
@AllArgsConstructor
static class Address {
String street;
String city;
}
}

View File

@@ -0,0 +1,379 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.domain.Sort.Order.*;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.RelationResolver;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.relational.core.dialect.PostgresDialect;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.sql.Condition;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Functions;
import org.springframework.data.relational.core.sql.OrderByField;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
/**
* Unit tests for {@link QueryMapper}.
*
* @author Mark Paluch
*/
public class QueryMapperUnitTests {
JdbcMappingContext context = new JdbcMappingContext();
JdbcConverter converter = new BasicJdbcConverter(context, mock(RelationResolver.class));
QueryMapper mapper = new QueryMapper(PostgresDialect.INSTANCE, converter);
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
@Test // DATAJDBC-318
public void shouldNotMapEmptyCriteria() {
Criteria criteria = Criteria.empty();
assertThatIllegalArgumentException().isThrownBy(() -> map(criteria));
}
@Test // DATAJDBC-318
public void shouldNotMapEmptyAndCriteria() {
Criteria criteria = Criteria.empty().and(Collections.emptyList());
assertThatIllegalArgumentException().isThrownBy(() -> map(criteria));
}
@Test // DATAJDBC-318
public void shouldNotMapEmptyNestedCriteria() {
Criteria criteria = Criteria.empty().and(Collections.emptyList()).and(Criteria.empty().and(Criteria.empty()));
assertThat(criteria.isEmpty()).isTrue();
assertThatIllegalArgumentException().isThrownBy(() -> map(criteria));
}
@Test // DATAJDBC-318
public void shouldMapSomeNestedCriteria() {
Criteria criteria = Criteria.empty().and(Collections.emptyList())
.and(Criteria.empty().and(Criteria.where("name").is("Hank")));
assertThat(criteria.isEmpty()).isFalse();
Condition condition = map(criteria);
assertThat(condition).hasToString("((person.\"NAME\" = ?[:name]))");
}
@Test // DATAJDBC-318
public void shouldMapNestedGroup() {
Criteria initial = Criteria.empty();
Criteria criteria = initial.and(Criteria.where("name").is("Foo")) //
.and(Criteria.where("name").is("Bar") //
.or("age").lessThan(49) //
.or(Criteria.where("name").not("Bar") //
.and("age").greaterThan(49) //
) //
);
assertThat(criteria.isEmpty()).isFalse();
Condition condition = map(criteria);
assertThat(condition).hasToString(
"(person.\"NAME\" = ?[:name]) AND (person.\"NAME\" = ?[:name1] OR person.age < ?[:age] OR (person.\"NAME\" != ?[:name2] AND person.age > ?[:age1]))");
}
@Test // DATAJDBC-318
public void shouldMapFrom() {
Criteria criteria = Criteria.from(Criteria.where("name").is("Foo")) //
.and(Criteria.where("name").is("Bar") //
.or("age").lessThan(49) //
);
assertThat(criteria.isEmpty()).isFalse();
Condition condition = map(criteria);
assertThat(condition)
.hasToString("person.\"NAME\" = ?[:name] AND (person.\"NAME\" = ?[:name1] OR person.age < ?[:age])");
}
@Test // DATAJDBC-318
public void shouldMapSimpleCriteria() {
Criteria criteria = Criteria.where("name").is("foo");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" = ?[:name]");
}
@Test // DATAJDBC-318
public void shouldMapSimpleCriteriaWithoutEntity() {
Criteria criteria = Criteria.where("name").is("foo");
Condition condition = mapper.getMappedObject(new MapSqlParameterSource(), criteria, Table.create("person"), null);
assertThat(condition).hasToString("person.name = ?[:name]");
}
@Test // DATAJDBC-318
public void shouldMapExpression() {
Table table = Table.create("my_table").as("my_aliased_table");
Expression mappedObject = mapper.getMappedObject(table.column("alternative").as("my_aliased_col"),
context.getRequiredPersistentEntity(Person.class));
assertThat(mappedObject).hasToString("my_aliased_table.\"another_name\" AS my_aliased_col");
}
@Test // DATAJDBC-318
public void shouldMapCountFunction() {
Table table = Table.create("my_table").as("my_aliased_table");
Expression mappedObject = mapper.getMappedObject(Functions.count(table.column("alternative")),
context.getRequiredPersistentEntity(Person.class));
assertThat(mappedObject).hasToString("COUNT(my_aliased_table.\"another_name\")");
}
@Test // DATAJDBC-318
public void shouldMapExpressionToUnknownColumn() {
Table table = Table.create("my_table").as("my_aliased_table");
Expression mappedObject = mapper.getMappedObject(table.column("unknown").as("my_aliased_col"),
context.getRequiredPersistentEntity(Person.class));
assertThat(mappedObject).hasToString("my_aliased_table.unknown AS my_aliased_col");
}
@Test // DATAJDBC-318
public void shouldMapExpressionWithoutEntity() {
Table table = Table.create("my_table").as("my_aliased_table");
Expression mappedObject = mapper.getMappedObject(table.column("my_col").as("my_aliased_col"), null);
assertThat(mappedObject).hasToString("my_aliased_table.my_col AS my_aliased_col");
}
@Test // DATAJDBC-318
public void shouldMapSimpleNullableCriteria() {
Criteria criteria = Criteria.where("name").isNull();
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" IS NULL");
}
@Test // DATAJDBC-318
public void shouldConsiderColumnName() {
Criteria criteria = Criteria.where("alternative").is("foo");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"another_name\" = ?[:another_name]");
}
@Test // DATAJDBC-318
public void shouldMapAndCriteria() {
Criteria criteria = Criteria.where("name").is("foo").and("bar").is("baz");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" = ?[:name] AND person.bar = ?[:bar]");
}
@Test // DATAJDBC-318
public void shouldMapOrCriteria() {
Criteria criteria = Criteria.where("name").is("foo").or("bar").is("baz");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" = ?[:name] OR person.bar = ?[:bar]");
}
@Test // DATAJDBC-318
public void shouldMapAndOrCriteria() {
Criteria criteria = Criteria.where("name").is("foo") //
.and("name").isNotNull() //
.or("bar").is("baz") //
.and("anotherOne").is("alternative");
Condition condition = map(criteria);
assertThat(condition).hasToString(
"person.\"NAME\" = ?[:name] AND person.\"NAME\" IS NOT NULL OR person.bar = ?[:bar] AND person.anotherOne = ?[:anotherOne]");
}
@Test // DATAJDBC-318
public void shouldMapNeq() {
Criteria criteria = Criteria.where("name").not("foo");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" != ?[:name]");
}
@Test // DATAJDBC-318
public void shouldMapIsNull() {
Criteria criteria = Criteria.where("name").isNull();
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" IS NULL");
}
@Test // DATAJDBC-318
public void shouldMapIsNotNull() {
Criteria criteria = Criteria.where("name").isNotNull();
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" IS NOT NULL");
}
@Test // DATAJDBC-318
public void shouldMapIsIn() {
Criteria criteria = Criteria.where("name").in("a", "b", "c");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" IN (?[:name], ?[:name1], ?[:name2])");
}
@Test // DATAJDBC-318
public void shouldMapIsNotIn() {
Criteria criteria = Criteria.where("name").notIn("a", "b", "c");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" NOT IN (?[:name], ?[:name1], ?[:name2])");
}
@Test // DATAJDBC-318
public void shouldMapIsGt() {
Criteria criteria = Criteria.where("name").greaterThan("a");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" > ?[:name]");
}
@Test // DATAJDBC-318
public void shouldMapIsGte() {
Criteria criteria = Criteria.where("name").greaterThanOrEquals("a");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" >= ?[:name]");
}
@Test // DATAJDBC-318
public void shouldMapIsLt() {
Criteria criteria = Criteria.where("name").lessThan("a");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" < ?[:name]");
}
@Test // DATAJDBC-318
public void shouldMapIsLte() {
Criteria criteria = Criteria.where("name").lessThanOrEquals("a");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" <= ?[:name]");
}
@Test // DATAJDBC-318
public void shouldMapBetween() {
Criteria criteria = Criteria.where("name").between("a", "b");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" BETWEEN ?[:name] AND ?[:name1]");
}
@Test // DATAJDBC-318
public void shouldMapIsLike() {
Criteria criteria = Criteria.where("name").like("a");
Condition condition = map(criteria);
assertThat(condition).hasToString("person.\"NAME\" LIKE ?[:name]");
}
@Test // DATAJDBC-318
public void shouldMapSort() {
Sort sort = Sort.by(desc("alternative"));
List<OrderByField> fields = mapper.getMappedSort(Table.create("tbl"), sort,
context.getRequiredPersistentEntity(Person.class));
assertThat(fields).hasSize(1);
assertThat(fields.get(0)).hasToString("tbl.\"another_name\" DESC");
}
private Condition map(Criteria criteria) {
return mapper.getMappedObject(parameterSource, criteria, Table.create("person"),
context.getRequiredPersistentEntity(Person.class));
}
static class Person {
String name;
@Column("another_name") String alternative;
}
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.sql.ResultSet;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.DataAccessException;
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.RelationResolver;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.repository.query.RelationalParameters;
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;
/**
* Unit tests for {@link StringBasedJdbcQuery}.
*
* @author Jens Schauder
* @author Oliver Gierke
* @author Maciej Walkowiak
* @author Evgeni Dimitrov
* @author Mark Paluch
*/
public class StringBasedJdbcQueryUnitTests {
JdbcQueryMethod queryMethod;
RowMapper<Object> defaultRowMapper;
NamedParameterJdbcOperations operations;
RelationalMappingContext context;
JdbcConverter converter;
@Before
public void setup() throws NoSuchMethodException {
this.queryMethod = mock(JdbcQueryMethod.class);
Parameters<?, ?> parameters = new RelationalParameters(
StringBasedJdbcQueryUnitTests.class.getDeclaredMethod("dummyMethod"));
doReturn(parameters).when(queryMethod).getParameters();
this.defaultRowMapper = mock(RowMapper.class);
this.operations = mock(NamedParameterJdbcOperations.class);
this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS);
this.converter = new BasicJdbcConverter(context, mock(RelationResolver.class));
}
@Test // DATAJDBC-165
public void emptyQueryThrowsException() {
doReturn(null).when(queryMethod).getDeclaredQuery();
Assertions.assertThatExceptionOfType(IllegalStateException.class) //
.isThrownBy(() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter)
.execute(new Object[] {}));
}
@Test // DATAJDBC-165
public void defaultRowMapperIsUsedByDefault() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(RowMapper.class).when(queryMethod).getRowMapperClass();
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter);
assertThat(query.determineRowMapper(defaultRowMapper)).isEqualTo(defaultRowMapper);
}
@Test // DATAJDBC-165, DATAJDBC-318
public void defaultRowMapperIsUsedForNull() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter);
assertThat(query.determineRowMapper(defaultRowMapper)).isEqualTo(defaultRowMapper);
}
@Test // DATAJDBC-165, DATAJDBC-318
public void customRowMapperIsUsedWhenSpecified() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass();
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter);
assertThat(query.determineRowMapper(defaultRowMapper)).isInstanceOf(CustomRowMapper.class);
}
@Test // DATAJDBC-290
public void customResultSetExtractorIsUsedWhenSpecified() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass();
new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter).execute(new Object[] {});
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter);
ResultSetExtractor<Object> resultSetExtractor = query.determineResultSetExtractor(defaultRowMapper);
assertThat(resultSetExtractor) //
.isInstanceOf(CustomResultSetExtractor.class) //
.matches(crse -> ((CustomResultSetExtractor) crse).rowMapper == defaultRowMapper,
"RowMapper is expected to be default.");
}
@Test // DATAJDBC-290
public void customResultSetExtractorAndRowMapperGetCombined() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass();
doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass();
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter);
ResultSetExtractor<Object> resultSetExtractor = query
.determineResultSetExtractor(query.determineRowMapper(defaultRowMapper));
assertThat(resultSetExtractor) //
.isInstanceOf(CustomResultSetExtractor.class) //
.matches(crse -> ((CustomResultSetExtractor) crse).rowMapper instanceof CustomRowMapper,
"RowMapper is not expected to be custom");
}
/**
* The whole purpose of this method is to easily generate a {@link DefaultParameters} instance during test setup.
*/
@SuppressWarnings("unused")
private void dummyMethod() {}
private static class CustomRowMapper implements RowMapper<Object> {
@Override
public Object mapRow(ResultSet rs, int rowNum) {
return null;
}
}
private static class CustomResultSetExtractor implements ResultSetExtractor<Object> {
private final RowMapper rowMapper;
CustomResultSetExtractor() {
rowMapper = null;
}
public CustomResultSetExtractor(RowMapper rowMapper) {
this.rowMapper = rowMapper;
}
@Override
public Object extractData(ResultSet rs) throws DataAccessException {
return null;
}
}
private static class DummyEntity {
private Long id;
public DummyEntity(Long id) {
this.id = id;
}
Long getId() {
return id;
}
}
}

View File

@@ -25,13 +25,13 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
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.mapping.callback.EntityCallbacks;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.relational.core.dialect.H2Dialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -49,6 +49,7 @@ import org.springframework.util.ReflectionUtils;
* @author Mark Paluch
* @author Maciej Walkowiak
* @author Evgeni Dimitrov
* @author Mark Paluch
*/
public class JdbcQueryLookupStrategyUnitTests {
@@ -56,7 +57,6 @@ public class JdbcQueryLookupStrategyUnitTests {
EntityCallbacks callbacks = mock(EntityCallbacks.class);
RelationalMappingContext mappingContext = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS);
JdbcConverter converter = mock(JdbcConverter.class);
DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class);
ProjectionFactory projectionFactory = mock(ProjectionFactory.class);
RepositoryMetadata metadata;
NamedQueries namedQueries = mock(NamedQueries.class);
@@ -82,13 +82,13 @@ public class JdbcQueryLookupStrategyUnitTests {
repositoryQuery.execute(new Object[] {});
verify(operations).queryForObject(anyString(), any(SqlParameterSource.class), eq(numberFormatMapper));
verify(operations).queryForObject(anyString(), any(SqlParameterSource.class), any(RowMapper.class));
}
private RepositoryQuery getRepositoryQuery(String name, QueryMappingConfiguration mappingConfiguration) {
JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(publisher, callbacks, mappingContext, converter,
mappingConfiguration, operations);
JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(publisher, callbacks, mappingContext,
converter, H2Dialect.INSTANCE, mappingConfiguration, operations);
Method method = ReflectionUtils.findMethod(MyRepository.class, name);
return queryLookupStrategy.resolveQuery(method, metadata, projectionFactory, namedQueries);

View File

@@ -1,266 +0,0 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.sql.ResultSet;
import java.util.Arrays;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DataAccessException;
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.RelationResolver;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.event.AfterLoadCallback;
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;
/**
* Unit tests for {@link JdbcRepositoryQuery}.
*
* @author Jens Schauder
* @author Oliver Gierke
* @author Maciej Walkowiak
* @author Evgeni Dimitrov
* @author Mark Paluch
*/
public class JdbcRepositoryQueryUnitTests {
JdbcQueryMethod queryMethod;
RowMapper<?> defaultRowMapper;
ResultSetExtractor<?> defaultResultSetExtractor;
NamedParameterJdbcOperations operations;
ApplicationEventPublisher publisher;
EntityCallbacks callbacks;
RelationalMappingContext context;
JdbcConverter converter;
@Before
public void setup() throws NoSuchMethodException {
this.queryMethod = mock(JdbcQueryMethod.class);
Parameters<?, ?> parameters = new DefaultParameters(
JdbcRepositoryQueryUnitTests.class.getDeclaredMethod("dummyMethod"));
doReturn(parameters).when(queryMethod).getParameters();
this.defaultRowMapper = mock(RowMapper.class);
this.operations = mock(NamedParameterJdbcOperations.class);
this.publisher = mock(ApplicationEventPublisher.class);
this.callbacks = mock(EntityCallbacks.class);
this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS);
this.converter = new BasicJdbcConverter(context, mock(RelationResolver.class));
}
@Test // DATAJDBC-165
public void emptyQueryThrowsException() {
doReturn(null).when(queryMethod).getDeclaredQuery();
Assertions.assertThatExceptionOfType(IllegalStateException.class) //
.isThrownBy(
() -> new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter)
.execute(new Object[] {}));
}
@Test // DATAJDBC-165
public void defaultRowMapperIsUsedByDefault() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(RowMapper.class).when(queryMethod).getRowMapperClass();
JdbcRepositoryQuery query = new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations,
defaultRowMapper, converter);
query.execute(new Object[] {});
verify(operations).queryForObject(anyString(), any(SqlParameterSource.class), eq(defaultRowMapper));
}
@Test // DATAJDBC-165
public void defaultRowMapperIsUsedForNull() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
JdbcRepositoryQuery query = new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations,
defaultRowMapper, converter);
query.execute(new Object[] {});
verify(operations).queryForObject(anyString(), any(SqlParameterSource.class), eq(defaultRowMapper));
}
@Test // DATAJDBC-165
public void customRowMapperIsUsedWhenSpecified() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass();
new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter)
.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).getDeclaredQuery();
doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass();
new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter)
.execute(new Object[] {});
ArgumentCaptor<CustomResultSetExtractor> captor = ArgumentCaptor.forClass(CustomResultSetExtractor.class);
verify(operations).query(anyString(), any(SqlParameterSource.class), captor.capture());
assertThat(captor.getValue()) //
.isInstanceOf(CustomResultSetExtractor.class) // not verified by the captor
.matches(crse -> crse.rowMapper == null, "RowMapper is expected to be null.");
}
@Test // DATAJDBC-290
public void customResultSetExtractorAndRowMapperGetCombined() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass();
doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass();
new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter)
.execute(new Object[] {});
ArgumentCaptor<CustomResultSetExtractor> captor = ArgumentCaptor.forClass(CustomResultSetExtractor.class);
verify(operations).query(anyString(), any(SqlParameterSource.class), captor.capture());
assertThat(captor.getValue()) //
.isInstanceOf(CustomResultSetExtractor.class) // not verified by the captor
.matches(crse -> crse.rowMapper != null, "RowMapper is not expected to be null");
}
@Test // DATAJDBC-263, DATAJDBC-354
public void publishesSingleEventWhenQueryReturnsSingleAggregate() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(false).when(queryMethod).isCollectionQuery();
doReturn(new DummyEntity(1L)).when(operations).queryForObject(anyString(), any(SqlParameterSource.class),
any(RowMapper.class));
doReturn(true).when(context).hasPersistentEntityFor(DummyEntity.class);
when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getIdentifier())
.thenReturn("some identifier");
new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter)
.execute(new Object[] {});
verify(publisher).publishEvent(any(AfterLoadEvent.class));
}
@Test // DATAJDBC-263, DATAJDBC-354
public void publishesAsManyEventsAsReturnedAggregates() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(true).when(queryMethod).isCollectionQuery();
doReturn(Arrays.asList(new DummyEntity(1L), new DummyEntity(1L))).when(operations).query(anyString(),
any(SqlParameterSource.class), any(RowMapper.class));
doReturn(true).when(context).hasPersistentEntityFor(DummyEntity.class);
when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getIdentifier())
.thenReturn("some identifier");
new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter)
.execute(new Object[] {});
verify(publisher, times(2)).publishEvent(any(AfterLoadEvent.class));
}
@Test // DATAJDBC-400
public void publishesCallbacks() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(false).when(queryMethod).isCollectionQuery();
DummyEntity dummyEntity = new DummyEntity(1L);
doReturn(dummyEntity).when(operations).queryForObject(anyString(), any(SqlParameterSource.class),
any(RowMapper.class));
doReturn(true).when(context).hasPersistentEntityFor(DummyEntity.class);
when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getIdentifier())
.thenReturn("some identifier");
new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter).execute(new Object[] {});
verify(publisher).publishEvent(any(AfterLoadEvent.class));
verify(callbacks).callback(AfterLoadCallback.class, dummyEntity);
}
/**
* The whole purpose of this method is to easily generate a {@link DefaultParameters} instance during test setup.
*/
@SuppressWarnings("unused")
private void dummyMethod() {}
private static class CustomRowMapper implements RowMapper<Object> {
@Override
public Object mapRow(ResultSet rs, int rowNum) {
return null;
}
}
private static class CustomResultSetExtractor implements ResultSetExtractor<Object> {
private final RowMapper rowMapper;
CustomResultSetExtractor() {
rowMapper = null;
}
public CustomResultSetExtractor(RowMapper rowMapper) {
this.rowMapper = rowMapper;
}
@Override
public Object extractData(ResultSet rs) throws DataAccessException {
return null;
}
}
private static class DummyEntity {
private Long id;
public DummyEntity(Long id) {
this.id = id;
}
Long getId() {
return id;
}
}
}

View File

@@ -68,10 +68,10 @@ public class TestConfiguration {
@Bean
JdbcRepositoryFactory jdbcRepositoryFactory(
@Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, RelationalMappingContext context,
JdbcConverter converter, Optional<NamedQueries> namedQueries) {
Dialect dialect, JdbcConverter converter, Optional<NamedQueries> namedQueries) {
JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, publisher,
namedParameterJdbcTemplate());
JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, dialect,
publisher, namedParameterJdbcTemplate());
namedQueries.ifPresent(factory::setNamedQueries);
return factory;
}
@@ -120,8 +120,7 @@ public class TestConfiguration {
relationResolver, //
conversions, //
new DefaultJdbcTypeFactory(template.getJdbcOperations()), //
dialect.getIdentifierProcessing()
);
dialect.getIdentifierProcessing());
}
@Bean

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.jdbc.testing;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.Assert;
/**
@@ -36,7 +37,14 @@ public interface TestUtils {
Assert.notNull(testClass, "Test class must not be null!");
Assert.hasText(databaseType, "Database type must not be null or empty!");
return String.format("%s/%s-%s.sql", testClass.getPackage().getName(), testClass.getSimpleName(),
String path = String.format("%s/%s-%s.sql", testClass.getPackage().getName(), testClass.getSimpleName(),
databaseType.toLowerCase());
ClassPathResource resource = new ClassPathResource(path);
if (!resource.exists()) {
throw new IllegalStateException("Test resource " + path + " not found");
}
return path;
}
}