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:
Simon Baslé
2015-07-31 18:24:40 +02:00
parent 60196095a6
commit f57a03ad21
11 changed files with 144 additions and 17 deletions

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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");
}
}
}
}

View File

@@ -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();
}

View File

@@ -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"]

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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) {

View File

@@ -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()) {

View File

@@ -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();
}

View File

@@ -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());
}
}