Support for SpEL inside @Query annotations.
Constructs like the following work now.
```
@Query("select u from User u where u.firstname = :#{#customer.firstname}")
List<User> findUsersByCustomersFirstname(@Param("customer") Customer customer);
```
Closes #619
Original pull request #229
See https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions
This commit is contained in:
committed by
Jens Schauder
parent
f326897950
commit
9abaa5a911
@@ -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
|
||||
|
||||
@@ -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<Object>) defaultRowMapper, converter);
|
||||
@Nullable RowMapper<?> defaultRowMapper, JdbcConverter converter,
|
||||
QueryMethodEvaluationContextProvider evaluationContextProvider) {
|
||||
this(queryMethod, operations, result -> (RowMapper<Object>) 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<ParameterBinding> 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<ParameterBinding> 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<ParameterBinding> 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
|
||||
|
||||
@@ -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 = "(?<![:\\\\]):";
|
||||
public static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
|
||||
|
||||
|
||||
static {
|
||||
|
||||
List<String> 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<ParameterBinding> 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<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
|
||||
? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1)
|
||||
: (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1);
|
||||
|
||||
String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
|
||||
|
||||
BiFunction<String, String, String> 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<ParameterBinding> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Object> 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<Type> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<EvaluationContextExtension> exts) {
|
||||
ExtensionAwareQueryMethodEvaluationContextProvider extensionAwareQueryMethodEvaluationContextProvider = new ExtensionAwareQueryMethodEvaluationContextProvider(exts);
|
||||
|
||||
factory.setEvaluationContextProvider(extensionAwareQueryMethodEvaluationContextProvider);
|
||||
|
||||
return extensionAwareQueryMethodEvaluationContextProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EvaluationContextExtension evaluationContextExtension() {
|
||||
return new MyIdContextProvider();
|
||||
}
|
||||
}
|
||||
|
||||
interface RootRepository extends ListCrudRepository<Root, Long> {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Object, Long> {
|
||||
@@ -275,6 +292,35 @@ class StringBasedJdbcQueryUnitTests {
|
||||
|
||||
@Query(value = "some sql statement")
|
||||
List<Object> 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<EvaluationContextExtension> list = new ArrayList<>();
|
||||
list.add(new MyEvaluationContextProvider());
|
||||
QueryMethodEvaluationContextProvider evaluationContextProviderImpl = new ExtensionAwareQueryMethodEvaluationContextProvider(
|
||||
list);
|
||||
|
||||
StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter,
|
||||
evaluationContextProviderImpl);
|
||||
|
||||
ArgumentCaptor<SqlParameterSource> paramSource = ArgumentCaptor.forClass(SqlParameterSource.class);
|
||||
ArgumentCaptor<String> 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<Object> {
|
||||
@@ -307,7 +353,7 @@ class StringBasedJdbcQueryUnitTests {
|
||||
private enum Direction {
|
||||
LEFT, CENTER, RIGHT
|
||||
}
|
||||
|
||||
|
||||
@WritingConverter
|
||||
enum DirectionToIntegerConverter implements Converter<Direction, JdbcValue> {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<? extends NumberFormat> 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);
|
||||
|
||||
@@ -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<List<NamedQueries>> namedQueries) {
|
||||
@Qualifier("defaultDataAccessStrategy") DataAccessStrategy dataAccessStrategy,
|
||||
RelationalMappingContext context, Dialect dialect, JdbcConverter converter,
|
||||
Optional<List<NamedQueries>> namedQueries, List<EvaluationContextExtension> 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;
|
||||
|
||||
@@ -479,6 +479,9 @@ interface PersonRepository extends PagingAndSortingRepository<Person, String> {
|
||||
List<Person> findByLastname(String lastname); <7>
|
||||
@Query("SELECT * FROM person WHERE lastname = :lastname")
|
||||
Stream<Person> 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:
|
||||
|
||||
Reference in New Issue
Block a user