DATACOUCH-141 - Change query lookup order and make N1QL default.
The @N1QL annotation was changed to @Query, for discoverability and also convey that this is now the preferred method. Order is now view if @View present, N1ql inline if @Query present with a Statement, N1ql query derivation if @Query without attribute or no annotation. @View can be without attribute in order to compute the view and design document from method name.
This commit is contained in:
@@ -18,6 +18,8 @@ package org.springframework.data.couchbase.repository;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.couchbase.client.java.Bucket;
|
||||
import com.couchbase.client.java.view.Stale;
|
||||
@@ -74,9 +76,14 @@ public class CouchbaseRepositoryViewTests {
|
||||
assertThat(value, is(100L));
|
||||
}
|
||||
|
||||
@Test(expected = InvalidDataAccessResourceUsageException.class)
|
||||
@Test
|
||||
public void shouldTrimOffFindOnCustomFinder() {
|
||||
repository.findAllSomething();
|
||||
try {
|
||||
repository.findAllSomething();
|
||||
fail("Expected InvalidDataAccessResourceException");
|
||||
} catch (InvalidDataAccessResourceUsageException e) {
|
||||
assertTrue(e.getMessage(), e.getMessage().startsWith("View user/allSomething does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public interface CustomUserRepository extends CouchbaseRepository<User, String>
|
||||
@View(designDocument = "userCustom", viewName = "customCountView")
|
||||
long count();
|
||||
|
||||
@View
|
||||
Iterable<User> findAllSomething();
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.data.couchbase.repository;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.couchbase.client.java.Bucket;
|
||||
import com.couchbase.client.java.view.Stale;
|
||||
@@ -146,4 +147,26 @@ public class SimpleCouchbaseRepositoryTests {
|
||||
assertEquals("uname-2", user.getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFindContainsWithoutAnnotation() {
|
||||
List<User> users = repository.findByUsernameContains("-9");
|
||||
assertNotNull(users);
|
||||
assertFalse(users.isEmpty());
|
||||
for (User user : users) {
|
||||
assertTrue(user.getUsername().startsWith("uname-9"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDefaultToN1qlQueryDerivation() {
|
||||
try {
|
||||
User u = repository.findByUsernameNear("london");
|
||||
fail("Expected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (!e.getMessage().contains("N1QL")) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.util.List;
|
||||
|
||||
import com.couchbase.client.java.view.ViewQuery;
|
||||
|
||||
import org.springframework.data.couchbase.core.view.N1QL;
|
||||
import org.springframework.data.couchbase.core.view.Query;
|
||||
import org.springframework.data.couchbase.core.view.View;
|
||||
|
||||
/**
|
||||
@@ -31,13 +31,16 @@ public interface UserRepository extends CouchbaseRepository<User, String> {
|
||||
@View(designDocument = "user", viewName = "all")
|
||||
Iterable<User> customViewQuery(ViewQuery query);
|
||||
|
||||
@N1QL("$SELECT_ENTITY$ WHERE username = $1")
|
||||
@Query("$SELECT_ENTITY$ WHERE username = $1")
|
||||
User findByUsername(String username);
|
||||
|
||||
@N1QL("SELECT * FROM $BUCKET$ WHERE username = $1")
|
||||
@Query("SELECT * FROM $BUCKET$ WHERE username = $1")
|
||||
User findByUsernameBadSelect(String username);
|
||||
|
||||
@N1QL
|
||||
@Query
|
||||
User findByUsernameRegexAndUsernameIn(String regex, List<String> sample);
|
||||
|
||||
List<User> findByUsernameContains(String contains);
|
||||
|
||||
User findByUsernameNear(String place);//this is to check that there's a N1QL derivation AND it fails
|
||||
}
|
||||
|
||||
@@ -24,17 +24,32 @@ import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.data.annotation.QueryAnnotation;
|
||||
import org.springframework.data.couchbase.core.CouchbaseTemplate;
|
||||
import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
|
||||
|
||||
/**
|
||||
* Annotation to support the use of N1QL queries with Couchbase.
|
||||
*
|
||||
* <p/>
|
||||
* Using it without parameter will resolve the query from the method name. Providing a value
|
||||
* (an inline N1QL statement) will execute that statement instead.
|
||||
*
|
||||
* <p/>
|
||||
* In this case, one can use a placeholder notation of {@code ?0}, {@code ?1} and so on.
|
||||
*
|
||||
* Also, the placeholder {@code $BUCKET$} can be used to be replaced by the underlying {@link CouchbaseTemplate}
|
||||
* associated bucket name.
|
||||
* <p/>
|
||||
* Also, the following placeholders can be used to be replaced by the underlying {@link CouchbaseTemplate}
|
||||
* associated information:
|
||||
* <ul>
|
||||
* <li>
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_SELECT_FROM}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_SELECT_FROM})
|
||||
* </li>
|
||||
* <li>
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_BUCKET}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_BUCKET})
|
||||
* </li>
|
||||
* <li>
|
||||
* {@value StringN1qlBasedQuery#PLACEHOLDER_ENTITY}
|
||||
* (see {@link StringN1qlBasedQuery#PLACEHOLDER_ENTITY})
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Simon Baslé.
|
||||
*/
|
||||
@@ -42,12 +57,12 @@ import org.springframework.data.couchbase.core.CouchbaseTemplate;
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@QueryAnnotation
|
||||
public @interface N1QL {
|
||||
public @interface Query {
|
||||
|
||||
/**
|
||||
* Takes a N1QL statement string to define the actual query to be executed. This one will take precedence over the
|
||||
* method name then.
|
||||
*/
|
||||
String value() default "";
|
||||
/**
|
||||
* Takes a N1QL statement string to define the actual query to be executed. This one will take precedence over the
|
||||
* method name then.
|
||||
*/
|
||||
String value() default "";
|
||||
|
||||
}
|
||||
@@ -35,19 +35,19 @@ public @interface View {
|
||||
/**
|
||||
* The name of the Design Document to use.
|
||||
* <p/>
|
||||
* This field is mandatory.
|
||||
* If the {@link #viewName()} field is set, this field is mandatory.
|
||||
*
|
||||
* @return name of the Design Document.
|
||||
*/
|
||||
String designDocument();
|
||||
String designDocument() default "";
|
||||
|
||||
/**
|
||||
* The name of the View to use.
|
||||
* <p/>
|
||||
* This field is mandatory.
|
||||
* If the {@link #designDocument()} field is set, his field is mandatory.
|
||||
*
|
||||
* @return name of the View
|
||||
*/
|
||||
String viewName();
|
||||
String viewName() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.lang.reflect.Method;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
|
||||
import org.springframework.data.couchbase.core.view.N1QL;
|
||||
import org.springframework.data.couchbase.core.view.Query;
|
||||
import org.springframework.data.couchbase.core.view.View;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
@@ -57,6 +57,19 @@ public class CouchbaseQueryMethod extends QueryMethod {
|
||||
return getViewAnnotation() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the method has a @View annotation with the designDocument and viewName specified.
|
||||
*
|
||||
* @return true if it has the annotation and full view specified.
|
||||
*/
|
||||
public boolean hasViewSpecification() {
|
||||
View annotation = getViewAnnotation();
|
||||
if (annotation == null) {
|
||||
return false;
|
||||
}
|
||||
return StringUtils.hasText(annotation.designDocument()) && StringUtils.hasText(annotation.viewName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the @View annotation if set, null otherwise.
|
||||
*
|
||||
@@ -67,7 +80,7 @@ public class CouchbaseQueryMethod extends QueryMethod {
|
||||
}
|
||||
|
||||
/**
|
||||
* If the method has a @N1QL annotation.
|
||||
* If the method has a @Query annotation.
|
||||
*
|
||||
* @return true if it has the annotation, false otherwise.
|
||||
*/
|
||||
@@ -76,16 +89,16 @@ public class CouchbaseQueryMethod extends QueryMethod {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the @N1QL annotation if set, null otherwise.
|
||||
* Returns the @Query annotation if set, null otherwise.
|
||||
*
|
||||
* @return the n1ql annotation if present.
|
||||
*/
|
||||
public N1QL getN1qlAnnotation() {
|
||||
return method.getAnnotation(N1QL.class);
|
||||
public Query getN1qlAnnotation() {
|
||||
return method.getAnnotation(Query.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the method has a @N1QL annotation with an inline N1QL statement inside.
|
||||
* If the method has a @Query annotation with an inline Query statement inside.
|
||||
*
|
||||
* @return true if this has the annotation and N1QL inline statement, false otherwise.
|
||||
*/
|
||||
@@ -94,7 +107,7 @@ public class CouchbaseQueryMethod extends QueryMethod {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query string declared in a {@link N1QL} annotation or {@literal null} if neither the annotation found
|
||||
* Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation found
|
||||
* nor the attribute was specified.
|
||||
*
|
||||
* @return the query statement if present.
|
||||
|
||||
@@ -218,7 +218,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression
|
||||
case WITHIN:
|
||||
case NEAR:
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported keyword");
|
||||
throw new IllegalArgumentException("Unsupported keyword in N1QL query derivation");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,22 @@ import org.springframework.data.repository.query.RepositoryQuery;
|
||||
*/
|
||||
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$";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static final String PLACEHOLDER_BUCKET = "$BUCKET$";
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
public static final String PLACEHOLDER_ENTITY = "$ENTITY$";
|
||||
|
||||
private final Statement statement;
|
||||
|
||||
@@ -69,11 +69,13 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
|
||||
* @return the design document name.
|
||||
*/
|
||||
private String designDocName() {
|
||||
if (method.hasViewAnnotation()) {
|
||||
return method.getViewAnnotation().designDocument();
|
||||
} else {
|
||||
return StringUtils.uncapitalize(method.getEntityInformation().getJavaType().getSimpleName());
|
||||
}
|
||||
if (method.hasViewSpecification()) {
|
||||
return method.getViewAnnotation().designDocument();
|
||||
} else if (method.hasViewAnnotation()) {
|
||||
return StringUtils.uncapitalize(method.getEntityInformation().getJavaType().getSimpleName());
|
||||
} else {
|
||||
throw new IllegalStateException("View-based query should only happen on a method with @View annotation");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,10 +84,12 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
|
||||
* @return the view name.
|
||||
*/
|
||||
private String viewName() {
|
||||
if (method.hasViewAnnotation()) {
|
||||
if (method.hasViewSpecification()) {
|
||||
return method.getViewAnnotation().viewName();
|
||||
} else {
|
||||
} else if (method.hasViewAnnotation()) {
|
||||
return StringUtils.uncapitalize(method.getName().replaceFirst("find", ""));
|
||||
} else {
|
||||
throw new IllegalStateException("View-based query should only happen on a method with @View annotation");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,17 +136,17 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
|
||||
CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, mappingContext);
|
||||
String namedQueryName = queryMethod.getNamedQueryName();
|
||||
|
||||
if (queryMethod.hasN1qlAnnotation()) {
|
||||
if (queryMethod.hasViewAnnotation()) {
|
||||
return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations);
|
||||
} else if (queryMethod.hasN1qlAnnotation()) {
|
||||
if (queryMethod.hasInlineN1qlQuery()) {
|
||||
return new StringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations);
|
||||
} else if (namedQueries.hasQuery(namedQueryName)) {
|
||||
String namedQuery = namedQueries.getQuery(namedQueryName);
|
||||
return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations);
|
||||
} else {
|
||||
return new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);
|
||||
}
|
||||
} //otherwise will do default, queryDerivation
|
||||
}
|
||||
return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations);
|
||||
return new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user