DATACOUCH-156 - Make generated N1QL queries filter on the type field.
The generated N1QL queries currently don't filter on the type at all. Add a criteria to the WHERE clause that checks the field holding type information is matching the entity fully qualified class name. Add a placeholder for inline N1QL queries that can be replaced by the same type information criteria.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package org.springframework.data.couchbase.repository;
|
||||
|
||||
import com.couchbase.client.java.repository.annotation.Field;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
|
||||
public class Item {
|
||||
|
||||
@Id
|
||||
public String id;
|
||||
|
||||
@Field("desc")
|
||||
public String description;
|
||||
|
||||
public Item(String id, String description) {
|
||||
this.id = id;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Item item = (Item) o;
|
||||
|
||||
if (!id.equals(item.id)) return false;
|
||||
return !(description != null ? !description.equals(item.description) : item.description != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = id.hashCode();
|
||||
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.springframework.data.couchbase.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface ItemRepository extends CrudRepository<Item, String> {
|
||||
|
||||
List<Object> findAllByDescriptionNotNull();
|
||||
}
|
||||
@@ -16,12 +16,15 @@
|
||||
|
||||
package org.springframework.data.couchbase.repository;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.couchbase.client.java.Bucket;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -47,19 +50,50 @@ public class N1qlCrudRepositoryTests {
|
||||
private CouchbaseTemplate template;
|
||||
|
||||
private PartyRepository partyRepository;
|
||||
private ItemRepository itemRepository;
|
||||
|
||||
private static final Item item = new Item("itemNotParty", "short description");
|
||||
private static final Party party = new Party("partyNotItem", "partyName", "short description", new Date(), 120);
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
partyRepository = new CouchbaseRepositoryFactory(template).getRepository(PartyRepository.class);
|
||||
itemRepository = new CouchbaseRepositoryFactory(template).getRepository(ItemRepository.class);
|
||||
|
||||
itemRepository.save(item);
|
||||
partyRepository.save(party);
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanUp() {
|
||||
itemRepository.delete("itemNotParty");
|
||||
partyRepository.delete("partyNotItem");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDistinguishBetweenItemsAndParties() {
|
||||
List<Object> items = itemRepository.findAllByDescriptionNotNull();
|
||||
List<Object> parties = partyRepository.findAllByDescriptionNotNull();
|
||||
|
||||
assertTrue(items.contains(item));
|
||||
assertTrue(parties.contains(party));
|
||||
|
||||
assertFalse(items.contains(party));
|
||||
assertFalse(parties.contains(item));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSaveObjectWithN1qlKeywordField() {
|
||||
Party party = new Party("partyHasKeyword", "party", "desc is a N1QL keyword", new Date(), 40);
|
||||
partyRepository.save(party);
|
||||
List<Party> parties = partyRepository.findAllByDescriptionNotNull();
|
||||
List<Object> parties = partyRepository.findAllByDescriptionNotNull();
|
||||
|
||||
assertTrue(client.exists("partyHasKeyword"));
|
||||
assertTrue(parties.contains(party));
|
||||
for (Object o : parties) {
|
||||
if (!(o instanceof Party)) {
|
||||
fail("expected only Party objects");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ public interface PartyRepository extends CouchbaseRepository<Party, String> {
|
||||
@View(designDocument = "party", viewName = "byDate")
|
||||
List<Party> findFirst3ByEventDateGreaterThanEqual(Date targetDate);
|
||||
|
||||
List<Party> findAllByDescriptionNotNull();
|
||||
List<Object> findAllByDescriptionNotNull();
|
||||
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ Here is an example:
|
||||
----
|
||||
public interface UserRepository extends CrudRepository<UserInfo, String> {
|
||||
|
||||
@Query("$SELECT_ENTITY$ WHERE role = 'admin'")
|
||||
@Query("$SELECT_ENTITY$ WHERE role = 'admin' AND $FILTER_TYPE$")
|
||||
List<UserInfo> findAllAdmins();
|
||||
|
||||
List<UserInfo> findByFirstname(String fname);
|
||||
@@ -122,10 +122,15 @@ 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 placeholder `$SELECT_ENTITY` which allows to easily make sure the statement will select all the fields necessary to build the full entity (including document ID and CAS value).
|
||||
The first one uses the `Query` annotation to provide a N1QL statement inline. Notice the special placeholders:
|
||||
|
||||
- `$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.
|
||||
|
||||
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`...
|
||||
|
||||
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.
|
||||
|
||||
Most Spring-Data keywords are supported:
|
||||
.Supported keywords inside @Query (N1QL) method names
|
||||
[options = "header, autowidth"]
|
||||
|
||||
@@ -51,4 +51,9 @@ public interface CouchbaseConverter
|
||||
* @see #convertForWriteIfNeeded(Object)
|
||||
*/
|
||||
Class<?> getWriteClassFor(Class<?> clazz);
|
||||
|
||||
/**
|
||||
* @return the name of the field that will hold type information.
|
||||
*/
|
||||
String getTypeKey();
|
||||
}
|
||||
|
||||
@@ -130,9 +130,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return mappingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name of the field that will hold type information.
|
||||
*/
|
||||
@Override
|
||||
public String getTypeKey() {
|
||||
return typeMapper.getTypeKey();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.data.couchbase.repository.query;
|
||||
|
||||
import static com.couchbase.client.java.query.dsl.Expression.i;
|
||||
import static com.couchbase.client.java.query.dsl.Expression.s;
|
||||
import static com.couchbase.client.java.query.dsl.Expression.x;
|
||||
|
||||
@@ -93,12 +94,14 @@ public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression
|
||||
|
||||
private final WherePath selectFrom;
|
||||
private final CouchbaseConverter converter;
|
||||
private final CouchbaseQueryMethod queryMethod;
|
||||
|
||||
public N1qlQueryCreator(PartTree tree, ParameterAccessor parameters, WherePath selectFrom,
|
||||
CouchbaseConverter converter) {
|
||||
CouchbaseConverter converter, CouchbaseQueryMethod queryMethod) {
|
||||
super(tree, parameters);
|
||||
this.selectFrom = selectFrom;
|
||||
this.converter = converter;
|
||||
this.queryMethod = queryMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,6 +125,16 @@ public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression
|
||||
|
||||
@Override
|
||||
protected LimitPath complete(Expression criteria, Sort sort) {
|
||||
//add part that filters on type key
|
||||
String typeKey = converter.getTypeKey();
|
||||
String typeValue = queryMethod.getEntityInformation().getJavaType().getName();
|
||||
Expression typeSelector = i(typeKey).eq(s(typeValue));
|
||||
if (criteria == null) {
|
||||
criteria = typeSelector;
|
||||
} else {
|
||||
criteria = criteria.and(typeSelector);
|
||||
}
|
||||
|
||||
OrderByPath selectFromWhere = selectFrom.where(criteria);
|
||||
|
||||
if (sort != null) {
|
||||
|
||||
@@ -61,7 +61,8 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery {
|
||||
}
|
||||
WherePath selectFrom = select.from(bucket);
|
||||
|
||||
N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom, getCouchbaseOperations().getConverter());
|
||||
N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom,
|
||||
getCouchbaseOperations().getConverter(), getQueryMethod());
|
||||
LimitPath selectFromWhereOrderBy = queryCreator.createQuery();
|
||||
|
||||
if (partTree.isLimiting()) {
|
||||
|
||||
@@ -52,18 +52,29 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
|
||||
*/
|
||||
public static final String PLACEHOLDER_ENTITY = "$ENTITY$";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static final String PLACEHOLDER_FILTER_TYPE = "$FILTER_TYPE$";
|
||||
|
||||
private final Statement statement;
|
||||
|
||||
public StringN1qlBasedQuery(String statement, CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) {
|
||||
super(queryMethod, couchbaseOperations);
|
||||
this.statement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name());
|
||||
String typeField = getCouchbaseOperations().getConverter().getTypeKey();
|
||||
Class<?> typeValue = getQueryMethod().getEntityInformation().getJavaType();
|
||||
this.statement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name(), typeField, typeValue);
|
||||
}
|
||||
|
||||
protected static Statement prepare(String statement, String bucketName) {
|
||||
protected static Statement prepare(String statement, String bucketName, String typeField, Class<?> typeValue) {
|
||||
String b = "`" + bucketName + "`";
|
||||
String entity = "META(" + b + ").id AS " + CouchbaseOperations.SELECT_ID +
|
||||
", META(" + b + ").cas AS " + CouchbaseOperations.SELECT_CAS;
|
||||
String selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b;
|
||||
String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\"";
|
||||
|
||||
String result = statement;
|
||||
if (statement.contains(PLACEHOLDER_SELECT_FROM)) {
|
||||
result = result.replaceFirst("\\$SELECT_ENTITY\\$", selectEntity);
|
||||
@@ -75,6 +86,11 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
|
||||
result = result.replaceFirst("\\$ENTITY\\$", entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (statement.contains(PLACEHOLDER_FILTER_TYPE)) {
|
||||
result = result.replaceFirst("\\$FILTER_TYPE\\$", typeSelection);
|
||||
}
|
||||
|
||||
return Query.simple(result).statement();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.springframework.data.couchbase.repository.query;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.PLACEHOLDER_BUCKET;
|
||||
import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.PLACEHOLDER_ENTITY;
|
||||
import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.PLACEHOLDER_SELECT_FROM;
|
||||
import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.*;
|
||||
|
||||
import com.couchbase.client.java.query.Statement;
|
||||
import org.junit.Test;
|
||||
@@ -13,7 +11,7 @@ public class StringN1QlBasedQueryTest {
|
||||
@Test
|
||||
public void testReplaceFullSelectPlaceholderOnce() throws Exception {
|
||||
String statement = PLACEHOLDER_SELECT_FROM + " where " + PLACEHOLDER_SELECT_FROM;
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "B");
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class);
|
||||
|
||||
assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B` where "
|
||||
+ PLACEHOLDER_SELECT_FROM, parsed.toString());
|
||||
@@ -22,7 +20,7 @@ public class StringN1QlBasedQueryTest {
|
||||
@Test
|
||||
public void testReplaceAllBucketPlaceholder() throws Exception {
|
||||
String statement = "SELECT * FROM " + PLACEHOLDER_BUCKET + " WHERE " + PLACEHOLDER_BUCKET + ".test = 1";
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "B");
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class);
|
||||
|
||||
assertEquals("SELECT * FROM `B` WHERE `B`.test = 1", parsed.toString());
|
||||
}
|
||||
@@ -30,9 +28,18 @@ public class StringN1QlBasedQueryTest {
|
||||
@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");
|
||||
Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "_class", String.class);
|
||||
|
||||
assertEquals("SELECT META(`A`).id AS _ID, META(`A`).cas AS _CAS FROM b where b.test = 1 and "
|
||||
+ PLACEHOLDER_ENTITY, parsed.toString());
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user