diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 576e6bc5..0df402a5 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -59,6 +59,7 @@ import org.springframework.util.Assert; * @author Milan Milanov * @author Myeonghyeon Lee * @author Chirag Tailor + * @author Christopher Klein */ public class MyBatisDataAccessStrategy implements DataAccessStrategy { @@ -286,7 +287,6 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { return sqlSession().selectList(statementName, new MyBatisContext(identifier, null, path.getRequiredLeafProperty().getType())); - } @Override 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 index 12ce4172..f887c58a 100644 --- 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 @@ -29,6 +29,9 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcValue; +import org.springframework.data.jdbc.repository.query.parameter.ParameterBindingParser; +import org.springframework.data.jdbc.repository.query.parameter.ParameterBindings.Metadata; +import org.springframework.data.jdbc.repository.query.parameter.ParameterBindings.ParameterBinding; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; @@ -36,11 +39,18 @@ import org.springframework.data.relational.repository.query.RelationalParameters import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; 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.jdbc.core.namedparam.SqlParameterSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -57,6 +67,7 @@ import org.springframework.util.ObjectUtils; * @author Mark Paluch * @author Hebert Coelho * @author Chirag Tailor + * @author Christopher Klein * @since 2.0 */ public class StringBasedJdbcQuery extends AbstractJdbcQuery { @@ -67,6 +78,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private final JdbcConverter converter; private final RowMapperFactory rowMapperFactory; private BeanFactory beanFactory; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; /** * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} @@ -77,8 +89,9 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { * @param defaultRowMapper can be {@literal null} (only in case of a modifying query). */ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - @Nullable RowMapper defaultRowMapper, JdbcConverter converter) { - this(queryMethod, operations, result -> (RowMapper) defaultRowMapper, converter); + @Nullable RowMapper defaultRowMapper, JdbcConverter converter, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + this(queryMethod, operations, result -> (RowMapper) defaultRowMapper, converter, evaluationContextProvider); } /** @@ -91,7 +104,8 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { * @since 2.3 */ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter) { + RowMapperFactory rowMapperFactory, JdbcConverter converter, + QueryMethodEvaluationContextProvider evaluationContextProvider) { super(queryMethod, operations); @@ -100,6 +114,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { this.queryMethod = queryMethod; this.converter = converter; this.rowMapperFactory = rowMapperFactory; + this.evaluationContextProvider = evaluationContextProvider; if (queryMethod.isSliceQuery()) { throw new UnsupportedOperationException( @@ -115,6 +130,16 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { @Override public Object execute(Object[] objects) { +// List parameterBindings = new ArrayList<>(); +// SpelQueryContext queryContext = SpelQueryContext.of((counter, expression) -> { +// +// String parameterName = String.format("__synthetic_%d__", counter); +// parameterBindings.add(new ParameterBinding(parameterName, expression)); +// return parameterName; +// }, String::concat); +// +// SpelQueryContext.SpelExtractor parsed = queryContext.parse(query); + RelationalParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(), objects); ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); ResultProcessingConverter converter = new ResultProcessingConverter(processor, this.converter.getMappingContext(), @@ -128,7 +153,51 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { determineResultSetExtractor(rowMapper), // rowMapper); - return queryExecution.execute(determineQuery(), this.bindParameters(accessor)); + Metadata queryMeta = new Metadata(); + + String query = determineQuery(); + + if (ObjectUtils.isEmpty(query)) { + throw new IllegalStateException(String.format("No query specified on %s", queryMethod.getName())); + } + List bindings = new ArrayList<>(); + + query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, + bindings, queryMeta); + + SqlParameterSource parameterMap = this.bindParameters(accessor); + extendParametersFromSpELEvaluation((MapSqlParameterSource) parameterMap, bindings, objects); + return queryExecution.execute(query, parameterMap); + } + + /** + * Extend the {@link MapSqlParameterSource} by evaluating each detected SpEL parameter in the original query. This is + * basically a simple variant of Spring Data JPA's SPeL implementation. + * + * @param parameterMap + * @param bindings + * @param values + */ + void extendParametersFromSpELEvaluation(MapSqlParameterSource parameterMap, List bindings, + Object[] values) { + + if (bindings.size() == 0) { + return; + } + + ExpressionParser parser = new SpelExpressionParser(); + + bindings.forEach(binding -> { + if (!binding.isExpression()) { + return; + } + + Expression expression = parser.parseExpression(binding.getExpression()); + EvaluationContext context = evaluationContextProvider.getEvaluationContext(this.queryMethod.getParameters(), + values); + + parameterMap.addValue(binding.getName(), expression.getValue(context, Object.class)); + }); } @Override diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/parameter/ParameterBindingParser.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/parameter/ParameterBindingParser.java new file mode 100755 index 00000000..a96eaa52 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/parameter/ParameterBindingParser.java @@ -0,0 +1,295 @@ +package org.springframework.data.jdbc.repository.query.parameter; + +import static java.util.regex.Pattern.CASE_INSENSITIVE; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.data.jdbc.repository.query.parameter.ParameterBindings.InParameterBinding; +import org.springframework.data.jdbc.repository.query.parameter.ParameterBindings.LikeParameterBinding; +import org.springframework.data.jdbc.repository.query.parameter.ParameterBindings.Metadata; +import org.springframework.data.jdbc.repository.query.parameter.ParameterBindings.ParameterBinding; +import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A parser that extracts the parameter bindings from a given query string. + * + * TODO This class comes from Spring Data JPA org.springframework.data.jpa.repository.query.StringQuery and should be probably moved to Spring Data Commons. + * + * @author Thomas Darimont + * @author Christopher Klein + */ +public enum ParameterBindingParser { + + INSTANCE; + + private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__"; + public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))"; + // .....................................................................^ not followed by a hash or a letter. + // .................................................................^ zero or more digits. + // .............................................................^ start with a question mark. + private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER); + private static final Pattern PARAMETER_BINDING_PATTERN; + private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type! " + + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same binding."; + private static final int INDEXED_PARAMETER_GROUP = 4; + private static final int NAMED_PARAMETER_GROUP = 6; + private static final int COMPARISION_TYPE_GROUP = 1; + + public static final String IDENTIFIER = "[._$[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{Punct}]]+"; + public static final String COLON_NO_DOUBLE_COLON = "(? keywords = new ArrayList<>(); + + for (ParameterBindingType type : ParameterBindingType.values()) { + if (type.getKeyword() != null) { + keywords.add(type.getKeyword()); + } + } + + StringBuilder builder = new StringBuilder(); + builder.append("("); + builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords + builder.append(")?"); + builder.append("(?: )?"); // some whitespace + builder.append("\\(?"); // optional braces around parameters + builder.append("("); + builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index + builder.append("|"); // or + + // named parameter and the parameter name + builder.append("%?(" + COLON_NO_DOUBLE_COLON + IDENTIFIER_GROUP + ")%?"); + + builder.append(")"); + builder.append("\\)?"); // optional braces around parameters + + PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); + } + + /** + * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns + * the cleaned up query. + */ + public String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, + List bindings, Metadata queryMeta) { + + int greatestParameterIndex = tryFindGreatestParameterIndexIn(query); + boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1; + + /* + * Prefer indexed access over named parameters if only SpEL Expression parameters are present. + */ + if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) { + parametersShouldBeAccessedByIndex = true; + greatestParameterIndex = 0; + } + + SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIndex, + greatestParameterIndex); + + String resultingQuery = spelExtractor.getQueryString(); + Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery); + + int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; + + boolean usesJpaStyleParameters = false; + while (matcher.find()) { + + if (spelExtractor.isQuoted(matcher.start())) { + continue; + } + + String parameterIndexString = matcher.group(INDEXED_PARAMETER_GROUP); + String parameterName = parameterIndexString != null ? null : matcher.group(NAMED_PARAMETER_GROUP); + Integer parameterIndex = getParameterIndex(parameterIndexString); + + String typeSource = matcher.group(COMPARISION_TYPE_GROUP); + String expression = spelExtractor.getParameter(parameterName == null ? parameterIndexString : parameterName); + String replacement = null; + + Assert.isTrue(parameterIndexString != null || parameterName != null, () -> String.format("We need either a name or an index! Offending query string: %s", query)); + + expressionParameterIndex++; + if ("".equals(parameterIndexString)) { + + queryMeta.setUsesJdbcStyleParameters(true); + parameterIndex = expressionParameterIndex; + } else { + usesJpaStyleParameters = true; + } + + if (usesJpaStyleParameters && queryMeta.isUsesJdbcStyleParameters()) { + throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported!"); + } + + switch (ParameterBindingType.of(typeSource)) { + + case LIKE: + + Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); + replacement = matcher.group(3); + + if (parameterIndex != null) { + checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings); + } else { + checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings); + + replacement = expression != null ? ":" + parameterName : matcher.group(5); + } + + break; + + case IN: + + if (parameterIndex != null) { + checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings); + } else { + checkAndRegister(new InParameterBinding(parameterName, expression), bindings); + } + + break; + + case AS_IS: // fall-through we don't need a special parameter binding for the given parameter. + default: + + bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression) + : new ParameterBinding(parameterName, null, expression)); + } + + if (replacement != null) { + resultingQuery = replaceFirst(resultingQuery, matcher.group(2), replacement); + } + + } + + return resultingQuery; + } + + private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean parametersShouldBeAccessedByIndex, + int greatestParameterIndex) { + + /* + * If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to + * not mix-up with the actual parameter indices. + */ + int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; + + BiFunction indexToParameterName = parametersShouldBeAccessedByIndex + ? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1) + : (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1); + + String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":"; + + BiFunction parameterNameToReplacement = (prefix, name) -> fixedPrefix + name; + + return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel); + } + + private static String replaceFirst(String text, String substring, String replacement) { + + int index = text.indexOf(substring); + if (index < 0) { + return text; + } + + return text.substring(0, index) + replacement + text.substring(index + substring.length()); + } + + @Nullable + private static Integer getParameterIndex(@Nullable String parameterIndexString) { + + if (parameterIndexString == null || parameterIndexString.isEmpty()) { + return null; + } + return Integer.valueOf(parameterIndexString); + } + + private static int tryFindGreatestParameterIndexIn(String query) { + + Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query); + + int greatestParameterIndex = -1; + while (parameterIndexMatcher.find()) { + + String parameterIndexString = parameterIndexMatcher.group(1); + Integer parameterIndex = getParameterIndex(parameterIndexString); + if (parameterIndex != null) { + greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex); + } + } + + return greatestParameterIndex; + } + + private static void checkAndRegister(ParameterBinding binding, List bindings) { + + bindings.stream() // + .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) // + .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding))); + + if (!bindings.contains(binding)) { + bindings.add(binding); + } + } + + /** + * An enum for the different types of bindings. + * + * @author Thomas Darimont + * @author Oliver Gierke + */ + private enum ParameterBindingType { + + // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace + // character, while = does not. + LIKE("like "), IN("in "), AS_IS(null); + + private final @Nullable String keyword; + + ParameterBindingType(@Nullable String keyword) { + this.keyword = keyword; + } + + /** + * Returns the keyword that will trigger the binding type or {@literal null} if the type is not triggered by a + * keyword. + * + * @return the keyword + */ + @Nullable + public String getKeyword() { + return keyword; + } + + /** + * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal #AS_IS} in + * case no other {@link ParameterBindingType} could be found. + */ + static ParameterBindingType of(String typeSource) { + + if (!StringUtils.hasText(typeSource)) { + return AS_IS; + } + + for (ParameterBindingType type : values()) { + if (type.name().equalsIgnoreCase(typeSource.trim())) { + return type; + } + } + + throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource)); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/parameter/ParameterBindings.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/parameter/ParameterBindings.java new file mode 100755 index 00000000..07e58db5 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/parameter/ParameterBindings.java @@ -0,0 +1,442 @@ +package org.springframework.data.jdbc.repository.query.parameter; + +import static org.springframework.util.ObjectUtils.nullSafeEquals; +import static org.springframework.util.ObjectUtils.nullSafeHashCode; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * TODO This class comes from Spring Data JPA org.springframework.data.jpa.repository.query.StringQuery and should be probably moved to Spring Data Commons. + * @author Christopher Klein + */ +public class ParameterBindings { + + /** + * A generic parameter binding with name or position information. + * + * @author Thomas Darimont + */ + public static class ParameterBinding { + + private final @Nullable String name; + private final @Nullable String expression; + private final @Nullable Integer position; + + /** + * Creates a new {@link ParameterBinding} for the parameter with the given + * position. + * + * @param position must not be {@literal null}. + */ + ParameterBinding(Integer position) { + this(null, position, null); + } + + /** + * Creates a new {@link ParameterBinding} for the parameter with the given name, + * position and expression information. Either {@literal name} or + * {@literal position} must be not {@literal null}. + * + * @param name of the parameter may be {@literal null}. + * @param position of the parameter may be {@literal null}. + * @param expression the expression to apply to any value for this parameter. + */ + ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) { + + if (name == null) { + Assert.notNull(position, "Position must not be null!"); + } + + if (position == null) { + Assert.notNull(name, "Name must not be null!"); + } + + this.name = name; + this.position = position; + this.expression = expression; + } + + /** + * Returns whether the binding has the given name. Will always be + * {@literal false} in case the {@link ParameterBinding} has been set up from a + * position. + */ + boolean hasName(@Nullable String name) { + return this.position == null && this.name != null && this.name.equals(name); + } + + /** + * Returns whether the binding has the given position. Will always be + * {@literal false} in case the {@link ParameterBinding} has been set up from a + * name. + */ + boolean hasPosition(@Nullable Integer position) { + return position != null && this.name == null && position.equals(this.position); + } + + /** + * @return the name + */ + @Nullable + public String getName() { + return name; + } + + /** + * @return the name + * @throws IllegalStateException if the name is not available. + * @since 2.0 + */ + String getRequiredName() throws IllegalStateException { + + String name = getName(); + + if (name != null) { + return name; + } + + throw new IllegalStateException(String.format("Required name for %s not available!", this)); + } + + /** + * @return the position + */ + @Nullable + Integer getPosition() { + return position; + } + + /** + * @return the position + * @throws IllegalStateException if the position is not available. + * @since 2.0 + */ + int getRequiredPosition() throws IllegalStateException { + + Integer position = getPosition(); + + if (position != null) { + return position; + } + + throw new IllegalStateException(String.format("Required position for %s not available!", this)); + } + + /** + * @return {@literal true} if this parameter binding is a synthetic SpEL + * expression. + */ + public boolean isExpression() { + return this.expression != null; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + + result += nullSafeHashCode(this.name); + result += nullSafeHashCode(this.position); + result += nullSafeHashCode(this.expression); + + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (!(obj instanceof ParameterBinding)) { + return false; + } + + ParameterBinding that = (ParameterBinding) obj; + + return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position) + && nullSafeEquals(this.expression, that.expression); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(), + getExpression()); + } + + /** + * @param valueToBind value to prepare + */ + @Nullable + public Object prepare(@Nullable Object valueToBind) { + return valueToBind; + } + + @Nullable + public String getExpression() { + return expression; + } + } + + /** + * Represents a {@link ParameterBinding} in a JPQL query augmented with + * instructions of how to apply a parameter as an {@code IN} parameter. + * + * @author Thomas Darimont + */ + public static class InParameterBinding extends ParameterBinding { + + /** + * Creates a new {@link InParameterBinding} for the parameter with the given + * name. + */ + InParameterBinding(String name, @Nullable String expression) { + super(name, null, expression); + } + + /** + * Creates a new {@link InParameterBinding} for the parameter with the given + * position. + */ + InParameterBinding(int position, @Nullable String expression) { + super(null, position, expression); + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding# + * prepare(java.lang.Object) + */ + @Override + public Object prepare(@Nullable Object value) { + + if (!ObjectUtils.isArray(value)) { + return value; + } + + int length = Array.getLength(value); + Collection result = new ArrayList<>(length); + + for (int i = 0; i < length; i++) { + result.add(Array.get(value, i)); + } + + return result; + } + } + + /** + * Represents a parameter binding in a JPQL query augmented with instructions of + * how to apply a parameter as LIKE parameter. This allows expressions like + * {@code …like %?1} in the JPQL query, which is not allowed by plain JPA. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ + public static class LikeParameterBinding extends ParameterBinding { + + private static final List SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, + Type.ENDING_WITH, Type.LIKE); + + private final Type type; + + /** + * Creates a new {@link LikeParameterBinding} for the parameter with the given + * name and {@link Type}. + * + * @param name must not be {@literal null} or empty. + * @param type must not be {@literal null}. + */ + LikeParameterBinding(String name, Type type) { + this(name, type, null); + } + + /** + * Creates a new {@link LikeParameterBinding} for the parameter with the given + * name and {@link Type} and parameter binding input. + * + * @param name must not be {@literal null} or empty. + * @param type must not be {@literal null}. + * @param expression may be {@literal null}. + */ + LikeParameterBinding(String name, Type type, @Nullable String expression) { + + super(name, null, expression); + + Assert.hasText(name, "Name must not be null or empty!"); + Assert.notNull(type, "Type must not be null!"); + + Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!", + StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES))); + + this.type = type; + } + + /** + * Creates a new {@link LikeParameterBinding} for the parameter with the given + * position and {@link Type}. + * + * @param position position of the parameter in the query. + * @param type must not be {@literal null}. + */ + LikeParameterBinding(int position, Type type) { + this(position, type, null); + } + + /** + * Creates a new {@link LikeParameterBinding} for the parameter with the given + * position and {@link Type}. + * + * @param position position of the parameter in the query. + * @param type must not be {@literal null}. + * @param expression may be {@literal null}. + */ + LikeParameterBinding(int position, Type type, @Nullable String expression) { + + super(null, position, expression); + + Assert.isTrue(position > 0, "Position must be greater than zero!"); + Assert.notNull(type, "Type must not be null!"); + + Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!", + StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES))); + + this.type = type; + } + + /** + * Returns the {@link Type} of the binding. + * + * @return the type + */ + public Type getType() { + return type; + } + + /** + * Prepares the given raw keyword according to the like type. + */ + @Nullable + @Override + public Object prepare(@Nullable Object value) { + + if (value == null) { + return null; + } + + switch (type) { + case STARTING_WITH: + return String.format("%s%%", value.toString()); + case ENDING_WITH: + return String.format("%%%s", value.toString()); + case CONTAINING: + return String.format("%%%s%%", value.toString()); + case LIKE: + default: + return value; + } + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (!(obj instanceof LikeParameterBinding)) { + return false; + } + + LikeParameterBinding that = (LikeParameterBinding) obj; + + return super.equals(obj) && this.type.equals(that.type); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = super.hashCode(); + + result += nullSafeHashCode(this.type); + + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type); + } + + /** + * Extracts the like {@link Type} from the given JPA like expression. + * + * @param expression must not be {@literal null} or empty. + */ + static Type getLikeTypeFrom(String expression) { + + Assert.hasText(expression, "Expression must not be null or empty!"); + + if (expression.matches("%.*%")) { + return Type.CONTAINING; + } + + if (expression.startsWith("%")) { + return Type.ENDING_WITH; + } + + if (expression.endsWith("%")) { + return Type.STARTING_WITH; + } + + return Type.LIKE; + } + } + + public static class Metadata { + private boolean usesJdbcStyleParameters = false; + + public boolean isUsesJdbcStyleParameters() { + return usesJdbcStyleParameters; + } + + public void setUsesJdbcStyleParameters(boolean usesJdbcStyleParameters) { + this.usesJdbcStyleParameters = usesJdbcStyleParameters; + } + } +} 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 ff8d3c94..c1bda427 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 @@ -39,6 +39,7 @@ import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.SingleColumnRowMapper; @@ -57,6 +58,7 @@ import org.springframework.util.Assert; * @author Moises Cisneros * @author Hebert Coelho * @author Diego Krupitza + * @author Christopher Klein */ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { @@ -70,11 +72,12 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { private final QueryMappingConfiguration queryMappingConfiguration; private final NamedParameterJdbcOperations operations; @Nullable private final BeanFactory beanfactory; + protected final QueryMethodEvaluationContextProvider evaluationContextProvider; JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, - @Nullable BeanFactory beanfactory) { + @Nullable BeanFactory beanfactory, QueryMethodEvaluationContextProvider evaluationContextProvider) { Assert.notNull(publisher, "ApplicationEventPublisher must not be null"); Assert.notNull(context, "RelationalMappingContextPublisher must not be null"); @@ -82,6 +85,7 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { Assert.notNull(dialect, "Dialect must not be null"); Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null"); Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); + Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvier must not be null"); this.publisher = publisher; this.callbacks = callbacks; @@ -91,6 +95,7 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { this.queryMappingConfiguration = queryMappingConfiguration; this.operations = operations; this.beanfactory = beanfactory; + this.evaluationContextProvider = evaluationContextProvider; } /** @@ -104,8 +109,10 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { CreateQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, - @Nullable BeanFactory beanfactory) { - super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory); + @Nullable BeanFactory beanfactory, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory, + evaluationContextProvider); } @Override @@ -131,8 +138,9 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { DeclaredQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, - @Nullable BeanFactory beanfactory) { - super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory); + @Nullable BeanFactory beanfactory, QueryMethodEvaluationContextProvider evaluationContextProvider) { + super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory, + evaluationContextProvider); } @Override @@ -149,7 +157,7 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { } StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, getOperations(), this::createMapper, - getConverter()); + getConverter(), evaluationContextProvider); query.setBeanFactory(getBeanFactory()); return query; } @@ -182,9 +190,10 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { RelationalMappingContext context, JdbcConverter converter, Dialect dialect, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, @Nullable BeanFactory beanfactory, CreateQueryLookupStrategy createStrategy, - DeclaredQueryLookupStrategy lookupStrategy) { + DeclaredQueryLookupStrategy lookupStrategy, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory); + super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory, + evaluationContextProvider); Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null"); Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null"); @@ -230,7 +239,7 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { public static QueryLookupStrategy create(@Nullable Key key, ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, - @Nullable BeanFactory beanFactory) { + @Nullable BeanFactory beanFactory, QueryMethodEvaluationContextProvider evaluationContextProvider) { Assert.notNull(publisher, "ApplicationEventPublisher must not be null"); Assert.notNull(context, "RelationalMappingContextPublisher must not be null"); @@ -240,10 +249,10 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); CreateQueryLookupStrategy createQueryLookupStrategy = new CreateQueryLookupStrategy(publisher, callbacks, context, - converter, dialect, queryMappingConfiguration, operations, beanFactory); + converter, dialect, queryMappingConfiguration, operations, beanFactory, evaluationContextProvider); DeclaredQueryLookupStrategy declaredQueryLookupStrategy = new DeclaredQueryLookupStrategy(publisher, callbacks, - context, converter, dialect, queryMappingConfiguration, operations, beanFactory); + context, converter, dialect, queryMappingConfiguration, operations, beanFactory, evaluationContextProvider); Key cleanedKey = key != null ? key : Key.CREATE_IF_NOT_FOUND; @@ -256,7 +265,8 @@ abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { return declaredQueryLookupStrategy; case CREATE_IF_NOT_FOUND: return new CreateIfNotFoundQueryLookupStrategy(publisher, callbacks, context, converter, dialect, - queryMappingConfiguration, operations, beanFactory, createQueryLookupStrategy, declaredQueryLookupStrategy); + queryMappingConfiguration, operations, beanFactory, createQueryLookupStrategy, declaredQueryLookupStrategy, + evaluationContextProvider); default: throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key)); } 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 fe0ed66b..fe60f2a3 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 @@ -47,6 +47,7 @@ import org.springframework.util.Assert; * @author Mark Paluch * @author Hebert Coelho * @author Diego Krupitza + * @author Christopher Klein */ public class JdbcRepositoryFactory extends RepositoryFactorySupport { @@ -136,7 +137,7 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { QueryMethodEvaluationContextProvider evaluationContextProvider) { return Optional.of(JdbcQueryLookupStrategy.create(key, publisher, entityCallbacks, context, converter, dialect, - queryMappingConfiguration, operations, beanFactory)); + queryMappingConfiguration, operations, beanFactory, evaluationContextProvider)); } /** 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 5430d4b0..85ac9814 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 @@ -78,8 +78,10 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.Param; +import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -90,6 +92,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.jdbc.JdbcTestUtils; import org.springframework.transaction.annotation.Transactional; +import lombok.Data; + /** * Very simple use cases for creation and usage of JdbcRepositories. * @@ -97,6 +101,7 @@ import org.springframework.transaction.annotation.Transactional; * @author Mark Paluch * @author Chirag Tailor * @author Diego Krupitza + * @author Christopher Klein */ @Transactional @TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) @@ -453,6 +458,17 @@ public class JdbcRepositoryIntegrationTests { assertThat(repository.countByName(one.getName())).isEqualTo(2); } + @Test // GH-619 + public void findBySpElWorksAsExpected() { + DummyEntity r = repository.save(createDummyEntity()); + + // assign the new id to the global ID provider holder; this is similar to Spring Security's SecurityContextHolder + MyIdContextProvider.ExtensionRoot.ID = r.getIdProp(); + + // expect, that we can find our newly created entity based upon the ID provider + assertThat(repository.findWithSpEL().getIdProp()).isEqualTo(r.getIdProp()); + } + @Test // GH-945 @EnabledOnFeature(TestDatabaseFeatures.Feature.IS_POSTGRES) public void usePrimitiveArrayAsArgument() { @@ -1305,6 +1321,9 @@ public class JdbcRepositoryIntegrationTests { boolean existsByNameNotIn(String... names); + @Query("SELECT * FROM dummy_entity WHERE id_prop = :#{myext.id}") + DummyEntity findWithSpEL(); + boolean existsByName(String name); int countByName(String name); @@ -1376,6 +1395,20 @@ public class JdbcRepositoryIntegrationTests { MyEventListener eventListener() { return new MyEventListener(); } + + @Bean + public ExtensionAwareQueryMethodEvaluationContextProvider extensionAware(List exts) { + ExtensionAwareQueryMethodEvaluationContextProvider extensionAwareQueryMethodEvaluationContextProvider = new ExtensionAwareQueryMethodEvaluationContextProvider(exts); + + factory.setEvaluationContextProvider(extensionAwareQueryMethodEvaluationContextProvider); + + return extensionAwareQueryMethodEvaluationContextProvider; + } + + @Bean + public EvaluationContextExtension evaluationContextExtension() { + return new MyIdContextProvider(); + } } interface RootRepository extends ListCrudRepository { @@ -1417,6 +1450,27 @@ public class JdbcRepositoryIntegrationTests { } } + // DATAJDBC-397 + public static class MyIdContextProvider implements EvaluationContextExtension { + @Override + public String getExtensionId() { + return "myext"; + } + + public static class ExtensionRoot { + // just public for testing purposes + public static Long ID = 1L; + + public Long getId() { + return ID; + } + } + + public Object getRootObject() { + return new ExtensionRoot(); + } + } + @Data @NoArgsConstructor static class DummyEntity { 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 index b2f844af..437e172c 100644 --- 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 @@ -21,6 +21,7 @@ import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.sql.JDBCType; import java.sql.ResultSet; +import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Set; @@ -49,6 +50,9 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing; 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.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -65,6 +69,7 @@ import org.springframework.util.ReflectionUtils; * @author Mark Paluch * @author Dennis Effing * @author Chirag Tailor + * @author Christopher Klein */ class StringBasedJdbcQueryUnitTests { @@ -72,6 +77,7 @@ class StringBasedJdbcQueryUnitTests { NamedParameterJdbcOperations operations; RelationalMappingContext context; JdbcConverter converter; + QueryMethodEvaluationContextProvider evaluationContextProvider; @BeforeEach void setup() { @@ -80,6 +86,7 @@ class StringBasedJdbcQueryUnitTests { this.operations = mock(NamedParameterJdbcOperations.class); this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS); this.converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); + this.evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); } @Test // DATAJDBC-165 @@ -167,9 +174,10 @@ class StringBasedJdbcQueryUnitTests { JdbcQueryMethod queryMethod = createMethod("sliceAll", Pageable.class); - assertThatThrownBy(() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter)) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessageContaining("Slice queries are not supported using string-based queries"); + assertThatThrownBy( + () -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("Slice queries are not supported using string-based queries"); } @Test // GH-774 @@ -177,17 +185,20 @@ class StringBasedJdbcQueryUnitTests { JdbcQueryMethod queryMethod = createMethod("pageAll", Pageable.class); - assertThatThrownBy(() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter)) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessageContaining("Page queries are not supported using string-based queries"); + assertThatThrownBy( + () -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider)) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("Page queries are not supported using string-based queries"); } @Test // GH-1212 void convertsEnumCollectionParameterIntoStringCollectionParameter() { JdbcQueryMethod queryMethod = createMethod("findByEnumTypeIn", Set.class); - BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), mock(RelationResolver.class)); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), converter); + BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), + mock(RelationResolver.class)); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), + converter, evaluationContextProvider); query.execute(new Object[] { Set.of(Direction.LEFT, Direction.RIGHT) }); @@ -202,8 +213,12 @@ class StringBasedJdbcQueryUnitTests { void convertsEnumCollectionParameterUsingCustomConverterWhenRegisteredForType() { JdbcQueryMethod queryMethod = createMethod("findByEnumTypeIn", Set.class); - BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), mock(RelationResolver.class), new JdbcCustomConversions(List.of(DirectionToIntegerConverter.INSTANCE, IntegerToDirectionConverter.INSTANCE)), JdbcTypeFactory.unsupported(), IdentifierProcessing.ANSI); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), converter); + BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), + mock(RelationResolver.class), + new JdbcCustomConversions(List.of(DirectionToIntegerConverter.INSTANCE, IntegerToDirectionConverter.INSTANCE)), + JdbcTypeFactory.unsupported(), IdentifierProcessing.ANSI); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), + converter, evaluationContextProvider); query.execute(new Object[] { Set.of(Direction.LEFT, Direction.RIGHT) }); @@ -218,8 +233,10 @@ class StringBasedJdbcQueryUnitTests { void doesNotConvertNonCollectionParameter() { JdbcQueryMethod queryMethod = createMethod("findBySimpleValue", Integer.class); - BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), mock(RelationResolver.class)); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), converter); + BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), + mock(RelationResolver.class)); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), + converter, evaluationContextProvider); query.execute(new Object[] { 1 }); @@ -238,7 +255,7 @@ class StringBasedJdbcQueryUnitTests { } private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod) { - return new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter); + return new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider); } interface MyRepository extends Repository { @@ -275,6 +292,35 @@ class StringBasedJdbcQueryUnitTests { @Query(value = "some sql statement") List findBySimpleValue(Integer value); + + @Query("SELECT * FROM table WHERE c = :#{myext.testValue} AND c2 = :#{myext.doSomething()}") + Object findBySpelExpression(Object object); + } + + @Test // GH-619 + public void spelCanBeUsedInsideQueries() { + + JdbcQueryMethod queryMethod = createMethod("findBySpelExpression", Object.class); + + List list = new ArrayList<>(); + list.add(new MyEvaluationContextProvider()); + QueryMethodEvaluationContextProvider evaluationContextProviderImpl = new ExtensionAwareQueryMethodEvaluationContextProvider( + list); + + StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, + evaluationContextProviderImpl); + + ArgumentCaptor paramSource = ArgumentCaptor.forClass(SqlParameterSource.class); + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + + sut.execute(new Object[] { "myValue" }); + + verify(this.operations).queryForObject(query.capture(), paramSource.capture(), any(RowMapper.class)); + + assertThat(query.getValue()) + .isEqualTo("SELECT * FROM table WHERE c = :__$synthetic$__1 AND c2 = :__$synthetic$__2"); + assertThat(paramSource.getValue().getValue("__$synthetic$__1")).isEqualTo("test-value1"); + assertThat(paramSource.getValue().getValue("__$synthetic$__2")).isEqualTo("test-value2"); } private static class CustomRowMapper implements RowMapper { @@ -307,7 +353,7 @@ class StringBasedJdbcQueryUnitTests { private enum Direction { LEFT, CENTER, RIGHT } - + @WritingConverter enum DirectionToIntegerConverter implements Converter { @@ -354,4 +400,27 @@ class StringBasedJdbcQueryUnitTests { return id; } } + + // DATAJDBC-397 + static class MyEvaluationContextProvider implements EvaluationContextExtension { + @Override + public String getExtensionId() { + return "myext"; + } + + public static class ExtensionRoot { + public String getTestValue() { + return "test-value1"; + } + + public String doSomething() { + return "test-value2"; + } + } + + public Object getRootObject() { + return new ExtensionRoot(); + } + } + } 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 73736df4..9e48b828 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 @@ -40,6 +40,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.util.TypeInformation; import org.springframework.jdbc.core.RowMapper; @@ -58,6 +59,7 @@ import org.springframework.util.ReflectionUtils; * @author Mark Paluch * @author Hebert Coelho * @author Diego Krupitza + * @author Christopher Klein */ class JdbcQueryLookupStrategyUnitTests { @@ -69,6 +71,7 @@ class JdbcQueryLookupStrategyUnitTests { private RepositoryMetadata metadata; private NamedQueries namedQueries = mock(NamedQueries.class); private NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); + QueryMethodEvaluationContextProvider evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); @BeforeEach void setup() { @@ -127,12 +130,13 @@ class JdbcQueryLookupStrategyUnitTests { @ParameterizedTest @MethodSource("correctLookUpStrategyForKeySource") void correctLookUpStrategyForKey(QueryLookupStrategy.Key key, Class expectedClass) { + RowMapper numberFormatMapper = mock(RowMapper.class); QueryMappingConfiguration mappingConfiguration = new DefaultQueryMappingConfiguration() .registerRowMapper(NumberFormat.class, numberFormatMapper); QueryLookupStrategy queryLookupStrategy = JdbcQueryLookupStrategy.create(key, publisher, callbacks, mappingContext, - converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null); + converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null, evaluationContextProvider); assertThat(queryLookupStrategy).isInstanceOf(expectedClass); } @@ -152,7 +156,7 @@ class JdbcQueryLookupStrategyUnitTests { QueryMappingConfiguration mappingConfiguration) { QueryLookupStrategy queryLookupStrategy = JdbcQueryLookupStrategy.create(key, publisher, callbacks, mappingContext, - converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null); + converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null, evaluationContextProvider); 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/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 324dc250..810c3a8e 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 @@ -44,6 +44,8 @@ import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; +import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; @@ -59,27 +61,36 @@ import org.springframework.transaction.PlatformTransactionManager; * @author Myeonghyeon Lee * @author Christoph Strobl * @author Chirag Tailor + * @author Christopher Klein */ @Configuration @ComponentScan // To pick up configuration classes (per activated profile) public class TestConfiguration { - @Autowired DataSource dataSource; - @Autowired BeanFactory beanFactory; + @Autowired + DataSource dataSource; + @Autowired + BeanFactory beanFactory; @Autowired ApplicationEventPublisher publisher; - @Autowired(required = false) SqlSessionFactory sqlSessionFactory; + @Autowired(required = false) + SqlSessionFactory sqlSessionFactory; @Bean JdbcRepositoryFactory jdbcRepositoryFactory( - @Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, RelationalMappingContext context, - Dialect dialect, JdbcConverter converter, Optional> namedQueries) { + @Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy, + RelationalMappingContext context, Dialect dialect, JdbcConverter converter, + Optional> namedQueries, List evaulationContextExtensions) { JdbcRepositoryFactory factory = new JdbcRepositoryFactory(dataAccessStrategy, context, converter, dialect, publisher, namedParameterJdbcTemplate()); namedQueries.map(it -> it.iterator().next()).ifPresent(factory::setNamedQueries); + + factory.setEvaluationContextProvider( + new ExtensionAwareQueryMethodEvaluationContextProvider(evaulationContextExtensions)); return factory; } + @Bean NamedParameterJdbcOperations namedParameterJdbcTemplate() { return new NamedParameterJdbcTemplate(dataSource); @@ -92,8 +103,8 @@ public class TestConfiguration { @Bean DataAccessStrategy defaultDataAccessStrategy( - @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, RelationalMappingContext context, - JdbcConverter converter, Dialect dialect) { + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, + RelationalMappingContext context, JdbcConverter converter, Dialect dialect) { return new DefaultDataAccessStrategy(new SqlGeneratorSource(context, converter, dialect), context, converter, template, new SqlParametersFactory(context, converter, dialect), @@ -128,8 +139,8 @@ public class TestConfiguration { @Bean JdbcConverter relationalConverter(RelationalMappingContext mappingContext, @Lazy RelationResolver relationResolver, - CustomConversions conversions, @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, - Dialect dialect) { + CustomConversions conversions, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, Dialect dialect) { JdbcArrayColumns arrayColumns = dialect instanceof JdbcDialect ? ((JdbcDialect) dialect).getArraySupport() : JdbcArrayColumns.DefaultSupport.INSTANCE; diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index 21f5dcc8..ac78ad9e 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -479,6 +479,9 @@ interface PersonRepository extends PagingAndSortingRepository { List findByLastname(String lastname); <7> @Query("SELECT * FROM person WHERE lastname = :lastname") Stream streamByLastname(String lastname); <8> + + @Query("SELECT * FROM person WHERE username = :#{ principal?.username }") + Person findActiveUser(); <6> } ---- <1> The method shows a query for all people with the given `firstname`. @@ -492,6 +495,7 @@ It completes with `IncorrectResultSizeDataAccessException` on non-unique results <6> In contrast to <3>, the first entity is always emitted even if the query yields more result documents. <7> The `findByLastname` method shows a query for all people with the given `lastname`. <8> The `streamByLastname` method returns a `Stream`, which makes values possible as soon as they are returned from the database. +<6> You can use the Spring Expression Language to dynamically resolve parameters. In the sample, Spring Security is used to resolve the username of the current user. ==== The following table shows the keywords that are supported for query methods: