From be3d05ec252a7a02b913c209b4e592b4d207006d Mon Sep 17 00:00:00 2001 From: Christopher Klein Date: Fri, 12 Jun 2020 07:57:24 +0200 Subject: [PATCH] DATAJDBC-397 - Experimental support for SpEL inside @Query annotations. --- .../mybatis/MyBatisDataAccessStrategy.java | 10 +- .../query/StringBasedJdbcQuery.java | 105 ++++- .../parameter/ParameterBindingParser.java | 295 ++++++++++++ .../query/parameter/ParameterBindings.java | 442 ++++++++++++++++++ .../support/JdbcQueryLookupStrategy.java | 11 +- .../support/JdbcRepositoryFactory.java | 3 +- .../support/JdbcRepositoryFactoryBean.java | 1 + .../JdbcRepositoryIntegrationTests.java | 62 ++- .../query/StringBasedJdbcQueryUnitTests.java | 83 +++- .../JdbcQueryLookupStrategyUnitTests.java | 5 +- .../data/jdbc/testing/TestConfiguration.java | 31 +- src/main/asciidoc/jdbc.adoc | 5 + 12 files changed, 999 insertions(+), 54 deletions(-) mode change 100644 => 100755 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java mode change 100644 => 100755 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java create mode 100755 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/parameter/ParameterBindingParser.java create mode 100755 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/parameter/ParameterBindings.java mode change 100644 => 100755 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java mode change 100644 => 100755 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java mode change 100644 => 100755 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactoryBean.java mode change 100644 => 100755 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java mode change 100644 => 100755 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java mode change 100644 => 100755 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java mode change 100644 => 100755 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java mode change 100644 => 100755 src/main/asciidoc/jdbc.adoc 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 old mode 100644 new mode 100755 index 3902ce3a2b..64fe5a7cff --- 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 @@ -63,6 +63,7 @@ * @author Tyler Van Gorder * @author Milan Milanov * @author Myeonghyeon Lee + * @author Christopher Klein */ public class MyBatisDataAccessStrategy implements DataAccessStrategy { @@ -319,12 +320,17 @@ public Iterable findAllById(Iterable ids, Class domainType) { public Iterable findAllByPath(Identifier identifier, PersistentPropertyPath path) { - String statementName = namespace(path.getBaseProperty().getOwner().getType()) + ".findAllByPath-" + + + // Using "path.getBaseProperty().getOwner().getType()" will throw "The method getOwner() is ambiguous for the type capture#12-of ? extends RelationalPersistentProperty" in Eclipse + RelationalPersistentProperty prop = path.getBaseProperty(); + Class clazz = prop.getOwner().getType(); + + String statementName = namespace(clazz) + ".findAllByPath-" + path.toDotPath(); return sqlSession().selectList(statementName, new MyBatisContext(identifier, null, path.getRequiredLeafProperty().getType())); - } /* 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 old mode 100644 new mode 100755 index e6b6b45651..56301d2d88 --- 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 @@ -17,14 +17,24 @@ import java.lang.reflect.Constructor; import java.sql.JDBCType; +import java.util.ArrayList; +import java.util.List; 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.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.repository.query.Parameter; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +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; @@ -34,14 +44,15 @@ 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. + * 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 + * @author Christopher Klein * @since 2.0 */ public class StringBasedJdbcQuery extends AbstractJdbcQuery { @@ -51,22 +62,27 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private final JdbcQueryMethod queryMethod; private final JdbcQueryExecution executor; private final JdbcConverter converter; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; /** - * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapper}. + * 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). + * @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) { + @Nullable RowMapper defaultRowMapper, JdbcConverter converter, + QueryMethodEvaluationContextProvider evaluationContextProvider) { super(queryMethod, operations, defaultRowMapper); this.queryMethod = queryMethod; this.converter = converter; + this.evaluationContextProvider = evaluationContextProvider; RowMapper rowMapper = determineRowMapper(defaultRowMapper); executor = getQueryExecution( // @@ -78,24 +94,76 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera /* * (non-Javadoc) - * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) + * + * @see + * org.springframework.data.repository.query.RepositoryQuery#execute(java.lang. + * Object[]) */ @Override public Object execute(Object[] objects) { - return executor.execute(determineQuery(), this.bindParameters(objects)); + + Metadata queryMeta = new Metadata(); + + String query = queryMethod.getDeclaredQuery(); + + if (StringUtils.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); + + MapSqlParameterSource parameterMap = this.bindMethodParameters(objects); + + extendParametersFromSpELEvaluation(parameterMap, bindings, objects); + return executor.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)); + }); } /* * (non-Javadoc) - * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() + * + * @see + * org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() */ @Override public JdbcQueryMethod getQueryMethod() { return queryMethod; } - MapSqlParameterSource bindParameters(Object[] objects) { - + MapSqlParameterSource bindMethodParameters(Object[] objects) { MapSqlParameterSource parameters = new MapSqlParameterSource(); queryMethod.getParameters().getBindableParameters() @@ -123,17 +191,6 @@ private void convertAndAddParameter(MapSqlParameterSource parameters, Parameter } } - 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) { 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 0000000000..a96eaa5201 --- /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 0000000000..07e58db5b3 --- /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 old mode 100644 new mode 100755 index 6c8ac5953b..fb8d30c0a8 --- 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 @@ -37,6 +37,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryCreationException; 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; @@ -53,6 +54,7 @@ * @author Mark Paluch * @author Maciej Walkowiak * @author Moises Cisneros + * @author Christopher Klein */ class JdbcQueryLookupStrategy implements QueryLookupStrategy { @@ -63,10 +65,13 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { private final Dialect dialect; private final QueryMappingConfiguration queryMappingConfiguration; private final NamedParameterJdbcOperations operations; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; public JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, - QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations) { + QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, + QueryMethodEvaluationContextProvider evaluationContextProvider + ) { Assert.notNull(publisher, "ApplicationEventPublisher must not be null"); Assert.notNull(context, "RelationalMappingContextPublisher must not be null"); @@ -74,6 +79,7 @@ public JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable En 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; @@ -82,6 +88,7 @@ public JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable En this.dialect = dialect; this.queryMappingConfiguration = queryMappingConfiguration; this.operations = operations; + this.evaluationContextProvider = evaluationContextProvider; } /* @@ -99,7 +106,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository if (namedQueries.hasQuery(queryMethod.getNamedQueryName()) || queryMethod.hasAnnotatedQuery()) { RowMapper mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod); - return new StringBasedJdbcQuery(queryMethod, operations, mapper, converter); + return new StringBasedJdbcQuery(queryMethod, operations, mapper, converter, evaluationContextProvider); } else { return new PartTreeJdbcQuery(context, queryMethod, dialect, converter, operations, createMapper(queryMethod)); } 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 old mode 100644 new mode 100755 index ce8786bacb..9cbeca4f64 --- 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 @@ -44,6 +44,7 @@ * @author Greg Turnquist * @author Christoph Strobl * @author Mark Paluch + * @author Christopher Klein */ public class JdbcRepositoryFactory extends RepositoryFactorySupport { @@ -143,7 +144,7 @@ protected Optional getQueryLookupStrategy(@Nullable QueryLo QueryMethodEvaluationContextProvider evaluationContextProvider) { return Optional.of(new JdbcQueryLookupStrategy(publisher, entityCallbacks, context, converter, dialect, - queryMappingConfiguration, operations)); + queryMappingConfiguration, operations, evaluationContextProvider)); } /** 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 old mode 100644 new mode 100755 index 8d17f38b28..89112d50b9 --- 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 @@ -44,6 +44,7 @@ * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch + * @author Christopher Klein */ public class JdbcRepositoryFactoryBean, S, ID extends Serializable> extends TransactionalRepositoryFactoryBeanSupport implements ApplicationEventPublisherAware { 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 old mode 100644 new mode 100755 index 2866223500..e967bd6396 --- 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 @@ -15,11 +15,9 @@ */ package org.springframework.data.jdbc.repository; -import static java.util.Arrays.*; -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.SoftAssertions.*; - -import lombok.Data; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.io.IOException; import java.sql.ResultSet; @@ -47,7 +45,9 @@ 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.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.Param; +import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -56,11 +56,14 @@ 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. * * @author Jens Schauder * @author Mark Paluch + * @author Christopher Klein */ @Transactional public class JdbcRepositoryIntegrationTests { @@ -94,6 +97,20 @@ NamedQueries namedQueries() throws IOException { 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(); + } } static class MyEventListener implements ApplicationListener> { @@ -381,6 +398,17 @@ public void countByQueryDerivation() { assertThat(repository.countByName(one.getName())).isEqualTo(2); } + + @Test // DATAJDBC-397 + 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()); + } private static DummyEntity createDummyEntity() { @@ -408,10 +436,34 @@ interface DummyEntityRepository extends CrudRepository { @Query("SELECT id_Prop from dummy_entity where id_Prop = :id") DummyEntity withMissingColumn(@Param("id") Long id); + @Query("SELECT * FROM dummy_entity WHERE id_prop = :#{myext.id}") + DummyEntity findWithSpEL(); + boolean existsByName(String name); int countByName(String name); } + + // 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 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 old mode 100644 new mode 100755 index bf7f952b9e..12f1c05577 --- 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 @@ -15,15 +15,22 @@ */ package org.springframework.data.jdbc.repository.query; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; - +import org.mockito.ArgumentCaptor; import org.springframework.dao.DataAccessException; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter; @@ -31,10 +38,14 @@ 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.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.Parameters; +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; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; /** * Unit tests for {@link StringBasedJdbcQuery}. @@ -44,6 +55,7 @@ * @author Maciej Walkowiak * @author Evgeni Dimitrov * @author Mark Paluch + * @author Christopher Klein */ public class StringBasedJdbcQueryUnitTests { @@ -53,6 +65,7 @@ public class StringBasedJdbcQueryUnitTests { NamedParameterJdbcOperations operations; RelationalMappingContext context; JdbcConverter converter; + QueryMethodEvaluationContextProvider evaluationContextProvider; @Before public void setup() throws NoSuchMethodException { @@ -67,6 +80,7 @@ public void setup() throws NoSuchMethodException { 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 @@ -75,7 +89,7 @@ public void emptyQueryThrowsException() { doReturn(null).when(queryMethod).getDeclaredQuery(); Assertions.assertThatExceptionOfType(IllegalStateException.class) // - .isThrownBy(() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter) + .isThrownBy(() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider) .execute(new Object[] {})); } @@ -84,7 +98,7 @@ public void defaultRowMapperIsUsedByDefault() { doReturn("some sql statement").when(queryMethod).getDeclaredQuery(); doReturn(RowMapper.class).when(queryMethod).getRowMapperClass(); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider); assertThat(query.determineRowMapper(defaultRowMapper)).isEqualTo(defaultRowMapper); } @@ -93,7 +107,7 @@ public void defaultRowMapperIsUsedByDefault() { public void defaultRowMapperIsUsedForNull() { doReturn("some sql statement").when(queryMethod).getDeclaredQuery(); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider); assertThat(query.determineRowMapper(defaultRowMapper)).isEqualTo(defaultRowMapper); } @@ -104,7 +118,7 @@ public void customRowMapperIsUsedWhenSpecified() { doReturn("some sql statement").when(queryMethod).getDeclaredQuery(); doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass(); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider); assertThat(query.determineRowMapper(defaultRowMapper)).isInstanceOf(CustomRowMapper.class); } @@ -115,9 +129,9 @@ 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[] {}); + new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider).execute(new Object[] {}); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider); ResultSetExtractor resultSetExtractor = query.determineResultSetExtractor(defaultRowMapper); @@ -134,7 +148,7 @@ public void customResultSetExtractorAndRowMapperGetCombined() { doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass(); doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass(); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider); ResultSetExtractor resultSetExtractor = query .determineResultSetExtractor(query.determineRowMapper(defaultRowMapper)); @@ -145,6 +159,32 @@ public void customResultSetExtractorAndRowMapperGetCombined() { "RowMapper is not expected to be custom"); } + + @Test // DATAJDBC-397 + public void spelCanBeUsedInsideQueries() { + + List list = new ArrayList<>(); + list.add(new MyEvaluationContextProvider()); + QueryMethodEvaluationContextProvider evaluationContextProviderImpl = new ExtensionAwareQueryMethodEvaluationContextProvider(list); + + doReturn("SELECT * FROM table WHERE c = :#{myext.testValue} AND c2 = :#{myext.doSomething()}").when(queryMethod).getDeclaredQuery(); + StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, + evaluationContextProviderImpl); + + ArgumentCaptor paramSource = ArgumentCaptor.forClass(SqlParameterSource.class); + ArgumentCaptor query = ArgumentCaptor.forClass(String.class); + ArgumentCaptor rse = ArgumentCaptor.forClass(RowMapper.class); + + sut.execute(new Object[] { "myValue"}); + + verify(this.operations).queryForObject(query.capture(), paramSource.capture(), rse.capture()); + + assertThat(query.getValue(), is("SELECT * FROM table WHERE c = :__$synthetic$__1 AND c2 = :__$synthetic$__2")); + assertThat(paramSource.getValue().getValue("__$synthetic$__1"), is("test-value1")); + assertThat(paramSource.getValue().getValue("__$synthetic$__2"), is("test-value2")); + } + + /** * The whole purpose of this method is to easily generate a {@link DefaultParameters} instance during test setup. */ @@ -189,4 +229,27 @@ Long getId() { 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 old mode 100644 new mode 100755 index 09a4b32a12..1b4632ef76 --- 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 @@ -35,6 +35,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.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -50,6 +51,7 @@ * @author Maciej Walkowiak * @author Evgeni Dimitrov * @author Mark Paluch + * @author Christopher Klein */ public class JdbcQueryLookupStrategyUnitTests { @@ -61,6 +63,7 @@ public class JdbcQueryLookupStrategyUnitTests { RepositoryMetadata metadata; NamedQueries namedQueries = mock(NamedQueries.class); NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); + QueryMethodEvaluationContextProvider evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); @Before public void setup() { @@ -88,7 +91,7 @@ public void typeBasedRowMapperGetsUsedForQuery() { private RepositoryQuery getRepositoryQuery(String name, QueryMappingConfiguration mappingConfiguration) { JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(publisher, callbacks, mappingContext, - converter, H2Dialect.INSTANCE, mappingConfiguration, operations); + converter, H2Dialect.INSTANCE, mappingConfiguration, operations, 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 old mode 100644 new mode 100755 index 6e69246f1d..70a87abe35 --- 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.testing; +import java.util.List; import java.util.Optional; import javax.sql.DataSource; @@ -43,6 +44,8 @@ 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; @@ -56,26 +59,36 @@ * @author Mark Paluch * @author Fei Dong * @author Myeonghyeon Lee + * @author Christopher Klein */ @Configuration @ComponentScan // To pick up configuration classes (per activated profile) public class TestConfiguration { - @Autowired DataSource dataSource; - @Autowired ApplicationEventPublisher publisher; - @Autowired(required = false) SqlSessionFactory sqlSessionFactory; + @Autowired + DataSource dataSource; + @Autowired + ApplicationEventPublisher publisher; + @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.ifPresent(factory::setNamedQueries); + + // DATAJDBC-397: we have to retrieve all EvaluationContextExtension's and assign them to the MethodEvaluationContextProvider + factory.setEvaluationContextProvider( + new ExtensionAwareQueryMethodEvaluationContextProvider(evaulationContextExtensions)); return factory; } + @Bean NamedParameterJdbcOperations namedParameterJdbcTemplate() { return new NamedParameterJdbcTemplate(dataSource); @@ -88,8 +101,8 @@ PlatformTransactionManager transactionManager() { @Bean DataAccessStrategy defaultDataAccessStrategy( - @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, RelationalMappingContext context, - JdbcConverter converter, Dialect dialect) { + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, + RelationalMappingContext context, JdbcConverter converter, Dialect dialect) { DefaultDataAccessStrategy defaultDataAccessStrategy = new DefaultDataAccessStrategy( new SqlGeneratorSource(context, converter, dialect), context, converter, template); @@ -112,8 +125,8 @@ CustomConversions jdbcCustomConversions() { @Bean JdbcConverter relationalConverter(RelationalMappingContext mappingContext, @Lazy RelationResolver relationResolver, - CustomConversions conversions, @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, - Dialect dialect) { + CustomConversions conversions, + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, Dialect dialect) { return new BasicJdbcConverter( // mappingContext, // diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc old mode 100644 new mode 100755 index 68e878d0aa..1e73f974ac --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -497,6 +497,9 @@ interface PersonRepository extends PagingAndSortingRepository { @Query("SELECT * FROM person WHERE lastname = :lastname") List findByLastname(String lastname); <5> + + @Query("SELECT * FROM person WHERE username = :#{ principal?.username }") + Person findActiveUser(); <6> } ---- <1> The method shows a query for all people with the given `lastname`. @@ -507,6 +510,8 @@ Thus, the method name results in a query expression of `SELECT … FROM person W It completes with `IncorrectResultSizeDataAccessException` on non-unique results. <4> In contrast to <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. +<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: