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.
This commit is contained in:
Simon Baslé
2016-01-15 18:16:00 +01:00
parent c589a13699
commit 15d0d27d71
9 changed files with 334 additions and 8 deletions

View File

@@ -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<Item, String> {
//
}
public interface UserRepository extends MyRepository<User, String> {
//
}
@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 {

View File

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

View File

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

View File

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

View File

@@ -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<MyItem, String>, MyRepositoryCustom {
}

View File

@@ -0,0 +1,7 @@
package org.springframework.data.couchbase.repository.extending.method;
public interface MyRepositoryCustom {
long customCountItems();
}

View File

@@ -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<Object> itemPersistenceEntity = (CouchbasePersistentEntity<Object>)
template.getConverter()
.getMappingContext()
.getPersistentEntity(MyItem.class);
CouchbaseEntityInformation<? extends Object, String> itemEntityInformation =
new MappingCouchbaseEntityInformation<Object, String>(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<CountFragment> 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;
}
}

View File

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

View File

@@ -317,7 +317,7 @@ List<UserInfo> 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 <<repositories.single-repository-behaviour>>.
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 <<couchbase.repository.changing-repository-behaviour>>.
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 <<repositories.single-repository-behaviour>>).
(see <<couchbase.repository.changing-repository-behaviour>>).
- 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 <<repositories.custom-implementations>> 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<T, ID extends Serializable> extends CouchbaseRepository<T, ID> { <2>
int sharedCustomMethod(ID id); <3>
}
public class MyRepositoryImpl<T, ID extends Serializable>
extends N1qlCouchbaseRepository<T, ID> <4>
implements MyRepository<T, ID> { <5>
public MyRepositoryImpl(CouchbaseEntityInformation<T, String> 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<MyItem, String>, 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<Object> itemPersistenceEntity = (CouchbasePersistentEntity<Object>)
template.getConverter()
.getMappingContext()
.getPersistentEntity(MyItem.class);
CouchbaseEntityInformation<? extends Object, String> itemEntityInformation =
new MappingCouchbaseEntityInformation<Object, String>(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<CountFragment> 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
----