diff --git a/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedFooRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedFooRepository.java new file mode 100644 index 00000000..c6f7e615 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedFooRepository.java @@ -0,0 +1,26 @@ +package org.springframework.data.couchbase.repository.index; + +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.query.ViewIndexed; +import org.springframework.data.couchbase.repository.CouchbaseRepository; + +@ViewIndexed(designDoc = "foo") +public interface IndexedFooRepository extends CouchbaseRepository { + + @Document + final class Foo { + @Id + private String id; + + private String value1; + + private int value2; + + public Foo(String id, String value1, int value2) { + this.id = id; + this.value1 = value1; + this.value2 = value2; + } + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTestListener.java b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTestListener.java index 9d060549..ab13eef5 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTestListener.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTestListener.java @@ -18,6 +18,7 @@ public class IndexedRepositoryTestListener extends DependencyInjectionTestExecut public void beforeTestClass(final TestContext testContext) throws Exception { Bucket client = (Bucket) testContext.getApplicationContext().getBean("couchbaseBucket"); client.bucketManager().removeDesignDocument(IndexedRepositoryTests.VIEW_DOC); + client.bucketManager().removeDesignDocument("foo"); client.query(N1qlQuery.simple(Index.dropPrimaryIndex(client.name()))); client.query(N1qlQuery.simple(Index.dropIndex(client.name(), IndexedRepositoryTests.SECONDARY))); } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTests.java index 016b0130..452b60ce 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTests.java @@ -18,6 +18,8 @@ package org.springframework.data.couchbase.repository.index; import static org.junit.Assert.*; +import java.util.Arrays; + import com.couchbase.client.java.query.N1qlQuery; import com.couchbase.client.java.query.N1qlQueryResult; import com.couchbase.client.java.view.DesignDocument; @@ -144,4 +146,40 @@ public class IndexedRepositoryTests { } } } + + @Test + public void shouldFindListOfIdsThroughDefaulViewIndexed() { + IndexedFooRepository.Foo foo1 = new IndexedFooRepository.Foo("foo1", "foo", 1); + IndexedFooRepository.Foo foo2 = new IndexedFooRepository.Foo("foo2", "bar", 2); + + IndexedFooRepository repository = factory.getRepository(IndexedFooRepository.class); + + DesignDocument designDoc = template.getCouchbaseBucket() + .bucketManager() + .getDesignDocument("foo"); + + assertNotNull(designDoc); + boolean foundView = false; + for (View view : designDoc.views()) { + if (view.name().equals("all")) { + foundView = true; + break; + } + } + assertTrue("Expected to find view \"all\" on design document \"foo\"", foundView); + + repository.save(foo1); + repository.save(foo2); + + int count = 0; + for (Object o : repository.findAll(Arrays.asList("foo1", "foo2"))) { + count++; + } + assertEquals(2L, count); + count = 0; + for (Object o : repository.findAll(Arrays.asList("foo1", "foo3"))) { + count++; + } + assertEquals(1L, count); + } } \ No newline at end of file diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 4cf78307..f0f27be0 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -269,7 +269,7 @@ Since every view has a design document and view name, by convention we default t // do not forget the _count reduce function! function (doc, meta) { if (doc._class == "namespace.to.entity.UserInfo") { - emit(null, null); + emit(meta.id, null); } } ---- @@ -277,7 +277,7 @@ function (doc, meta) { Note that the important part in this map function is to only include the document IDs which correspond to our entity. Because the library always adds the `_class` property, this is a quick and easy way to do it. If you have another property in your JSON which does the same job (like a explicit `type` field), then you can use that as well - you don't have to stick to `_class` all the time. -Also make sure to publish your design documents into production so that they can be picked up by the library! Also, if you are curious why we use `emit(null, null)` in the view: the document id is always sent over to the client implicitly, so we can shave off a view bytes in our view by not duplicating the id. If you use `emit(meta.id, null)` it won't hurt much too. +Also make sure to publish your design documents into production so that they can be picked up by the library! Also, if you are curious why we use `emit(meta.id, null)` in the view despite the document id being always sent over to the client implicitly, it is so the view can be queried with a list of ids, eg. in the `findAll(Iterable ids)` CRUD method. [[couchbase.repository.indexing]] === Automatic Index Management @@ -286,7 +286,7 @@ In order for the CRUD operations to work, the adequate view must have been creat In the case where the index creation cost isn't considered too high, it can be triggered automatically instead. You just need to annotate the repositories you want managed with the relevant annotation(s): - - `@ViewIndexed` will create a view like the "all" view previously seen, to list all entities in the bucket. It can also be used on methods, allowing multiple views to be created with custom map function and reduce function. + - `@ViewIndexed` will create a view like the "all" view previously seen, to list all entities in the bucket. - `@N1qlPrimaryIndexed` can be used to ensure a general-purpose PRIMARY INDEX is available in N1QL. - `@N1qlSecondaryIndexed` will create a more specific N1QL index that does the same kind of filtering on entity type that the view does. It'll allow for efficient listing of all documents that correspond to a Repository's associated domain object. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java b/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java index e3274a53..b3ffa670 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java @@ -29,24 +29,25 @@ import java.lang.annotation.Target; * The view must at least be described as a design document name and view name. Default map function * will filter documents on the type associated to the repository, and default reduce function is "_count". *

- * One can specify a custom reduce function as well as a non-default map function. This can be done on methods, - * allowing for multiple views per repository to be created. + * One can specify a custom reduce function as well as a non-default map function. * * @author Simon Baslé */ -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface ViewIndexed { /** - * The design document in which to create/look for the view. + * The design document in which to create/look for the view. Usually to create the backing view for CRUD + * methods, the expected designDoc value is the entity's simple class name, with a lowercase first letter + * (eg. a UserInfo repository would expect a design document named "userInfo"). */ String designDoc(); /** - * The name of the view. + * The name of the view, defaults to "all" (which is what CRUD methods expect by default). */ - String viewName(); + String viewName() default "all"; /** * The map function to use (default is empty, which will trigger a default map function filtering on the diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/IndexManager.java b/src/main/java/org/springframework/data/couchbase/repository/support/IndexManager.java index fe301272..27177a73 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/IndexManager.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/IndexManager.java @@ -56,7 +56,7 @@ public class IndexManager { private static final Logger LOGGER = LoggerFactory.getLogger(IndexManager.class); - private static final String TEMPLATE_MAP_FUNCTION = "function (doc, meta) { if(doc.%s == \"%s\") { emit(null, null); } }"; + private static final String TEMPLATE_MAP_FUNCTION = "function (doc, meta) { if(doc.%s == \"%s\") { emit(meta.id, null); } }"; private static final JsonObject SUCCESS_MARKER = JsonObject.empty();