diff --git a/src/integration/java/org/springframework/data/couchbase/repository/Item.java b/src/integration/java/org/springframework/data/couchbase/repository/Item.java new file mode 100644 index 00000000..7e5b3638 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/Item.java @@ -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; + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/ItemRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/ItemRepository.java new file mode 100644 index 00000000..3e75a461 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/ItemRepository.java @@ -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 { + + List findAllByDescriptionNotNull(); +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCrudRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCrudRepositoryTests.java index 522de35d..94efb4b7 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCrudRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCrudRepositoryTests.java @@ -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 items = itemRepository.findAllByDescriptionNotNull(); + List 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 parties = partyRepository.findAllByDescriptionNotNull(); + List parties = partyRepository.findAllByDescriptionNotNull(); assertTrue(client.exists("partyHasKeyword")); assertTrue(parties.contains(party)); + for (Object o : parties) { + if (!(o instanceof Party)) { + fail("expected only Party objects"); + } + } } } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java index 072a3315..905fff99 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java @@ -17,6 +17,6 @@ public interface PartyRepository extends CouchbaseRepository { @View(designDocument = "party", viewName = "byDate") List findFirst3ByEventDateGreaterThanEqual(Date targetDate); - List findAllByDescriptionNotNull(); + List findAllByDescriptionNotNull(); } diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 00507949..4d84a33c 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -112,7 +112,7 @@ Here is an example: ---- public interface UserRepository extends CrudRepository { - @Query("$SELECT_ENTITY$ WHERE role = 'admin'") + @Query("$SELECT_ENTITY$ WHERE role = 'admin' AND $FILTER_TYPE$") List findAllAdmins(); List findByFirstname(String fname); @@ -122,10 +122,15 @@ public interface UserRepository extends CrudRepository { 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"] diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java index d2cc1c96..a9ef12d2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java @@ -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(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index c2837ee0..e8917f4f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -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(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java index 2caee9dc..0a0d71c5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java @@ -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 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(); } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java index ef1a9a23..4d773a77 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java @@ -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()); + } } \ No newline at end of file