DATACOUCH-166 - Add basic SpEL support
Added the step of parsing SpEL expressions delimited by ${ } in StringN1qlBasedQuery, then the actual evaluation of each expression.
Refactored StringN1qlBasedQuery to use SpEL across the board, including for N1QL specific placeholders (SELECT-FROM, WHERE filter, etc...).
Refactored StringN1qlBasedQuery to use local methods rather than static methods, and amended the unit tests accordingly (using mocks).
Refactored getStatement and getCountStatement so they can see the parameters passed in at runtime.
Added unit tests + integration test.
Added SpEL support example in documentation.
This commit is contained in:
@@ -139,6 +139,14 @@ public class SimpleCouchbaseRepositoryTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFindFromUsernameInlineWithSpelParsing() {
|
||||
User user = repository.findByUsernameWithSpelAndPlaceholder();
|
||||
assertNotNull(user);
|
||||
assertEquals("testuser-4", user.getKey());
|
||||
assertEquals("uname-4", user.getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFindFromDeriveQueryWithRegexpAndIn() {
|
||||
User user = repository.findByUsernameRegexAndUsernameIn("uname-[123]", Arrays.asList("uname-2", "uname-4"));
|
||||
|
||||
@@ -34,12 +34,15 @@ public interface UserRepository extends CouchbaseRepository<User, String> {
|
||||
@View(designDocument = "user", viewName = "all")
|
||||
Iterable<User> customViewQuery(ViewQuery query);
|
||||
|
||||
@Query("$SELECT_ENTITY$ WHERE username = $1")
|
||||
@Query("${#n1ql.selectEntity} WHERE username = $1")
|
||||
User findByUsername(String username);
|
||||
|
||||
@Query("SELECT * FROM $BUCKET$ WHERE username = $1")
|
||||
@Query("SELECT * FROM ${#n1ql.bucket} WHERE username = $1")
|
||||
User findByUsernameBadSelect(String username);
|
||||
|
||||
@Query("${#n1ql.selectEntity} WHERE username LIKE '%-${3 + 1}'")
|
||||
User findByUsernameWithSpelAndPlaceholder();
|
||||
|
||||
@Query
|
||||
User findByUsernameRegexAndUsernameIn(String regex, List<String> sample);
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ Here is an example:
|
||||
----
|
||||
public interface UserRepository extends CrudRepository<UserInfo, String> {
|
||||
|
||||
@Query("$SELECT_ENTITY$ WHERE role = 'admin' AND $FILTER_TYPE$")
|
||||
@Query("${#n1ql.selectEntity} WHERE role = 'admin' AND ${#n1ql.filter}")
|
||||
List<UserInfo> findAllAdmins();
|
||||
|
||||
List<UserInfo> findByFirstname(String fname);
|
||||
@@ -129,12 +129,18 @@ public interface UserRepository extends CrudRepository<UserInfo, String> {
|
||||
|
||||
Here we see two N1QL-backed ways of querying.
|
||||
|
||||
The first one uses the `Query` annotation to provide a N1QL statement inline. Notice the special placeholders:
|
||||
The first method uses the `Query` annotation to provide a N1QL statement inline. SpEL (Spring Expression Language) is supported by surrounding SpEL expressions between `${` and `}`. You can also include N1QL positional placeholders (eg. `$1`, `$2`).
|
||||
A few N1QL-specific values are provided through SpEL:
|
||||
|
||||
- `$SELECT_ENTITY` allows to easily make sure the statement will select all the fields necessary to build the full entity (including document ID and CAS value).
|
||||
- `$FILTER_TYPE$` in the WHERE clause adds a criteria matching the entity type with the field that Spring Data uses to store type information.
|
||||
- `#n1ql.selectEntity` allows to easily make sure the statement will select all the fields necessary to build the full entity (including document ID and CAS value).
|
||||
- `#n1ql.filter` in the WHERE clause adds a criteria matching the entity type with the field that Spring Data uses to store type information.
|
||||
- `#n1ql.bucket` will be replaced by the name of the bucket the entity is stored in, escaped in backticks.
|
||||
- `#n1ql.fields` will be replaced by the list of fields (eg. for a SELECT clause) necessary to reconstruct the entity.
|
||||
|
||||
The second one use Spring-Data's query derivation mechanism to build a N1QL query from the method name and parameters. This will produce a query looking like this: `SELECT ... FROM ... WHERE firstName = "valueOfFnameAtRuntime"`. You can combine these criteria, even do a count with a name like `countByFirstname` or a limit with a name like `findFirst3ByLastname`...
|
||||
Another example: "`${#n1ql.selectEntity} WHERE ${#n1ql.filter} AND test = $1`", which is equivalent to
|
||||
`SELECT ${#n1ql.fields} FROM ${#n1ql.bucket} WHERE ${#n1ql.filter} AND test = $1`".
|
||||
|
||||
The second method uses Spring-Data's query derivation mechanism to build a N1QL query from the method name and parameters. This will produce a query looking like this: `SELECT ... FROM ... WHERE firstName = "valueOfFnameAtRuntime"`. You can combine these criteria, even do a count with a name like `countByFirstname` or a limit with a name like `findFirst3ByLastname`...
|
||||
|
||||
NOTE: Actually the generated N1QL query will also contain an additional N1QL criteria in order to only select documents that match the repository's entity class.
|
||||
|
||||
|
||||
@@ -34,24 +34,25 @@ import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
|
||||
* <p/>
|
||||
* In this case, one can use a placeholder notation of {@code ?0}, {@code ?1} and so on.
|
||||
* <p/>
|
||||
* Also, the following placeholders can be used to be replaced by the underlying {@link CouchbaseTemplate}
|
||||
* Also, SpEL in the form <code>${spelExpression}</code> is supported, including the
|
||||
* following N1QL variables that will be replaced by the underlying {@link CouchbaseTemplate}
|
||||
* associated information:
|
||||
* <ul>
|
||||
* <li>
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_SELECT_FROM}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_SELECT_FROM})
|
||||
* {@value StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE}
|
||||
* (see {@link StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE})
|
||||
* </li>
|
||||
* <li>
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_BUCKET}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_BUCKET})
|
||||
* {@value StringN1qlBasedQuery#SPEL_BUCKET}
|
||||
* (see {@link StringN1qlBasedQuery#SPEL_BUCKET})
|
||||
* </li>
|
||||
* <li>
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_ENTITY}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_ENTITY})
|
||||
* {@value StringN1qlBasedQuery#SPEL_ENTITY}
|
||||
* (see {@link StringN1qlBasedQuery#SPEL_ENTITY})
|
||||
* </li>
|
||||
* <li>
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_FILTER_TYPE}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_FILTER_TYPE})
|
||||
* {@value StringN1qlBasedQuery#SPEL_FILTER}
|
||||
* (see {@link StringN1qlBasedQuery#SPEL_FILTER})
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
|
||||
@@ -20,7 +20,6 @@ import java.util.List;
|
||||
|
||||
import com.couchbase.client.java.document.json.JsonArray;
|
||||
import com.couchbase.client.java.query.N1qlQuery;
|
||||
import com.couchbase.client.java.query.N1qlQueryResult;
|
||||
import com.couchbase.client.java.query.N1qlParams;
|
||||
import com.couchbase.client.java.query.Statement;
|
||||
import com.couchbase.client.java.query.consistency.ScanConsistency;
|
||||
@@ -28,7 +27,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.couchbase.core.CouchbaseOperations;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.SliceImpl;
|
||||
@@ -55,16 +53,16 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery {
|
||||
this.couchbaseOperations = couchbaseOperations;
|
||||
}
|
||||
|
||||
protected abstract Statement getCount(ParameterAccessor accessor);
|
||||
protected abstract Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters);
|
||||
|
||||
protected abstract Statement getStatement(ParameterAccessor accessor);
|
||||
protected abstract Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters);
|
||||
|
||||
protected abstract JsonArray getPlaceholderValues(ParameterAccessor accessor);
|
||||
|
||||
@Override
|
||||
public Object execute(Object[] parameters) {
|
||||
ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
|
||||
Statement statement = getStatement(accessor);
|
||||
ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters);
|
||||
Statement statement = getStatement(accessor, parameters);
|
||||
JsonArray queryPlaceholderValues = getPlaceholderValues(accessor);
|
||||
|
||||
//prepare the final query
|
||||
@@ -73,7 +71,7 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery {
|
||||
|
||||
//prepare a count query
|
||||
//TODO only do that when necessary (isPageQuery or isSliceQuery)
|
||||
Statement countStatement = getCount(accessor);
|
||||
Statement countStatement = getCount(accessor, parameters);
|
||||
N1qlQuery countQuery = buildQuery(countStatement, queryPlaceholderValues,
|
||||
getCouchbaseOperations().getDefaultConsistency().n1qlConsistency());
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Statement getCount(ParameterAccessor accessor) {
|
||||
protected Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters) {
|
||||
Expression bucket = i(getCouchbaseOperations().getCouchbaseBucket().name());
|
||||
WherePath countFrom = select(count("*").as(CountFragment.COUNT_ALIAS)).from(bucket);
|
||||
|
||||
@@ -60,7 +60,7 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
|
||||
return queryCreator.createQuery();
|
||||
}
|
||||
@Override
|
||||
protected Statement getStatement(ParameterAccessor accessor) {
|
||||
protected Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters) {
|
||||
String bucketName = getCouchbaseOperations().getCouchbaseBucket().name();
|
||||
Expression bucket = N1qlUtils.escapedBucket(bucketName);
|
||||
|
||||
|
||||
@@ -19,10 +19,16 @@ package org.springframework.data.couchbase.repository.query;
|
||||
import com.couchbase.client.java.document.json.JsonArray;
|
||||
import com.couchbase.client.java.query.N1qlQuery;
|
||||
import com.couchbase.client.java.query.Statement;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.couchbase.core.CouchbaseOperations;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
|
||||
/**
|
||||
* A {@link RepositoryQuery} for Couchbase, based on N1QL and a String statement.
|
||||
@@ -30,51 +36,77 @@ import org.springframework.data.repository.query.RepositoryQuery;
|
||||
* The statement can contain positional placeholders (eg. <code>name = $1</code>) that will map to the
|
||||
* method's parameters, in the same order.
|
||||
* <p/>
|
||||
* The statement can contain placeholders for the {@link #PLACEHOLDER_BUCKET bucket namespace},
|
||||
* the {@link #PLACEHOLDER_ENTITY ID and CAS fields} necessary for entity reconstruction or
|
||||
* a shortcut that covers {@link #PLACEHOLDER_SELECT_FROM SELECT AND FROM clauses}.
|
||||
* The statement can also contain SpEL expressions enclosed in <code>${}</code>.
|
||||
* <p/> There are couchbase-provided variables included for the {@link #SPEL_BUCKET bucket namespace},
|
||||
* the {@link #SPEL_ENTITY ID and CAS fields} necessary for entity reconstruction
|
||||
* or a shortcut that covers {@link #SPEL_SELECT_FROM_CLAUSE SELECT AND FROM clauses},
|
||||
* along with a variable for {@link #SPEL_FILTER WHERE clause filtering} of the correct entity.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
|
||||
|
||||
/**
|
||||
* Use this placeholder in a {@link org.springframework.data.couchbase.core.view.Query @Query} annotation's inline
|
||||
* statement. This will be replaced by the correct <code>SELECT x FROM y</code> clause needed for entity mapping.
|
||||
*/
|
||||
public static final String PLACEHOLDER_SELECT_FROM = "$SELECT_ENTITY$";
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(StringN1qlBasedQuery.class);
|
||||
|
||||
|
||||
public static final String SPEL_PREFIX = "n1ql";
|
||||
|
||||
/**
|
||||
* Use this placeholder in a {@link org.springframework.data.couchbase.core.view.Query @Query} annotation's inline
|
||||
* statement. This will be replaced by the bucket name corresponding to the repository's entity.
|
||||
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.Query @Query}
|
||||
* annotation's inline statement. This will be replaced by the correct <code>SELECT x FROM y</code> clause needed
|
||||
* for entity mapping. Eg. <code>"${{@value StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE}} WHERE test = true"</code>.
|
||||
* Note this only makes sense once, as the beginning of the statement.
|
||||
*/
|
||||
public static final String PLACEHOLDER_BUCKET = "$BUCKET$";
|
||||
public static final String SPEL_SELECT_FROM_CLAUSE = "#" + SPEL_PREFIX + ".selectEntity";
|
||||
|
||||
/**
|
||||
* Use this placeholder in a {@link org.springframework.data.couchbase.core.view.Query @Query} annotation's inline
|
||||
* statement. This will be replaced by the fields allowing to construct the repository's entity (SELECT clause).
|
||||
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.Query @Query}
|
||||
* annotation's inline statement. This will be replaced by the (escaped) bucket name corresponding to the repository's
|
||||
* entity. Eg. <code>"SELECT * FROM ${{@value StringN1qlBasedQuery#SPEL_BUCKET}} LIMIT 3"</code>.
|
||||
*/
|
||||
public static final String PLACEHOLDER_ENTITY = "$ENTITY$";
|
||||
public static final String SPEL_BUCKET = "#" + SPEL_PREFIX + ".bucket";
|
||||
|
||||
/**
|
||||
* Use this placeholder in a {@link org.springframework.data.couchbase.core.view.Query @Query} annotation's inline
|
||||
* statement WHERE clause. This will be replaced by the expression allowing to only select documents matching the
|
||||
* entity's class.
|
||||
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.Query @Query}
|
||||
* annotation's inline statement. This will be replaced by the fields allowing to construct the repository's entity
|
||||
* (SELECT clause). Eg. <code>"SELECT ${{@value StringN1qlBasedQuery#SPEL_ENTITY}} FROM test"</code>.
|
||||
*/
|
||||
public static final String PLACEHOLDER_FILTER_TYPE = "$FILTER_TYPE$";
|
||||
public static final String SPEL_ENTITY = "#" + SPEL_PREFIX + ".fields";
|
||||
|
||||
private final Statement statement;
|
||||
private final Statement countStatement;
|
||||
/**
|
||||
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.Query @Query}
|
||||
* annotation's inline statement WHERE clause. This will be replaced by the expression allowing to only select
|
||||
* documents matching the entity's class. Eg. <code>"SELECT * FROM test WHERE test = true AND ${{@value StringN1qlBasedQuery#SPEL_FILTER}}"</code>.
|
||||
*/
|
||||
public static final String SPEL_FILTER = "#" + SPEL_PREFIX + ".filter";
|
||||
|
||||
public StringN1qlBasedQuery(String statement, CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) {
|
||||
super(queryMethod, couchbaseOperations);
|
||||
String typeField = getCouchbaseOperations().getConverter().getTypeKey();
|
||||
Class<?> typeValue = getQueryMethod().getEntityInformation().getJavaType();
|
||||
this.statement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name(), typeField, typeValue, false);
|
||||
this.countStatement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name(), typeField, typeValue, true);
|
||||
private final String originalStatement;
|
||||
private final SpelExpressionParser parser;
|
||||
private final EvaluationContextProvider evaluationContextProvider;
|
||||
private final N1qlSpelValues countContext;
|
||||
private final N1qlSpelValues statementContext;
|
||||
|
||||
protected String getTypeField() {
|
||||
return getCouchbaseOperations().getConverter().getTypeKey();
|
||||
}
|
||||
|
||||
protected static Statement prepare(String statement, String bucketName, String typeField, Class<?> typeValue, boolean isCount) {
|
||||
protected Class<?> getTypeValue() {
|
||||
return getQueryMethod().getEntityInformation().getJavaType();
|
||||
}
|
||||
|
||||
public StringN1qlBasedQuery(String statement, CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations,
|
||||
SpelExpressionParser spelParser, final EvaluationContextProvider evaluationContextProvider) {
|
||||
super(queryMethod, couchbaseOperations);
|
||||
|
||||
this.originalStatement = statement;
|
||||
this.parser = spelParser;
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
|
||||
this.statementContext = createN1qlSpelValues(getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue(), false);
|
||||
this.countContext = createN1qlSpelValues(getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue(), true);
|
||||
}
|
||||
|
||||
public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, Class<?> typeValue, boolean isCount) {
|
||||
String b = "`" + bucketName + "`";
|
||||
String entity = "META(" + b + ").id AS " + CouchbaseOperations.SELECT_ID +
|
||||
", META(" + b + ").cas AS " + CouchbaseOperations.SELECT_CAS;
|
||||
@@ -87,23 +119,85 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
|
||||
}
|
||||
String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\"";
|
||||
|
||||
String result = statement;
|
||||
if (statement.contains(PLACEHOLDER_SELECT_FROM)) {
|
||||
result = result.replaceFirst("\\$SELECT_ENTITY\\$", selectEntity);
|
||||
} else {
|
||||
if (statement.contains(PLACEHOLDER_BUCKET)) {
|
||||
result = result.replaceAll("\\$BUCKET\\$", b);
|
||||
return new N1qlSpelValues(selectEntity, entity, b, typeSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the statement to detect SPEL expressions (delimited by <code>${</code> and <code>}</code>)
|
||||
* and replace said expressions with their corresponding values (see {@link #evaluateSpelExpression(String, boolean, Object[])}).
|
||||
*
|
||||
* @param statement the full statement into which SpEL expressions should be parsed and replaced.
|
||||
* @param runtimeParameters the parameters passed into the method at runtime.
|
||||
* @return the statement with the SpEL interpreted and replaced by its values.
|
||||
*/
|
||||
protected String parseSpel(String statement, boolean isCount, Object[] runtimeParameters) {
|
||||
StringBuilder parsed = new StringBuilder(statement);
|
||||
|
||||
int currentIndex = 0;
|
||||
while(currentIndex > -1 && currentIndex < parsed.length()) {
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Evaluating from index {}, {}", currentIndex, parsed.substring(currentIndex));
|
||||
}
|
||||
if (statement.contains(PLACEHOLDER_ENTITY)) {
|
||||
result = result.replaceFirst("\\$ENTITY\\$", entity);
|
||||
int opening = parsed.indexOf("${", currentIndex);
|
||||
if (opening == -1) {
|
||||
break;
|
||||
} else {
|
||||
//find the end of the ${ } expression, eat inner braces
|
||||
int closing = opening + 2;
|
||||
int curlyBraceOpenCnt = 1;
|
||||
|
||||
while (curlyBraceOpenCnt > 0) {
|
||||
switch (parsed.charAt(closing++)) {
|
||||
case '{':
|
||||
curlyBraceOpenCnt++;
|
||||
break;
|
||||
case '}':
|
||||
curlyBraceOpenCnt--;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
//we're now at the end of the expression
|
||||
//evaluate and replace
|
||||
String expression = parsed.substring(opening + 2, closing - 1);
|
||||
String replacement = evaluateSpelExpression(expression, isCount, runtimeParameters);
|
||||
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Replacing SPEL \"{}\" with value \"{}\"", expression, replacement);
|
||||
}
|
||||
|
||||
parsed.replace(opening, closing, replacement);
|
||||
|
||||
//move on
|
||||
currentIndex = opening + replacement.length();
|
||||
}
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
if (statement.contains(PLACEHOLDER_FILTER_TYPE)) {
|
||||
result = result.replaceFirst("\\$FILTER_TYPE\\$", typeSelection);
|
||||
/**
|
||||
* This method is in charge of evaluating a single SPEL expression (as delimited by <code>${</code> and <code>}</code>)
|
||||
* and returning the corresponding computed value.
|
||||
*
|
||||
* @param expression the SPEL expression.
|
||||
* @param runtimeParameters the parameters using during method call.
|
||||
* @return the corresponding value.
|
||||
*/
|
||||
protected String evaluateSpelExpression(String expression, boolean isCount, Object[] runtimeParameters) {
|
||||
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), runtimeParameters);
|
||||
Expression parsedExpression = parser.parseExpression(expression);
|
||||
N1qlSpelValues n1qlSpelValues = this.statementContext;
|
||||
if (isCount) {
|
||||
n1qlSpelValues = this.countContext;
|
||||
}
|
||||
|
||||
return N1qlQuery.simple(result).statement();
|
||||
return doEvaluate(evaluationContext, parsedExpression, n1qlSpelValues);
|
||||
}
|
||||
|
||||
protected static String doEvaluate(EvaluationContext context, Expression parsedExpression, N1qlSpelValues spelValues) {
|
||||
context.setVariable(SPEL_PREFIX, spelValues);
|
||||
return parsedExpression.getValue(context, String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,12 +210,52 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement getStatement(ParameterAccessor accessor) {
|
||||
return this.statement;
|
||||
public Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters) {
|
||||
String parsedStatement = parseSpel(this.originalStatement, false, runtimeParameters);
|
||||
return N1qlQuery.simple(parsedStatement).statement();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Statement getCount(ParameterAccessor accessor) {
|
||||
return this.countStatement;
|
||||
protected Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters) {
|
||||
String parsedCountStatement = parseSpel(this.originalStatement, true, runtimeParameters);
|
||||
return N1qlQuery.simple(parsedCountStatement).statement();
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is exposed to SpEL parsing through the variable <code>#{@value StringN1qlBasedQuery#SPEL_PREFIX}</code>.
|
||||
* Use the attributes in your SpEL expressions: {@link #selectEntity}, {@link #fields}, {@link #bucket} and {@link #filter}.
|
||||
*/
|
||||
public static final class N1qlSpelValues {
|
||||
|
||||
/**
|
||||
* <code>${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* selectEntity</code> will be replaced by the SELECT-FROM clause corresponding to the entity. Use once at the beginning.
|
||||
*/
|
||||
public final String selectEntity;
|
||||
|
||||
/**
|
||||
* <code>${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* fields</code> will be replaced by the list of N1QL fields allowing to reconstruct the entity.
|
||||
*/
|
||||
public final String fields;
|
||||
|
||||
/**
|
||||
* <code>${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* bucket</code> will be replaced by (escaped) bucket name in which the entity is stored.
|
||||
*/
|
||||
public final String bucket;
|
||||
|
||||
/**
|
||||
* <code>${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* filter</code> will be replaced by an expression allowing to select only entries matching the entity in a WHERE clause.
|
||||
*/
|
||||
public final String filter;
|
||||
|
||||
public N1qlSpelValues(String selectClause, String entityFields, String bucket, String filter) {
|
||||
this.selectEntity = selectClause;
|
||||
this.fields = entityFields;
|
||||
this.bucket = bucket;
|
||||
this.filter = filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -53,6 +54,8 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
|
||||
|
||||
/**
|
||||
* Holds the reference to the template.
|
||||
*/
|
||||
@@ -170,13 +173,20 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
|
||||
|
||||
@Override
|
||||
protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key, EvaluationContextProvider contextProvider) {
|
||||
return new CouchbaseQueryLookupStrategy();
|
||||
return new CouchbaseQueryLookupStrategy(contextProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy to lookup Couchbase queries implementation to be used.
|
||||
*/
|
||||
private class CouchbaseQueryLookupStrategy implements QueryLookupStrategy {
|
||||
|
||||
private final EvaluationContextProvider evaluationContextProvider;
|
||||
|
||||
public CouchbaseQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider) {
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries) {
|
||||
CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(metadata.getRepositoryInterface(),
|
||||
@@ -191,10 +201,12 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
|
||||
return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations);
|
||||
} else if (queryMethod.hasN1qlAnnotation()) {
|
||||
if (queryMethod.hasInlineN1qlQuery()) {
|
||||
return new StringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations);
|
||||
return new StringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations,
|
||||
SPEL_PARSER, evaluationContextProvider);
|
||||
} else if (namedQueries.hasQuery(namedQueryName)) {
|
||||
String namedQuery = namedQueries.getQuery(namedQueryName);
|
||||
return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations);
|
||||
return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations,
|
||||
SPEL_PARSER, evaluationContextProvider);
|
||||
} //otherwise will do default, queryDerivation
|
||||
}
|
||||
return new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);
|
||||
|
||||
@@ -1,53 +1,120 @@
|
||||
package org.springframework.data.couchbase.repository.query;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.*;
|
||||
|
||||
import com.couchbase.client.java.query.Statement;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
public class StringN1QlBasedQueryTest {
|
||||
|
||||
private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser();
|
||||
private StringN1qlBasedQuery mockStringUnderscoreClass;
|
||||
private StringN1qlBasedQuery mockStringAtClass;
|
||||
|
||||
@Before
|
||||
public void initMock() {
|
||||
N1qlSpelValues contextStringUnderscoreClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "_class", String.class, false);
|
||||
N1qlSpelValues contextCountStringUnderscoreClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "_class", String.class, true);
|
||||
N1qlSpelValues contextStringAtClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "@class", String.class, false);
|
||||
N1qlSpelValues contextCountStringAtClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "@class", String.class, true);
|
||||
|
||||
mockStringUnderscoreClass = mock(StringN1qlBasedQuery.class);
|
||||
when(mockStringUnderscoreClass.parseSpel(anyString(), anyBoolean(), any(Object[].class))).thenCallRealMethod();
|
||||
when(mockStringUnderscoreClass.evaluateSpelExpression(anyString(), anyBoolean(), any(Object[].class)))
|
||||
.thenAnswer(mockSpelEvaluation(contextStringUnderscoreClass, contextCountStringUnderscoreClass));
|
||||
|
||||
mockStringAtClass = mock(StringN1qlBasedQuery.class);
|
||||
when(mockStringAtClass.parseSpel(anyString(),anyBoolean(), any(Object[].class))).thenCallRealMethod();
|
||||
when(mockStringAtClass.evaluateSpelExpression(anyString(), anyBoolean(), any(Object[].class)))
|
||||
.thenAnswer(mockSpelEvaluation(contextStringAtClass, contextCountStringAtClass));
|
||||
}
|
||||
|
||||
private static Answer<?> mockSpelEvaluation(final N1qlSpelValues spelValues, final N1qlSpelValues countSpelValues) {
|
||||
return new Answer<Object>() {
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
Expression expression = SPEL_PARSER.parseExpression((String) invocation.getArguments()[0]);
|
||||
boolean isCount = (Boolean) invocation.getArguments()[1];
|
||||
if (isCount)
|
||||
return doEvaluate(context, expression, countSpelValues);
|
||||
else
|
||||
return doEvaluate(context, expression, spelValues);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static String spel(String expression) {
|
||||
return "${" + expression + "}";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceFullSelectPlaceholderOnce() throws Exception {
|
||||
String statement = PLACEHOLDER_SELECT_FROM + " where " + PLACEHOLDER_SELECT_FROM;
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class, false);
|
||||
public void testReplaceAllFullSelectPlaceholder() throws Exception {
|
||||
String statement = spel(SPEL_SELECT_FROM_CLAUSE) + " where " + spel(SPEL_SELECT_FROM_CLAUSE);
|
||||
String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]);
|
||||
|
||||
assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B` where "
|
||||
+ PLACEHOLDER_SELECT_FROM, parsed.toString());
|
||||
+ "SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B`", parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceAllBucketPlaceholder() throws Exception {
|
||||
String statement = "SELECT * FROM " + PLACEHOLDER_BUCKET + " WHERE " + PLACEHOLDER_BUCKET + ".test = 1";
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class, false);
|
||||
String statement = "SELECT * FROM " + spel(SPEL_BUCKET) + " WHERE " + spel(SPEL_BUCKET) + ".test = 1";
|
||||
String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]);
|
||||
|
||||
assertEquals("SELECT * FROM `B` WHERE `B`.test = 1", parsed.toString());
|
||||
assertEquals("SELECT * FROM `B` WHERE `B`.test = 1", parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceFirstEntityPlaceholder() throws Exception {
|
||||
String statement = "SELECT " + PLACEHOLDER_ENTITY + " FROM b where b.test = 1 and " + PLACEHOLDER_ENTITY;
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "_class", String.class, false);
|
||||
public void testReplaceAllEntityPlaceholder() throws Exception {
|
||||
String statement = "SELECT " + spel(SPEL_ENTITY) + " FROM a where a.test = 1 and " + spel(SPEL_ENTITY);
|
||||
String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]);
|
||||
|
||||
assertEquals("SELECT META(`A`).id AS _ID, META(`A`).cas AS _CAS FROM b where b.test = 1 and "
|
||||
+ PLACEHOLDER_ENTITY, parsed.toString());
|
||||
assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS FROM a where a.test = 1 and "
|
||||
+ "META(`B`).id AS _ID, META(`B`).cas AS _CAS", parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceTypePlaceholder() throws Exception {
|
||||
String statement = "SELECT " + PLACEHOLDER_ENTITY + " FROM b WHERE b.test = 1 AND " + PLACEHOLDER_FILTER_TYPE;
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "@class", String.class, false);
|
||||
String statement = "SELECT " + spel(SPEL_ENTITY) + " FROM a WHERE a.test = 1 AND " + spel(SPEL_FILTER);
|
||||
String parsed = mockStringAtClass.parseSpel(statement, false, new Object[0]);
|
||||
|
||||
assertEquals("SELECT META(`A`).id AS _ID, META(`A`).cas AS _CAS FROM b WHERE b.test = 1 AND `@class` = "
|
||||
+ "\"java.lang.String\"", parsed.toString());
|
||||
assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS FROM a WHERE a.test = 1 AND `@class` = "
|
||||
+ "\"java.lang.String\"", parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceSelectFromPlaceholderWithCountIfCountTrue() {
|
||||
String statement = PLACEHOLDER_SELECT_FROM + " WHERE true";
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "_class", String.class, true);
|
||||
String statement = spel(SPEL_SELECT_FROM_CLAUSE) + " WHERE true";
|
||||
String parsed = mockStringUnderscoreClass.parseSpel(statement, true, new Object[0]);
|
||||
|
||||
assertEquals("SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `A` WHERE true", parsed.toString());
|
||||
assertEquals("SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `B` WHERE true", parsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParsingOfSpel() {
|
||||
StringN1qlBasedQuery mock = mock(StringN1qlBasedQuery.class);
|
||||
when(mock.parseSpel(anyString(), anyBoolean(), any(Object[].class))).thenCallRealMethod();
|
||||
when(mock.evaluateSpelExpression(anyString(), anyBoolean(), any(Object[].class)))
|
||||
.thenReturn("EXPRESSION1", "EXPRESSION2isMuchMoreLongerThanTheTextItReplaces", "EXPRESSION3", "LASTEXPRESSION"
|
||||
);
|
||||
|
||||
String spelStatement = "some statement with ${ad{\"toto\"}.lib} spel ${expression.parsed}${{several{close}}expressions}";
|
||||
String expected = "some statement with EXPRESSION1 spel EXPRESSION2isMuchMoreLongerThanTheTextItReplacesEXPRESSION3";
|
||||
|
||||
String parsed = mock.parseSpel(spelStatement, false, new Object[0]);
|
||||
|
||||
assertEquals(expected, parsed);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@
|
||||
<appender-ref ref="console" />
|
||||
</root>
|
||||
|
||||
<!-- You can use these logger configurations to log more details:
|
||||
- log the Couchbase queries produced by the framework
|
||||
- log details of the parsing of SpEL in N1QL inline queries-->
|
||||
<logger name="org.springframework.data.couchbase.repository.query" level="debug"/>
|
||||
<logger name="org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery" level="trace"/>
|
||||
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user