diff --git a/src/integration/java/org/springframework/data/couchbase/repository/base/RepositoryBaseTest.java b/src/integration/java/org/springframework/data/couchbase/repository/extending/base/RepositoryBaseTest.java similarity index 79% rename from src/integration/java/org/springframework/data/couchbase/repository/base/RepositoryBaseTest.java rename to src/integration/java/org/springframework/data/couchbase/repository/extending/base/RepositoryBaseTest.java index 8d3b6878..c85ae9fb 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/base/RepositoryBaseTest.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/extending/base/RepositoryBaseTest.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package org.springframework.data.couchbase.repository.base; +package org.springframework.data.couchbase.repository.extending.base; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -36,9 +37,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.annotation.Id; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.couchbase.repository.base.impl.MyRepository; -import org.springframework.data.couchbase.repository.base.impl.MyRepositoryImpl; +import org.springframework.data.couchbase.repository.User; +import org.springframework.data.couchbase.repository.UserRepository; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.extending.base.impl.MyRepository; +import org.springframework.data.couchbase.repository.extending.base.impl.MyRepositoryImpl; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -67,10 +70,17 @@ public class RepositoryBaseTest { @Autowired ItemRepository repositoryA; + @Autowired + UserRepository repositoryB; + public interface ItemRepository extends MyRepository { // } + public interface UserRepository extends MyRepository { + // + } + @Configuration @EnableCouchbaseRepositories(considerNestedRepositories = true, repositoryBaseClass = MyRepositoryImpl.class) static class Config extends AbstractCouchbaseConfiguration { @@ -99,9 +109,13 @@ public class RepositoryBaseTest { @Test public void testRepositoryBaseIsChanged() { assertNotNull(repositoryA); + assertNotNull(repositoryB); assertEquals(4, repositoryA.sharedCustomMethod("toto")); assertEquals(4000, repositoryA.sharedCustomMethod("anna")); + + assertEquals(repositoryA.sharedCustomMethod("sameInput"), repositoryB.sharedCustomMethod("sameInput")); + assertEquals(repositoryA.sharedCustomMethod("anna"), repositoryB.sharedCustomMethod("anna")); } private static class Item { diff --git a/src/integration/java/org/springframework/data/couchbase/repository/base/impl/MyRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/extending/base/impl/MyRepository.java similarity index 80% rename from src/integration/java/org/springframework/data/couchbase/repository/base/impl/MyRepository.java rename to src/integration/java/org/springframework/data/couchbase/repository/extending/base/impl/MyRepository.java index fabf896d..9177a6b4 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/base/impl/MyRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/extending/base/impl/MyRepository.java @@ -1,4 +1,4 @@ -package org.springframework.data.couchbase.repository.base.impl; +package org.springframework.data.couchbase.repository.extending.base.impl; import java.io.Serializable; diff --git a/src/integration/java/org/springframework/data/couchbase/repository/base/impl/MyRepositoryImpl.java b/src/integration/java/org/springframework/data/couchbase/repository/extending/base/impl/MyRepositoryImpl.java similarity index 82% rename from src/integration/java/org/springframework/data/couchbase/repository/base/impl/MyRepositoryImpl.java rename to src/integration/java/org/springframework/data/couchbase/repository/extending/base/impl/MyRepositoryImpl.java index 91eb3d5d..3958946e 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/base/impl/MyRepositoryImpl.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/extending/base/impl/MyRepositoryImpl.java @@ -1,8 +1,9 @@ -package org.springframework.data.couchbase.repository.base.impl; +package org.springframework.data.couchbase.repository.extending.base.impl; import java.io.Serializable; import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.repository.extending.base.impl.MyRepository; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.couchbase.repository.support.N1qlCouchbaseRepository; diff --git a/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyItem.java b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyItem.java new file mode 100644 index 00000000..5561fada --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyItem.java @@ -0,0 +1,15 @@ +package org.springframework.data.couchbase.repository.extending.method; + +import org.springframework.data.annotation.Id; + +class MyItem { + @Id + public final String id; + + public final String value; + + public MyItem(String id, String value) { + this.id = id; + this.value = value; + } +} \ No newline at end of file diff --git a/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepository.java new file mode 100644 index 00000000..1f1fe452 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepository.java @@ -0,0 +1,10 @@ +package org.springframework.data.couchbase.repository.extending.method; + +import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; +import org.springframework.data.couchbase.core.query.ViewIndexed; +import org.springframework.data.repository.CrudRepository; + +@N1qlPrimaryIndexed +@ViewIndexed(designDoc = "myItem", viewName = "all") +public interface MyRepository extends CrudRepository, MyRepositoryCustom { +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepositoryCustom.java b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepositoryCustom.java new file mode 100644 index 00000000..006bf811 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepositoryCustom.java @@ -0,0 +1,7 @@ +package org.springframework.data.couchbase.repository.extending.method; + +public interface MyRepositoryCustom { + + long customCountItems(); + +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepositoryImpl.java b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepositoryImpl.java new file mode 100644 index 00000000..acd158c6 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepositoryImpl.java @@ -0,0 +1,58 @@ +package org.springframework.data.couchbase.repository.extending.method; + +import java.util.List; + +import com.couchbase.client.java.query.N1qlParams; +import com.couchbase.client.java.query.N1qlQuery; +import com.couchbase.client.java.query.Statement; +import com.couchbase.client.java.query.consistency.ScanConsistency; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.repository.Item; +import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.query.CountFragment; +import org.springframework.data.couchbase.repository.query.support.N1qlUtils; +import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; + +public class MyRepositoryImpl implements MyRepositoryCustom { + + @Autowired + RepositoryOperationsMapping templateProvider; + + @Override + public long customCountItems() { + CouchbaseOperations template = templateProvider.resolve(MyRepository.class, Item.class); + + CouchbasePersistentEntity itemPersistenceEntity = (CouchbasePersistentEntity) + template.getConverter() + .getMappingContext() + .getPersistentEntity(MyItem.class); + + CouchbaseEntityInformation itemEntityInformation = + new MappingCouchbaseEntityInformation(itemPersistenceEntity); + + Statement countStatement = N1qlUtils.createCountQueryForEntity( + template.getCouchbaseBucket().name(), + template.getConverter(), + itemEntityInformation); + + ScanConsistency consistency = template.getDefaultConsistency().n1qlConsistency(); + N1qlParams queryParams = N1qlParams.build().consistency(consistency); + N1qlQuery query = N1qlQuery.simple(countStatement, queryParams); + + List countFragments = template.findByN1QLProjection(query, CountFragment.class); + + if (countFragments == null || countFragments.isEmpty()) { + return 0L; + } else { + return countFragments.get(0).count * -1L; + } + } + + public long count() { + return 100; + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/extending/method/RepositoryCustomMethodTest.java b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/RepositoryCustomMethodTest.java new file mode 100644 index 00000000..0a0d6fa3 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/extending/method/RepositoryCustomMethodTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.repository.extending.method; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.query.Consistency; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * This tests custom repository methods. + * + * @author Simon Baslé + */ +@SuppressWarnings("SpringJavaAutowiringInspection") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class RepositoryCustomMethodTest { + + @Autowired + MyRepository repository; + + @Configuration + @EnableCouchbaseRepositories + static class Config extends AbstractCouchbaseConfiguration { + + @Override + protected List getBootstrapHosts() { + return Arrays.asList("127.0.0.1"); + } + + @Override + protected String getBucketName() { + return "default"; + } + + @Override + protected String getBucketPassword() { + return ""; + } + + @Override + protected Consistency getDefaultConsistency() { + return Consistency.STRONGLY_CONSISTENT; + } + } + + private static final String KEY = "customMethodTestItem"; + + @Before + public void initData() { + try { repository.delete(KEY); } catch (Exception e) { } + repository.save(new MyItem(KEY, "new item for custom count")); + } + + @After + public void clearData() { + repository.delete(KEY); + } + + @Test + public void testRepositoryCustomMethodIsWeavedIn() { + long customCount = repository.customCountItems(); + assertEquals(-1L, customCount); + } + + @Test + public void testRepositoryCrudMethodIsReplaced() { + long count = repository.count(); + assertEquals(100L, count); + } + +} diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 19e533f2..b2350f35 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -317,7 +317,7 @@ List users = repo.findByFirstnameStartingWith("Mich"); On all these derived custom finder methods, you have to use the `@View` annotation with at least the view name specified (and you can also override the design document name, otherwise determined by convention). -IMPORTANT: For any other usage and customization of the `ViewQuery` that goes beyond that, recommended approach is to provide an implementation that uses the underlying template, like described in <>. +IMPORTANT: For any other usage and customization of the `ViewQuery` that goes beyond that, recommended approach is to provide an implementation that uses the underlying template, like described in <>. For more details on behavior, please consult the Couchbase Server and Java SDK documentation directly. For view-based query derivation, here are the supported keywords (A and B are method parameters in this table): @@ -395,7 +395,7 @@ IMPORTANT: This is **only used in repositories**, either for index-backed method ==== Provide an implementation Provide the implementation and directly use `queryView` and `queryN1QL` methods on the template with a specific consistency -(see <>). +(see <>). - one can specify the consistency on those via their respective query classes, according to the Couchbase Java SDK documentation. - for example for views `ViewQuery.stale(Stale.FALSE)` @@ -456,3 +456,122 @@ public class ConcreteCouchbaseConfig extends AbstractCouchbaseConfig { } ---- ==== + +[[couchbase.repository.changing-repository-behaviour]] +== Changing repository behaviour +Sometimes you don't simply want the repository to create methods for you, but instead you want to tune the base repository's behaviour. You can either do that for *all* repositories - by changing the _base class_ for them - or just for a single repository - by adding custom implementations for either new or existing methods - (see <> for a generic introduction to these concepts). + +=== Couchbase specifics about changing the base class +This follows the standard procedure for changing all repositories' base class: + +. Create an generic interface for your base that extends `CouchbaseRepository` (CRUD) or `CouchbasePagingAndSortingRepository`. Declare any method you want to add to all repositories there. +. Create an implementation (eg. `MyRepositoryImpl`). This should extend one the concrete base classes (`SimpleCouchbaseRepository` or `N1qlCouchbaseRepository`) and you can also override existing methods from the Spring Data interfaces. +. Declare your repository interfaces as extending `MyRepository` instead of eg. `CRUDRepository` or `CouchbaseRepository`. +. In the `@EnableCouchbaseRepositories` annotation of your configuration, use the `repositoryBaseClass` parameter. + +Here is a complete example that you can find in `RepositoryBaseTest` in the integration tests: + +.Changing repository base class +[source,java] +---- +@NoRepositoryBean <1> +public interface MyRepository extends CouchbaseRepository { <2> + + int sharedCustomMethod(ID id); <3> +} + +public class MyRepositoryImpl + extends N1qlCouchbaseRepository <4> + implements MyRepository { <5> + + public MyRepositoryImpl(CouchbaseEntityInformation metadata, CouchbaseOperations couchbaseOperations) { <6> + super(metadata, couchbaseOperations); + } + + @Override + public int sharedCustomMethod(ID id) { + //... implement common behavior <7> + } +} + +@EnableCouchbaseRepositories(repositoryBaseClass = MyRepositoryImpl.class) <8> +public class MyConfig extends AbstractCouchbaseConfiguration { /** ... */ } +---- +<1> This annotation prevents picking this custom interface as a repository declaration. +<2> The new base interface extends one from Spring Data Couchbase. +<3> This method will be available in all repositories. +<4> Custom base implementation relies on the existing bases... +<5> ...and also implements new interface (so that common methods are exposed). +<6> Constructors that follow the signature of superconstructor will be picked up by the framework. +<7> Custom functionality to be implemented by the user (eg. return string's length). +<8> Weaving it all in by changing the repository base class. + +=== Couchbase specifics about adding methods to a single repository +Again following the standard procedure for custom repository methods, here is a complete example that you can find in `RepositoryCustomMethodTest` in the integration tests: + +.Adding and overriding methods in a single repository +[source,java] +---- +public interface MyRepositoryCustom { + long customCountItems(); <1> +} + +public interface MyRepository extends CrudRepository, MyRepositoryCustom { } <2> + +public class MyRepositoryImpl implements MyRepositoryCustom { <3> + + @Autowired + RepositoryOperationsMapping templateProvider; <4> + + @Override + public long customCountItems() { + CouchbaseOperations template = templateProvider.resolve(MyRepository.class, Item.class); <5> + + CouchbasePersistentEntity itemPersistenceEntity = (CouchbasePersistentEntity) + template.getConverter() + .getMappingContext() + .getPersistentEntity(MyItem.class); + + CouchbaseEntityInformation itemEntityInformation = + new MappingCouchbaseEntityInformation(itemPersistenceEntity); + + Statement countStatement = N1qlUtils.createCountQueryForEntity( <6> + template.getCouchbaseBucket().name(), + template.getConverter(), + itemEntityInformation); + + ScanConsistency consistency = template.getDefaultConsistency().n1qlConsistency(); <7> + N1qlParams queryParams = N1qlParams.build().consistency(consistency); + N1qlQuery query = N1qlQuery.simple(countStatement, queryParams); + + List countFragments = template.findByN1QLProjection(query, CountFragment.class); <8> + + if (countFragments == null || countFragments.isEmpty()) { + return 0L; + } else { + return countFragments.get(0).count * -1L; <9> + } + } + + public long count() { <10> + return 100; + } +} +---- +<1> This method is to be added with a user-provided implementation for a single repository. +<2> This is the declaration of the customized repository, both a CRUD and exposing the custom interface. +<3> This is the implementation of the custom interface. +<4> The custom implementation doesn't have access to the original base implementation, so use dependency injection to get access to necessary resources. +<5> Here is a couchbase specificity: if you need to use the `CouchbaseTemplate`, be sure to use the one that would be associated with the customized repository or associated entity type. +<6> We use `N1QLUtils` to prepare a complete `N1QL` statement for counting. It relies on the information above that we got from the correct template. +<7> We want to make sure that the default consistency configured in the associated template is used for this query. +<8> Using `CouchbaseTemplate.findByN1qlProjection`, we execute the count query and store the single aggregation result into a `CountFragment`. +<9> Now we return this count result with a twist: it is negated. +<10> *TIP*: You can actually also change implementation of methods from the `CRUDRepository` interface! + +By storing 3 items using a `MyRepository` instance and calling `count()` then `customCountItems()`, we'd obtain + +---- +100 +-3 +---- \ No newline at end of file