From 15d0d27d71158a30960f172620bb7b7ea57ddcfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 15 Jan 2016 18:16:00 +0100 Subject: [PATCH] DATACOUCH-189 - Test and document how to change repo implementation Added an integration test for custom method customization. Moved the integration test for base class customization. Put the samples in the documentation in a new "Changing Repository Behaviour" section. --- .../base/RepositoryBaseTest.java | 22 +++- .../base/impl/MyRepository.java | 2 +- .../base/impl/MyRepositoryImpl.java | 3 +- .../repository/extending/method/MyItem.java | 15 +++ .../extending/method/MyRepository.java | 10 ++ .../extending/method/MyRepositoryCustom.java | 7 + .../extending/method/MyRepositoryImpl.java | 58 +++++++++ .../method/RepositoryCustomMethodTest.java | 102 +++++++++++++++ src/main/asciidoc/repository.adoc | 123 +++++++++++++++++- 9 files changed, 334 insertions(+), 8 deletions(-) rename src/integration/java/org/springframework/data/couchbase/repository/{ => extending}/base/RepositoryBaseTest.java (79%) rename src/integration/java/org/springframework/data/couchbase/repository/{ => extending}/base/impl/MyRepository.java (80%) rename src/integration/java/org/springframework/data/couchbase/repository/{ => extending}/base/impl/MyRepositoryImpl.java (82%) create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyItem.java create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepository.java create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepositoryCustom.java create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/extending/method/MyRepositoryImpl.java create mode 100644 src/integration/java/org/springframework/data/couchbase/repository/extending/method/RepositoryCustomMethodTest.java 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