diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java new file mode 100644 index 00000000..cac47ce5 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java @@ -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 extractor, RowMapper 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 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 singleObjectQuery(RowMapper rowMapper) { + + return (query, parameters) -> { + try { + return operations.queryForObject(query, parameters, rowMapper); + } catch (EmptyResultDataAccessException e) { + return null; + } + }; + } + + private JdbcQueryExecution> collectionQuery(RowMapper rowMapper) { + return getQueryExecution(new RowMapperResultSetExtractor<>(rowMapper)); + } + + private JdbcQueryExecution getQueryExecution(ResultSetExtractor resultSetExtractor) { + return (query, parameters) -> operations.query(query, parameters, resultSetExtractor); + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java new file mode 100644 index 00000000..7695885a --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -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 { + + private final PartTree tree; + private final RelationalParameterAccessor accessor; + private final QueryMapper queryMapper; + + private final MappingContext, 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 columnNames = unwrapColumnNames("", tableEntity); + + return columnNames.toArray(new SqlIdentifier[0]); + } + + private Collection unwrapColumnNames(String prefix, RelationalPersistentEntity persistentEntity) { + + Collection 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; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java new file mode 100644 index 00000000..227f9608 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java @@ -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 { + + /** + * Execute the given {@code query}. + * + * @param query + * @param parameter + * @return + */ + @Nullable + T execute(String query, SqlParameterSource parameter); +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java new file mode 100644 index 00000000..d62f6534 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java @@ -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 RelationalPersistentProperty> mappingContext; + private final Map, Optional> 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 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) 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) 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 getRowMapperClass() { + return getMergedAnnotationAttribute("rowMapperClass"); + } + + /** + * Returns the class to be used as {@link org.springframework.jdbc.core.ResultSetExtractor} + * + * @return May be {@code null}. + */ + @Nullable + Class 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 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 findAnnotatedQuery() { + + return lookupQueryAnnotation() // + .map(Query::value) // + .filter(StringUtils::hasText); + } + + Optional lookupQueryAnnotation() { + return doFindAnnotation(Query.class); + } + + @SuppressWarnings("unchecked") + private Optional doFindAnnotation(Class annotationType) { + + return (Optional) this.annotationCache.computeIfAbsent(annotationType, + it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, it))); + } + +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java new file mode 100644 index 00000000..b72d807e --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java @@ -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; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java new file mode 100644 index 00000000..3ad0c862 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java @@ -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 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)); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java new file mode 100644 index 00000000..65eb0925 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java @@ -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, 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 getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity entity) { + + List 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 arguments = function.getExpressions(); + List 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 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 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 valueFunction = (ValueFunction) 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 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 pair = (Pair) 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 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, 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 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 pair = (Pair) 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, 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 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, RelationalPersistentProperty> mappingContext; + private final RelationalPersistentProperty property; + private final @Nullable PersistentPropertyPath 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, 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, 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 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 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; + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java new file mode 100644 index 00000000..e6b6b456 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -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 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 determineResultSetExtractor(@Nullable RowMapper rowMapper) { + + Class resultSetExtractorClass = queryMethod.getResultSetExtractorClass(); + + if (isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class)) { + return null; + } + + Constructor constructor = ClassUtils + .getConstructorIfAvailable(resultSetExtractorClass, RowMapper.class); + + if (constructor != null) { + return BeanUtils.instantiateClass(constructor, rowMapper); + } + + return BeanUtils.instantiateClass(resultSetExtractorClass); + } + + @SuppressWarnings("unchecked") + RowMapper determineRowMapper(@Nullable RowMapper defaultMapper) { + + Class rowMapperClass = queryMethod.getRowMapperClass(); + + if (isUnconfigured(rowMapperClass, RowMapper.class)) { + return (RowMapper) defaultMapper; + } + + return (RowMapper) BeanUtils.instantiateClass(rowMapperClass); + } + + private static boolean isUnconfigured(@Nullable Class configuredClass, Class defaultClass) { + return configuredClass == null || configuredClass == defaultClass; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/package-info.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/package-info.java index a46db80e..96d41b6b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/package-info.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/package-info.java @@ -1,3 +1,6 @@ +/** + * Query derivation mechanism for JDBC specific repositories. + */ @NonNullApi package org.springframework.data.jdbc.repository.query; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index 0ea262ef..f48dd174 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -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 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 implements RowMapper { + + private final RowMapper delegate; + + PostProcessingRowMapper(RowMapper 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; + } } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java deleted file mode 100644 index 960103fa..00000000 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java +++ /dev/null @@ -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 getRowMapperClass() { - return getMergedAnnotationAttribute("rowMapperClass"); - } - - /** - * Returns the class to be used as {@link org.springframework.jdbc.core.ResultSetExtractor} - * - * @return May be {@code null}. - */ - @Nullable - Class 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 getMergedAnnotationAttribute(String attribute) { - - Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); - return (T) AnnotationUtils.getValue(queryAnnotation, attribute); - } -} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index 74a3fac8..ce8786ba 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -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 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)); } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java index d7b591b3..8d17f38b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java @@ -86,7 +86,7 @@ public class JdbcRepositoryFactoryBean, 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); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java deleted file mode 100644 index a5000007..00000000 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java +++ /dev/null @@ -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 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 rowMapper = determineRowMapper(defaultRowMapper); - executor = createExecutor( // - queryMethod, // - determineResultSetExtractor(rowMapper != defaultRowMapper ? rowMapper : null), // - rowMapper // - ); - - this.converter = converter; - } - - private QueryExecutor createExecutor(JdbcQueryMethod queryMethod, - @Nullable ResultSetExtractor extractor, RowMapper rowMapper) { - - String query = determineQuery(); - - if (queryMethod.isModifyingQuery()) { - return createModifyingQueryExecutor(query); - } - if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) { - QueryExecutor innerExecutor = extractor != null ? createResultSetExtractorQueryExecutor(query, extractor) - : createListRowMapperQueryExecutor(query, rowMapper); - return createCollectionQueryExecutor(innerExecutor); - } - - QueryExecutor 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 createObjectQueryExecutor(QueryExecutor executor) { - - return parameters -> { - - try { - - Object result = executor.execute(parameters); - - publishAfterLoad(result); - - return result; - - } catch (EmptyResultDataAccessException e) { - return null; - } - }; - } - - private QueryExecutor createCollectionQueryExecutor(QueryExecutor 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 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 createListRowMapperQueryExecutor(String query, RowMapper rowMapper) { - return parameters -> operations.query(query, parameters, rowMapper); - } - - private QueryExecutor createObjectRowMapperQueryExecutor(String query, RowMapper rowMapper) { - return parameters -> operations.queryForObject(query, parameters, rowMapper); - } - - private QueryExecutor 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 determineResultSetExtractor(@Nullable RowMapper rowMapper) { - - Class resultSetExtractorClass = queryMethod.getResultSetExtractorClass(); - - if (isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class)) { - return null; - } - - Constructor constructor = ClassUtils - .getConstructorIfAvailable(resultSetExtractorClass, RowMapper.class); - - if (constructor != null) { - return BeanUtils.instantiateClass(constructor, rowMapper); - } - - return BeanUtils.instantiateClass(resultSetExtractorClass); - } - - @SuppressWarnings("unchecked") - private RowMapper determineRowMapper(RowMapper defaultMapper) { - - Class rowMapperClass = queryMethod.getRowMapperClass(); - - if (isUnconfigured(rowMapperClass, RowMapper.class)) { - return (RowMapper) defaultMapper; - } - - return (RowMapper) BeanUtils.instantiateClass(rowMapperClass); - } - - private static boolean isUnconfigured(@Nullable Class configuredClass, Class defaultClass) { - return configuredClass == null || configuredClass == defaultClass; - } - - private void publishAfterLoad(Iterable all) { - - for (T e : all) { - publishAfterLoad(e); - } - } - - private 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 { - @Nullable - T execute(MapSqlParameterSource parameter); - } -} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 0bb48460..392d42af 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -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> { + + private List> 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 findAllByNamedQuery(); + List findAllByPointInTimeAfter(Instant instant); + + @Query("SELECT * FROM DUMMY_ENTITY") + List findAllWithSql(); + + @Query(value = "SELECT * FROM DUMMY_ENTITY", rowMapperClass = CustomRowMapper.class) + List findAllWithCustomMapper(); + @Query("SELECT * FROM DUMMY_ENTITY WHERE POINT_IN_TIME > :threshhold") - List after(@Param("threshhold")Instant threshhold); + List 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 { + + @Override + public DummyEntity mapRow(ResultSet rs, int rowNum) { + return new DummyEntity(); + } + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index 79086e77..42bf4811 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -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); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java similarity index 96% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java index d13a3a8a..476888c4 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java @@ -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 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethodUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java similarity index 94% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethodUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java index 5072a79f..26792735 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethodUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethodUnitTests.java @@ -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 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java new file mode 100644 index 00000000..90b7330d --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -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 { + + List findAllByFirstName(String firstName); + + List findAllByLastNameAndFirstName(String lastName, String firstName); + + List findAllByLastNameOrFirstName(String lastName, String firstName); + + Boolean existsByFirstName(String firstName); + + List findAllByDateOfBirthBetween(Date from, Date to); + + List findAllByAgeLessThan(Integer age); + + List findAllByAgeLessThanEqual(Integer age); + + List findAllByAgeGreaterThan(Integer age); + + List findAllByAgeGreaterThanEqual(Integer age); + + List findAllByDateOfBirthAfter(Date date); + + List findAllByDateOfBirthBefore(Date date); + + List findAllByAgeIsNull(); + + List findAllByAgeIsNotNull(); + + List findAllByFirstNameLike(String like); + + List findAllByFirstNameNotLike(String like); + + List findAllByFirstNameStartingWith(String starting); + + List findAllByFirstNameEndingWith(String ending); + + List findAllByFirstNameContaining(String containing); + + List findAllByFirstNameNotContaining(String notContaining); + + List findAllByAgeOrderByLastNameAsc(Integer age); + + List findAllByAgeOrderByLastNameDesc(Integer age); + + List findAllByLastNameNot(String lastName); + + List findAllByAgeIn(Collection ages); + + List findAllByAgeNotIn(Collection ages); + + List findAllByActiveTrue(); + + List findAllByActiveFalse(); + + List findAllByFirstNameIgnoreCase(String firstName); + + User findByIdIgnoringCase(Long id); + + List findAllByIdIsEmpty(); + + List 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; + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryMapperUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryMapperUnitTests.java new file mode 100644 index 00000000..658e55dc --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryMapperUnitTests.java @@ -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 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; + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java new file mode 100644 index 00000000..bf7f952b --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -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 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 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 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 { + + @Override + public Object mapRow(ResultSet rs, int rowNum) { + return null; + } + } + + private static class CustomResultSetExtractor implements ResultSetExtractor { + + 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; + } + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java index fd9e32c8..09a4b32a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java @@ -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); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java deleted file mode 100644 index 66156dfc..00000000 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java +++ /dev/null @@ -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 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 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 { - - @Override - public Object mapRow(ResultSet rs, int rowNum) { - return null; - } - } - - private static class CustomResultSetExtractor implements ResultSetExtractor { - - 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; - } - } -} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 75ac9274..6e69246f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -68,10 +68,10 @@ public class TestConfiguration { @Bean JdbcRepositoryFactory jdbcRepositoryFactory( @Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, - JdbcConverter converter, Optional namedQueries) { + Dialect dialect, JdbcConverter converter, Optional 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 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestUtils.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestUtils.java index 46de9be1..aa640a94 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestUtils.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestUtils.java @@ -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; } } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-h2.sql similarity index 100% rename from spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-h2.sql rename to spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-h2.sql diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-hsql.sql similarity index 100% rename from spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-hsql.sql rename to spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-hsql.sql diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-mariadb.sql similarity index 100% rename from spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-mariadb.sql rename to spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-mariadb.sql diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-mssql.sql similarity index 100% rename from spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-mssql.sql rename to spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-mssql.sql diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-mysql.sql similarity index 100% rename from spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-mysql.sql rename to spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-mysql.sql diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-postgres.sql similarity index 100% rename from spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryQueryMappingConfigurationIntegrationTests-postgres.sql rename to spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-postgres.sql diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java index 19a4d15f..ec1fa9e9 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java @@ -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() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java index 708162dc..00eadcd7 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java @@ -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); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java index 79206b3a..4db4410d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java @@ -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 extends AbstractQueryCreator parameters) { int argCount = 0; @@ -109,7 +111,7 @@ public abstract class RelationalQueryCreator extends AbstractQueryCreator parameters, int index) { Part.Type type = part.getType(); String property = part.getProperty().toDotPath(); @@ -121,7 +123,7 @@ public abstract class RelationalQueryCreator extends AbstractQueryCreator extends AbstractQueryCreator `@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 { + + List findByFirstname(String firstname); <1> + + List 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 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 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 { @@ -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` 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> 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 { @@ -764,11 +893,13 @@ public interface UserRepository extends CrudRepository { 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 `` 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 `` 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 { @@ -819,9 +954,13 @@ public interface UserRepository extends CrudRepository { ---- ==== -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`. diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index d50f9237..6c5dee91 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -8,6 +8,7 @@ This section covers the significant changes for each version. * Optimistic Locking support. * Support for `PagingAndSortingRepository`. +* <>. * Full Support for H2. * All SQL identifiers know get quoted by default. * Missing columns no longer cause exceptions. diff --git a/src/main/asciidoc/repository-query-keywords-reference.adoc b/src/main/asciidoc/repository-query-keywords-reference.adoc deleted file mode 100644 index f5a0a16a..00000000 --- a/src/main/asciidoc/repository-query-keywords-reference.adoc +++ /dev/null @@ -1,7 +0,0 @@ -[[repository-query-keywords]] -[appendix] -= Repository query keywords - -== Supported query keywords - -Spring Data JDBC does not support query derivation yet.