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;
}
}

View File

@@ -645,7 +645,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#is(java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#is(java.lang.Object)
*/
@Override
public Criteria is(Object value) {
@@ -657,7 +657,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#not(java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#not(java.lang.Object)
*/
@Override
public Criteria not(Object value) {
@@ -669,7 +669,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#in(java.lang.Object[])
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#in(java.lang.Object[])
*/
@Override
public Criteria in(Object... values) {
@@ -687,7 +687,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#in(java.util.Collection)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#in(java.util.Collection)
*/
@Override
public Criteria in(Collection<?> values) {
@@ -700,7 +700,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notIn(java.lang.Object[])
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#notIn(java.lang.Object[])
*/
@Override
public Criteria notIn(Object... values) {
@@ -718,7 +718,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notIn(java.util.Collection)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#notIn(java.util.Collection)
*/
@Override
public Criteria notIn(Collection<?> values) {
@@ -731,7 +731,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#between(java.lang.Object, java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#between(java.lang.Object, java.lang.Object)
*/
@Override
public Criteria between(Object begin, Object end) {
@@ -744,7 +744,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notBetween(java.lang.Object, java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#notBetween(java.lang.Object, java.lang.Object)
*/
@Override
public Criteria notBetween(Object begin, Object end) {
@@ -757,7 +757,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#lessThan(java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#lessThan(java.lang.Object)
*/
@Override
public Criteria lessThan(Object value) {
@@ -769,7 +769,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#lessThanOrEquals(java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#lessThanOrEquals(java.lang.Object)
*/
@Override
public Criteria lessThanOrEquals(Object value) {
@@ -781,7 +781,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#greaterThan(java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#greaterThan(java.lang.Object)
*/
@Override
public Criteria greaterThan(Object value) {
@@ -793,7 +793,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#greaterThanOrEquals(java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#greaterThanOrEquals(java.lang.Object)
*/
@Override
public Criteria greaterThanOrEquals(Object value) {
@@ -805,7 +805,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#like(java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#like(java.lang.Object)
*/
@Override
public Criteria like(Object value) {
@@ -817,7 +817,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notLike(java.lang.Object)
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#notLike(java.lang.Object)
*/
@Override
public Criteria notLike(Object value) {
@@ -827,7 +827,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNull()
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#isNull()
*/
@Override
public Criteria isNull() {
@@ -836,7 +836,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNotNull()
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#isNotNull()
*/
@Override
public Criteria isNotNull() {
@@ -845,7 +845,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isTrue()
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#isTrue()
*/
@Override
public Criteria isTrue() {
@@ -854,7 +854,7 @@ public class Criteria implements CriteriaDefinition {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isFalse()
* @see org.springframework.data.relational.query.Criteria.CriteriaStep#isFalse()
*/
@Override
public Criteria isFalse() {

View File

@@ -24,6 +24,7 @@ import org.springframework.util.Assert;
* Simple factory to contain logic to create {@link Criteria}s from {@link Part}s.
*
* @author Roman Chigvintsev
* @author Mark Paluch
*/
class CriteriaFactory {
@@ -49,7 +50,7 @@ class CriteriaFactory {
public Criteria createCriteria(Part part) {
Part.Type type = part.getType();
String propertyName = part.getProperty().getSegment();
String propertyName = part.getProperty().toDotPath();
Class<?> propertyType = part.getProperty().getType();
Criteria.CriteriaStep criteriaStep = Criteria.where(propertyName);

View File

@@ -19,6 +19,8 @@ import java.util.Collection;
import java.util.Iterator;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.PartTree;
@@ -95,7 +97,7 @@ public abstract class RelationalQueryCreator<T> extends AbstractQueryCreator<T,
* @param tree
* @param parameters
*/
public static void validate(PartTree tree, RelationalParameters parameters) {
public static void validate(PartTree tree, Parameters<?, ?> parameters) {
int argCount = 0;
@@ -109,7 +111,7 @@ public abstract class RelationalQueryCreator<T> extends AbstractQueryCreator<T,
}
}
private static void throwExceptionOnArgumentMismatch(Part part, RelationalParameters parameters, int index) {
private static void throwExceptionOnArgumentMismatch(Part part, Parameters<?, ?> parameters, int index) {
Part.Type type = part.getType();
String property = part.getProperty().toDotPath();
@@ -121,7 +123,7 @@ public abstract class RelationalQueryCreator<T> extends AbstractQueryCreator<T,
throw new IllegalStateException(formattedMsg);
}
RelationalParameters.RelationalParameter parameter = parameters.getBindableParameter(index);
Parameter parameter = parameters.getBindableParameter(index);
if (expectsCollection(type) && !parameterIsCollectionLike(parameter)) {
String message = wrongParameterTypeMessage(property, type, "Collection", parameter);
throw new IllegalStateException(message);
@@ -135,16 +137,16 @@ public abstract class RelationalQueryCreator<T> extends AbstractQueryCreator<T,
return type == Part.Type.IN || type == Part.Type.NOT_IN;
}
private static boolean parameterIsCollectionLike(RelationalParameters.RelationalParameter parameter) {
private static boolean parameterIsCollectionLike(Parameter parameter) {
return parameter.getType().isArray() || Collection.class.isAssignableFrom(parameter.getType());
}
private static boolean parameterIsScalarLike(RelationalParameters.RelationalParameter parameter) {
private static boolean parameterIsScalarLike(Parameter parameter) {
return !Collection.class.isAssignableFrom(parameter.getType());
}
private static String wrongParameterTypeMessage(String property, Part.Type operatorType, String expectedArgumentType,
RelationalParameters.RelationalParameter parameter) {
Parameter parameter) {
return String.format("Operator %s on %s requires a %s argument, found %s", operatorType.name(), property,
expectedArgumentType, parameter.getType());
}

View File

@@ -1,5 +0,0 @@
[[faq]]
[appendix]
= Frequently Asked Questions
Sorry. We have no frequently asked questions so far.

View File

@@ -1,18 +1,19 @@
[[glossary]]
[appendix, glossary]
[appendix,glossary]
= Glossary
AOP::
Aspect-Oriented Programming
Aspect-Oriented Programming
CRUD::
Create, Read, Update, Delete - Basic persistence operations
Create, Read, Update, Delete - Basic persistence operations
Dependency Injection::
Pattern to hand a component's dependency to the component from outside, freeing the component to lookup the dependent itself. For more information, see link:$$https://en.wikipedia.org/wiki/Dependency_Injection$$[https://en.wikipedia.org/wiki/Dependency_Injection].
Pattern to hand a component's dependency to the component from outside, freeing the component to lookup the dependent itself.
For more information, see link:$$https://en.wikipedia.org/wiki/Dependency_Injection$$[https://en.wikipedia.org/wiki/Dependency_Injection].
JPA::
Java Persistence API
Java Persistence API
Spring::
Java application framework -- link:$$https://projects.spring.io/spring-framework$$[https://projects.spring.io/spring-framework]
Java application framework -- link:$$https://projects.spring.io/spring-framework$$[https://projects.spring.io/spring-framework]

View File

@@ -7,7 +7,7 @@ ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1
:spring-data-commons-docs: ../../../../../spring-data-commons/src/main/asciidoc
:spring-framework-docs: https://docs.spring.io/spring-framework/docs/{springVersion}/
(C) 2018-2019 The original authors.
(C) 2018-2020 The original authors.
NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
@@ -27,9 +27,9 @@ include::jdbc.adoc[leveloffset=+1]
= Appendix
:numbered!:
include::faq.adoc[leveloffset=+1]
include::glossary.adoc[leveloffset=+1]
include::{spring-data-commons-docs}/repository-namespace-reference.adoc[leveloffset=+1]
include::{spring-data-commons-docs}/repository-populator-namespace-reference.adoc[leveloffset=+1]
include::repository-query-keywords-reference.adoc[leveloffset=+1]
include::{spring-data-commons-docs}/repository-query-keywords-reference.adoc[leveloffset=+1]
include::{spring-data-commons-docs}/repository-query-return-types-reference.adoc[leveloffset=+1]

View File

@@ -63,6 +63,7 @@ You can overwrite the repository methods with implementations that match your st
[[jdbc.java-config]]
== Annotation-based Configuration
The Spring Data JDBC repositories support can be activated by an annotation through Java configuration, as the following example shows:
.Spring Data JDBC repositories using Java configuration
@@ -93,7 +94,8 @@ class ApplicationConfig extends AbstractJdbcConfiguration {
----
<1> `@EnableJdbcRepositories` creates implementations for interfaces derived from `Repository`
<2> `AbstractJdbcConfiguration` provides various default beans required by Spring Data JDBC
<3> Creates a `DataSource` connecting to a database. This is required by the following two bean methods.
<3> Creates a `DataSource` connecting to a database.
This is required by the following two bean methods.
<4> Creates the `NamedParameterJdbcOperations` used by Spring Data JDBC to access the database.
<5> Spring Data JDBC utilizes the transaction management provided by Spring JDBC.
====
@@ -116,7 +118,7 @@ By default the `AbstractJdbcConfiguration` tries to determine the database in us
This behavior can be changed by overwriting `jdbcDialect(NamedParameterJdbcOperations)`.
TIP: Dialects are resolved by [`JdbcDialectResolver`] from `JdbcOperations`, typically by inspecting `Connection`.
+ You can let Spring auto-discover your `Dialect` by registering a class that implements `org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider` through `META-INF/spring.factories`.
You can let Spring auto-discover your `Dialect` by registering a class that implements `org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider` through `META-INF/spring.factories`.
`DialectResolver` discovers dialect provider implementations from the class path using Spring's `SpringFactoriesLoader`.
[[jdbc.entity-persistence]]
@@ -153,7 +155,8 @@ The properties of the following types are currently supported:
* Anything your database driver accepts.
* References to other entities. They are considered a one-to-one relationship, or an embedded type.
* References to other entities.
They are considered a one-to-one relationship, or an embedded type.
It is optional for one-to-one relationship entities to have an `id` attribute.
The table of the referenced entity is expected to have an additional column named the same as the table of the referencing entity.
You can change this name by implementing `NamingStrategy.getReverseColumnName(PersistentPropertyPathExtension path)`.
@@ -180,14 +183,13 @@ This also means references are 1-1 or 1-n, but not n-1 or n-m.
If you have n-1 or n-m references, you are, by definition, dealing with two separate aggregates.
References between those should be encoded as simple `id` values, which should map properly with Spring Data JDBC.
[[jdbc.entity-persistence.custom-converters]]
=== Custom converters
Custom converters can be registered, for types that are not supported by default, by inheriting your configuration from `AbstractJdbcConfiguration` and overwriting the method `jdbcCustomConversions()`.
====
[source, java]
[source,java]
----
@Configuration
public class DataJdbcConfiguration extends AbstractJdbcConfiguration {
@@ -236,10 +238,11 @@ You can tweak that by providing a {javadoc-base}org/springframework/data/relatio
=== `Custom table names`
When the NamingStrategy does not matching on your database table names, you can customize the names with the {javadoc-base}org/springframework/data/relational/core/mapping/Table.html[`@Table`] annotation.
The element `value` of this annotation provides the custom table name. The following example maps the `MyEntity` class to the `CUSTOM_TABLE_NAME` table in the database:
The element `value` of this annotation provides the custom table name.
The following example maps the `MyEntity` class to the `CUSTOM_TABLE_NAME` table in the database:
====
[source, java]
[source,java]
----
@Table("CUSTOM_TABLE_NAME")
public class MyEntity {
@@ -259,7 +262,7 @@ The element `value` of this annotation provides the custom column name.
The following example maps the `name` property of the `MyEntity` class to the `CUSTOM_COLUMN_NAME` column in the database:
====
[source, java]
[source,java]
----
public class MyEntity {
@Id
@@ -272,12 +275,12 @@ public class MyEntity {
====
The {javadoc-base}org/springframework/data/relational/core/mapping/MappedCollection.html[`@MappedCollection`]
annotation can be used on a reference type (one-to-one relationship) or on Sets, Lists, and Maps (one-to-many relationship).
annotation can be used on a reference type (one-to-one relationship) or on Sets, Lists, and Maps (one-to-many relationship).
`idColumn` element of the annotation provides a custom name for the foreign key column referencing the id column in the other table.
In the following example the corresponding table for the `MySubEntity` class has a `NAME` column, and the `CUSTOM_MY_ENTITY_ID_COLUMN_NAME` column of the `MyEntity` id for relationship reasons:
====
[source, java]
[source,java]
----
public class MyEntity {
@Id
@@ -297,7 +300,7 @@ When using `List` and `Map` you must have an additional column for the position
This additional column name may be customized with the `keyColumn` Element of the {javadoc-base}org/springframework/data/relational/core/mapping/MappedCollection.html[`@MappedCollection`] annotation:
====
[source, java]
[source,java]
----
public class MyEntity {
@Id
@@ -325,7 +328,7 @@ Opposite to this behavior `USE_EMPTY` tries to create a new instance using eithe
.Sample Code of embedding objects
====
[source, java]
[source,java]
----
public class MyEntity {
@@ -340,7 +343,8 @@ public class EmbeddedEntity {
String name;
}
----
<1> ``Null``s `embeddedEntity` if `name` in `null`. Use `USE_EMPTY` to instantiate `embeddedEntity` with a potential `null` value for the `name` property.
<1> ``Null``s `embeddedEntity` if `name` in `null`.
Use `USE_EMPTY` to instantiate `embeddedEntity` with a potential `null` value for the `name` property.
====
If you need a value object multiple times in an entity, this can be achieved with the optional `prefix` element of the `@Embedded` annotation.
@@ -350,7 +354,7 @@ This element represents a prefix and is prepend for each column name in the embe
====
Make use of the shortcuts `@Embedded.Nullable` & `@Embedded.Empty` for `@Embedded(onEmpty = USE_NULL)` and `@Embedded(onEmpty = USE_EMPTY)` to reduce verbosity and simultaneously set JSR-305 `@javax.annotation.Nonnull` accordingly.
[source, java]
[source,java]
----
public class MyEntity {
@@ -397,12 +401,11 @@ Note that whether an entity is new is part of the entity's state.
With auto-increment columns, this happens automatically, because the ID gets set by Spring Data with the value from the ID column.
If you are not using auto-increment columns, you can use a `BeforeSave` listener, which sets the ID of the entity (covered later in this document).
[[jdbc.entity-persistence.optimistic-locking]]
=== Optimistic Locking
Spring Data JDBC supports optimistic locking by means of a numeric attribute that is annotated with
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/annotation/Version.html[`@Version`] on the aggregate root.
https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/annotation/Version.html[`@Version`] on the aggregate root.
Whenever Spring Data JDBC saves an aggregate with such a version attribute two things happen:
The update statement for the aggregate root will contain a where clause checking that the version stored in the database is actually unchanged.
If this isn't the case an `OptimisticLockingFailureException` will be thrown.
@@ -417,6 +420,129 @@ During deletes the version check also applies but no version is increased.
This section offers some specific information about the implementation and use of Spring Data JDBC.
Most of the data access operations you usually trigger on a repository result in a query being run against the databases.
Defining such a query is a matter of declaring a method on the repository interface, as the following example shows:
.PersonRepository with query methods
====
[source,java]
----
interface PersonRepository extends PagingAndSortingRepository<Person, String> {
List<Person> findByFirstname(String firstname); <1>
List<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); <2>
Person findByFirstnameAndLastname(String firstname, String lastname); <3>
Person findFirstByLastname(String lastname); <4>
@Query("SELECT * FROM person WHERE lastname = :lastname")
List<Person> findByLastname(String lastname); <5>
}
----
<1> The method shows a query for all people with the given `lastname`.
The query is derived by parsing the method name for constraints that can be concatenated with `And` and `Or`.
Thus, the method name results in a query expression of `SELECT … FROM person WHERE firstname = :firstname`.
<2> Use `Pageable` to pass offset and sorting parameters to the database.
<3> Find a single entity for the given criteria.
It completes with `IncorrectResultSizeDataAccessException` on non-unique results.
<4> Unless <3>, the first entity is always emitted even if the query yields more result documents.
<5> The `findByLastname` method shows a query for all people with the given last name.
====
The following table shows the keywords that are supported for query methods:
[cols="1,2,3",options="header",subs="quotes"]
.Supported keywords for query methods
|===
| Keyword
| Sample
| Logical result
| `After`
| `findByBirthdateAfter(Date date)`
| `birthdate > date`
| `GreaterThan`
| `findByAgeGreaterThan(int age)`
| `age > age`
| `GreaterThanEqual`
| `findByAgeGreaterThanEqual(int age)`
| `age >= age`
| `Before`
| `findByBirthdateBefore(Date date)`
| `birthdate < date`
| `LessThan`
| `findByAgeLessThan(int age)`
| `age < age`
| `LessThanEqual`
| `findByAgeLessThanEqual(int age)`
| `age <= age`
| `Between`
| `findByAgeBetween(int from, int to)`
| `age BETWEEN from AND to`
| `NotBetween`
| `findByAgeBetween(int from, int to)`
| `age NOT BETWEEN from AND to`
| `In`
| `findByAgeIn(Collection<Integer> ages)`
| `age IN (age1, age2, ageN)`
| `NotIn`
| `findByAgeNotIn(Collection ages)`
| `age NOT IN (age1, age2, ageN)`
| `IsNotNull`, `NotNull`
| `findByFirstnameNotNull()`
| `firstname IS NOT NULL`
| `IsNull`, `Null`
| `findByFirstnameNull()`
| `firstname IS NULL`
| `Like`, `StartingWith`, `EndingWith`
| `findByFirstnameLike(String name)`
| `firstname LIKE name`
| `NotLike`, `IsNotLike`
| `findByFirstnameNotLike(String name)`
| `firstname NOT LIKE name`
| `Containing` on String
| `findByFirstnameContaining(String name)`
| `firstname LIKE '%' name +'%'`
| `NotContaining` on String
| `findByFirstnameNotContaining(String name)`
| `firstname NOT LIKE '%' name +'%'`
| `(No keyword)`
| `findByFirstname(String name)`
| `firstname = name`
| `Not`
| `findByFirstnameNot(String name)`
| `firstname != name`
| `IsTrue`, `True`
| `findByActiveIsTrue()`
| `active IS TRUE`
| `IsFalse`, `False`
| `findByActiveIsFalse()`
| `active IS FALSE`
|===
NOTE: Query derivation is limited to properties that can be used in a `WHERE` clause without involving joins.
[[jdbc.query-methods.strategies]]
=== Query Lookup Strategies
@@ -430,7 +556,7 @@ The following example shows how to use `@Query` to declare a query method:
.Declare a query method by using @Query
====
[source, java]
[source,java]
----
public interface UserRepository extends CrudRepository<User, Long> {
@@ -465,20 +591,20 @@ Named queries are expected to be provided in the property file `META-INF/jdbc-na
The location of that file may be changed by setting a value to `@EnableJdbcRepositories.namedQueriesLocation`.
[[jdbc.query-methods.at-query.custom-rowmapper]]
==== Custom `RowMapper`
You can configure which `RowMapper` to use, either by using the `@Query(rowMapperClass = ....)` or by registering a `RowMapperMap` bean and registering a `RowMapper` per method return type. The following example shows how to register `RowMappers`:
You can configure which `RowMapper` to use, either by using the `@Query(rowMapperClass = ....)` or by registering a `RowMapperMap` bean and registering a `RowMapper` per method return type.
The following example shows how to register `DefaultQueryMappingConfiguration`:
====
[source,java]
----
@Bean
RowMapperMap rowMappers() {
return new ConfigurableRowMapperMap() //
.register(Person.class, new PersonRowMapper()) //
.register(Address.class, new AddressRowMapper());
QueryMappingConfiguration rowMappers() {
return new DefaultQueryMappingConfiguration()
.register(Person.class, new PersonRowMapper())
.register(Address.class, new AddressRowMapper());
}
----
====
@@ -488,7 +614,7 @@ When determining which `RowMapper` to use for a method, the following steps are
. If the type is a simple type, no `RowMapper` is used.
+
Instead, the query is expected to return a single row with a single column, and a conversion to the return type is applied to that value.
. The entity classes in the `RowMapperMap` are iterated until one is found that is a superclass or interface of the return type in question.
. The entity classes in the `QueryMappingConfiguration` are iterated until one is found that is a superclass or interface of the return type in question.
The `RowMapper` registered for that class is used.
+
Iterating happens in the order of registration, so make sure to register more general types after specific ones.
@@ -496,6 +622,7 @@ Iterating happens in the order of registration, so make sure to register more ge
If applicable, wrapper types such as collections or `Optional` are unwrapped.
Thus, a return type of `Optional<Person>` uses the `Person` type in the preceding process.
NOTE: Using a custom `RowMapper` through `QueryMappingConfiguration`, `@Query(rowMapperClass=…)`, or a custom `ResultSetExtractor` disables Entity Callbacks and Lifecycle Events as these components are under full control of the result mapping and can issue their own events/callbacks if needed.
[[jdbc.query-methods.at-query.modifying]]
==== Modifying Query
@@ -517,7 +644,6 @@ You can specify the following return types:
* `int` (updated record count)
* `boolean`(whether a record was updated)
[[jdbc.mybatis]]
== MyBatis Integration
@@ -529,7 +655,7 @@ This section describes how to configure Spring Data JDBC to integrate with MyBat
The easiest way to properly plug MyBatis into Spring Data JDBC is by importing `MyBatisJdbcConfiguration` into you application configuration:
[source, java]
[source,java]
----
@Configuration
@EnableJdbcRepositories
@@ -659,8 +785,8 @@ public ApplicationListener<BeforeSaveEvent<Object>> loggingSaves() {
----
====
If you want to handle events only for a specific domain type you may derive your listener from `AbstractRelationalEventListener` and overwrite one or more of the `onXXX` methods,
where `XXX` stands for an event type. Callback methods will only get invoked for events related to the domain type and their subtypes so you don't require further casting.
If you want to handle events only for a specific domain type you may derive your listener from `AbstractRelationalEventListener` and overwrite one or more of the `onXXX` methods, where `XXX` stands for an event type.
Callback methods will only get invoked for events related to the domain type and their subtypes so you don't require further casting.
====
[source,java]
@@ -743,13 +869,16 @@ Thus, if you want to inspect what SQL statements are executed, activate logging
[[jdbc.transactions]]
== Transactionality
CRUD methods on repository instances are transactional by default.
For reading operations, the transaction configuration `readOnly` flag is set to `true`. All others are configured with a plain `@Transactional` annotation so that default transaction configuration applies.
For details, see the Javadoc of link:{javadoc-base}org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.html[`SimpleJdbcRepository`]. If you need to tweak transaction configuration for one of the methods declared in a repository, redeclare the method in your repository interface, as follows:
For reading operations, the transaction configuration `readOnly` flag is set to `true`.
All others are configured with a plain `@Transactional` annotation so that default transaction configuration applies.
For details, see the Javadoc of link:{javadoc-base}org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.html[`SimpleJdbcRepository`].
If you need to tweak transaction configuration for one of the methods declared in a repository, redeclare the method in your repository interface, as follows:
.Custom transaction configuration for CRUD
====
[source, java]
[source,java]
----
public interface UserRepository extends CrudRepository<User, Long> {
@@ -764,11 +893,13 @@ public interface UserRepository extends CrudRepository<User, Long> {
The preceding causes the `findAll()` method to be executed with a timeout of 10 seconds and without the `readOnly` flag.
Another way to alter transactional behavior is by using a facade or service implementation that typically covers more than one repository. Its purpose is to define transactional boundaries for non-CRUD operations. The following example shows how to create such a facade:
Another way to alter transactional behavior is by using a facade or service implementation that typically covers more than one repository.
Its purpose is to define transactional boundaries for non-CRUD operations.
The following example shows how to create such a facade:
.Using a facade to define transactions for multiple repository calls
====
[source, java]
[source,java]
----
@Service
class UserManagementImpl implements UserManagement {
@@ -796,15 +927,19 @@ class UserManagementImpl implements UserManagement {
----
====
The preceding example causes calls to `addRoleToAllUsers(…)` to run inside a transaction (participating in an existing one or creating a new one if none are already running). The transaction configuration for the repositories is neglected, as the outer transaction configuration determines the actual repository to be used. Note that you have to explicitly activate `<tx:annotation-driven />` or use `@EnableTransactionManagement` to get annotation-based configuration for facades working. Note that the preceding example assumes you use component scanning.
The preceding example causes calls to `addRoleToAllUsers(…)` to run inside a transaction (participating in an existing one or creating a new one if none are already running).
The transaction configuration for the repositories is neglected, as the outer transaction configuration determines the actual repository to be used.
Note that you have to explicitly activate `<tx:annotation-driven />` or use `@EnableTransactionManagement` to get annotation-based configuration for facades working.
Note that the preceding example assumes you use component scanning.
[[jdbc.transaction.query-methods]]
=== Transactional Query Methods
To let your query methods be transactional, use `@Transactional` at the repository interface you define, as the following example shows:
.Using @Transactional at query methods
====
[source, java]
[source,java]
----
@Transactional(readOnly = true)
public interface UserRepository extends CrudRepository<User, Long> {
@@ -819,9 +954,13 @@ public interface UserRepository extends CrudRepository<User, Long> {
----
====
Typically, you want the `readOnly` flag to be set to true, because most of the query methods only read data. In contrast to that, `deleteInactiveUsers()` uses the `@Modifying` annotation and overrides the transaction configuration. Thus, the method is with the `readOnly` flag set to `false`.
Typically, you want the `readOnly` flag to be set to true, because most of the query methods only read data.
In contrast to that, `deleteInactiveUsers()` uses the `@Modifying` annotation and overrides the transaction configuration.
Thus, the method is with the `readOnly` flag set to `false`.
NOTE: It is definitely reasonable to use transactions for read-only queries, and we can mark them as such by setting the `readOnly` flag. This does not, however, act as a check that you do not trigger a manipulating query (although some databases reject `INSERT` and `UPDATE` statements inside a read-only transaction). Instead, the `readOnly` flag is propagated as a hint to the underlying JDBC driver for performance optimizations.
NOTE: It is definitely reasonable to use transactions for read-only queries, and we can mark them as such by setting the `readOnly` flag.
This does not, however, act as a check that you do not trigger a manipulating query (although some databases reject `INSERT` and `UPDATE` statements inside a read-only transaction).
Instead, the `readOnly` flag is propagated as a hint to the underlying JDBC driver for performance optimizations.
include::{spring-data-commons-docs}/auditing.adoc[leveloffset=+1]
@@ -832,7 +971,7 @@ In order to activate auditing, add `@EnableJdbcAuditing` to your configuration,
.Activating auditing with Java configuration
====
[source, java]
[source,java]
----
@Configuration
@EnableJdbcAuditing
@@ -846,4 +985,5 @@ class Config {
----
====
If you expose a bean of type `AuditorAware` to the `ApplicationContext`, the auditing infrastructure automatically picks it up and uses it to determine the current user to be set on domain types. If you have multiple implementations registered in the `ApplicationContext`, you can select the one to be used by explicitly setting the `auditorAwareRef` attribute of `@EnableJdbcAuditing`.
If you expose a bean of type `AuditorAware` to the `ApplicationContext`, the auditing infrastructure automatically picks it up and uses it to determine the current user to be set on domain types.
If you have multiple implementations registered in the `ApplicationContext`, you can select the one to be used by explicitly setting the `auditorAwareRef` attribute of `@EnableJdbcAuditing`.

View File

@@ -8,6 +8,7 @@ This section covers the significant changes for each version.
* Optimistic Locking support.
* Support for `PagingAndSortingRepository`.
* <<jdbc.query-methods,Query Derivation>>.
* Full Support for H2.
* All SQL identifiers know get quoted by default.
* Missing columns no longer cause exceptions.

View File

@@ -1,7 +0,0 @@
[[repository-query-keywords]]
[appendix]
= Repository query keywords
== Supported query keywords
Spring Data JDBC does not support query derivation yet.