DATACOUCH-146 - Reactive repositories support

- Adds experimental support for reactor types in reactive CRUD and Sorting
  repositories using RxJava1Template
- Refactor configuration and query classes to be reusable by reactive repositories

Original pull request: #130.
This commit is contained in:
Subhashni Balakrishnan
2017-01-11 14:55:13 -08:00
parent 56b3842ac1
commit d62f4bbe8c
54 changed files with 4721 additions and 670 deletions

View File

@@ -11,6 +11,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.config.CouchbaseConfigurer;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.WriteResultChecking;
import org.springframework.data.couchbase.core.query.Consistency;
@@ -77,4 +78,10 @@ public class IntegrationTestApplicationConfig extends AbstractCouchbaseConfigura
protected Consistency getDefaultConsistency() {
return Consistency.READ_YOUR_OWN_WRITES;
}
@Override
protected CouchbaseConfigurer couchbaseConfigurer() {
return this;
}
}

View File

@@ -0,0 +1,77 @@
package org.springframework.data.couchbase;
import java.util.Collections;
import java.util.List;
import com.couchbase.client.java.env.CouchbaseEnvironment;
import com.couchbase.client.java.env.DefaultCouchbaseEnvironment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.couchbase.config.AbstractReactiveCouchbaseConfiguration;
import org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate;
import org.springframework.data.couchbase.core.WriteResultChecking;
import org.springframework.data.couchbase.core.query.Consistency;
import org.springframework.data.couchbase.repository.support.IndexManager;
@Configuration
public class ReactiveIntegrationTestApplicationConfig extends AbstractReactiveCouchbaseConfiguration {
@Autowired
private Environment springEnv;
@Bean
public String couchbaseAdminUser() {
return springEnv.getProperty("couchbase.adminUser", "Administrator");
}
@Bean
public String couchbaseAdminPassword() {
return springEnv.getProperty("couchbase.adminUser", "password");
}
@Override
protected List<String> getBootstrapHosts() {
return Collections.singletonList(springEnv.getProperty("couchbase.host", "127.0.0.1"));
}
@Override
protected String getBucketName() {
return springEnv.getProperty("couchbase.bucket", "default");
}
@Override
protected String getBucketPassword() {
return springEnv.getProperty("couchbase.password", "");
}
@Override
protected CouchbaseEnvironment getEnvironment() {
return DefaultCouchbaseEnvironment.builder()
.connectTimeout(10000)
.kvTimeout(10000)
.queryTimeout(10000)
.viewTimeout(10000)
.build();
}
@Override
public RxJavaCouchbaseTemplate reactiveCouchbaseTemplate() throws Exception {
RxJavaCouchbaseTemplate template = super.reactiveCouchbaseTemplate();
template.setWriteResultChecking(WriteResultChecking.LOG);
return template;
}
//this is for dev so it is ok to auto-create indexes
@Override
public IndexManager indexManager() {
return new IndexManager();
}
@Override
protected Consistency getDefaultConsistency() {
return Consistency.READ_YOUR_OWN_WRITES;
}
}

View File

@@ -16,6 +16,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.WriteResultChecking;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
import org.springframework.stereotype.Repository;
@@ -23,7 +25,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* This test case demonstrates that the {@link AbstractCouchbaseDataConfiguration} can take its SDK beans
* This test case demonstrates that the {@link AbstractCouchbaseConfiguration} can take its SDK beans
* from a sibling {@link Configuration}.
*
* @author Simon Baslé
@@ -31,7 +33,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@SuppressWarnings("SpringJavaAutowiringInspection")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class AbstractCouchbaseDataConfigurationTest {
public class AbstractCouchbaseConfigurationTest {
@Autowired
ItemRepository repository;
@@ -72,10 +74,10 @@ public class AbstractCouchbaseDataConfigurationTest {
}
@Configuration
@EnableCouchbaseRepositories(basePackageClasses = AbstractCouchbaseDataConfigurationTest.class, considerNestedRepositories = true)
static class Config extends AbstractCouchbaseDataConfiguration {
@EnableCouchbaseRepositories(basePackageClasses = AbstractCouchbaseConfigurationTest.class, considerNestedRepositories = true)
abstract static class Config extends AbstractCouchbaseConfiguration {
@Autowired
@Autowired
Cluster c;
@Autowired
@@ -87,6 +89,25 @@ public class AbstractCouchbaseDataConfigurationTest {
@Autowired
CouchbaseEnvironment e;
//TODO maybe create the bucket if doesn't exist
@Override
protected CouchbaseEnvironment getEnvironment() {
return DefaultCouchbaseEnvironment.builder()
.connectTimeout(10000)
.kvTimeout(10000)
.queryTimeout(10000)
.viewTimeout(10000)
.build();
}
@Override
public CouchbaseTemplate couchbaseTemplate() throws Exception {
CouchbaseTemplate template = super.couchbaseTemplate();
template.setWriteResultChecking(WriteResultChecking.LOG);
return template;
}
@Override
protected CouchbaseConfigurer couchbaseConfigurer() {
return new TestCouchbaseConfigurer(e, c, ci, b);

View File

@@ -93,6 +93,8 @@ public class CouchbaseTemplateTests {
@Test
public void saveSimpleEntityCorrectly() throws Exception {
String id = "beers:awesome-stout";
removeIfExist(id);
String name = "The Awesome Stout";
boolean active = false;
Beer beer = new Beer(id, name, active, "");

View File

@@ -58,4 +58,16 @@ public class CouchbaseTemplateViewListener extends DependencyInjectionTestExecut
client.bucketManager().upsertDesignDocument(designDoc);
}
@Override
public void afterTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_BUCKET);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_CLUSTER_INFO);
CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
Beer b = new Beer("testbeer-" + i, "MyBeer" + i, true, "");
template.remove(b);
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2017 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.core;
import java.util.Collections;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.view.*;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import rx.Observable;
/**
* @author Subhashni Balakrishnan
*/
public class ReactiveCouchbaseTemplateViewListener extends DependencyInjectionTestExecutionListener {
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_BUCKET);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_CLUSTER_INFO);
populateTestData(client, clusterInfo);
createAndWaitForDesignDocs(client);
}
private void populateTestData(Bucket client, ClusterInfo clusterInfo) {
RxJavaCouchbaseTemplate template = new RxJavaCouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
ReactiveBeer b = new ReactiveBeer("testbeer-" + i, "MyBeer" + i, true, "");
template.save(b).subscribe();
}
}
private void createAndWaitForDesignDocs(Bucket client) {
String mapFunction = "function (doc, meta) { if(doc._class == "
+ "\"org.springframework.data.couchbase.core.ReactiveBeer\") { emit(doc.name, null); } }";
View view = DefaultView.create("by_name", mapFunction);
DesignDocument designDoc = DesignDocument.create("reactive_test_beers", Collections.singletonList(view));
client.bucketManager().upsertDesignDocument(designDoc);
}
@Override
public void afterTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_BUCKET);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_CLUSTER_INFO);
RxJavaCouchbaseTemplate template = new RxJavaCouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
ReactiveBeer b = new ReactiveBeer("testbeer-" + i, "MyBeer" + i, true, "");
template.remove(b).subscribe();
}
}
}

View File

@@ -0,0 +1,590 @@
/*
* Copyright 2017 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.core;
import static com.couchbase.client.java.query.Select.select;
import static com.couchbase.client.java.query.dsl.Expression.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.*;
import java.util.*;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.PersistTo;
import com.couchbase.client.java.ReplicateTo;
import com.couchbase.client.java.document.RawJsonDocument;
import com.couchbase.client.java.error.DocumentAlreadyExistsException;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.query.AsyncN1qlQueryResult;
import com.couchbase.client.java.query.N1qlParams;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.consistency.ScanConsistency;
import com.couchbase.client.java.repository.annotation.Field;
import com.couchbase.client.java.view.Stale;
import com.couchbase.client.java.view.ViewQuery;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.ReactiveIntegrationTestApplicationConfig;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Subhashni Balakrishnan
**/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ReactiveIntegrationTestApplicationConfig.class)
@TestExecutionListeners(ReactiveCouchbaseTemplateViewListener.class)
public class RxJavaCouchbaseTemplateTests {
@Rule
public TestName testName = new TestName();
@Autowired
private Bucket client;
@Autowired
private RxJavaCouchbaseOperations template;
private static final ObjectMapper MAPPER = new ObjectMapper();
private void removeIfExist(String key) {
template.remove(key).subscribe();
}
private void removeCollectionIfExist(Collection<ReactiveBeer> beers) {
template.remove(beers, PersistTo.MASTER, ReplicateTo.NONE)
.subscribe();
}
@Test
public void saveSimpleEntityCorrectly() throws Exception {
String id = "reactivebeers:awesome-stout";
removeIfExist(id);
String name = "The Awesome Stout";
boolean active = false;
ReactiveBeer beer = new ReactiveBeer(id, name, active, "");
template.save(beer)
.subscribe();
RawJsonDocument resultDoc = client.get(id, RawJsonDocument.class);
assertNotNull(resultDoc);
String result = resultDoc.content();
assertNotNull(result);
Map<String, Object> resultConv = MAPPER.readValue(result, new TypeReference<Map<String, Object>>() {
});
assertNotNull(resultConv.get(MappingCouchbaseConverter.TYPEKEY_DEFAULT));
assertNull(resultConv.get("javaClass"));
assertEquals("org.springframework.data.couchbase.core.ReactiveBeer", resultConv.get(MappingCouchbaseConverter.TYPEKEY_DEFAULT));
assertEquals(false, resultConv.get("is_active"));
assertEquals("The Awesome Stout", resultConv.get("name"));
removeIfExist(id);
}
@Test
public void saveCollectionCorrectly() throws Exception {
Collection<ReactiveBeer> beers = new ArrayList<>();
String name = "The Awesome Stout";
for (int i=0; i < 10000; i++) {
beers.add(new ReactiveBeer("beerCollItem" + i, name + i, false, ""));
}
removeCollectionIfExist(beers);
template.save(beers).subscribe();
}
@Test
public void removeDocument() {
String id = "beers:to-delete-stout";
ReactiveBeer beer = new ReactiveBeer(id, "", false, "");
removeIfExist(id);
template.save(beer).subscribe();
Object result = client.get(id);
assertNotNull(result);
template.remove(beer).subscribe();
result = client.get(id);
assertNull(result);
}
@Test
public void storeListsAndMaps() {
String id = "persons:lots-of-names";
List<String> names = new ArrayList<String>();
names.add("Michael");
names.add("Thomas");
names.add(null);
List<Integer> votes = new LinkedList<Integer>();
Map<String, Boolean> info1 = new HashMap<String, Boolean>();
info1.put("foo", true);
info1.put("bar", false);
info1.put("nullValue", null);
Map<String, Integer> info2 = new HashMap<String, Integer>();
ComplexPerson complex = new ComplexPerson(id, names, votes, info1, info2);
template.save(complex).toBlocking();
assertNotNull(client.get(id));
ComplexPerson response = template.findById(id, ComplexPerson.class).toBlocking().single();
assertEquals(names, response.getFirstnames());
assertEquals(votes, response.getVotes());
assertEquals(id, response.getId());
assertEquals(info1, response.getInfo1());
assertEquals(info2, response.getInfo2());
}
@Test
public void validFindById() {
String id = "reactive beers:findme-stout";
String name = "Findme Stout";
boolean active = true;
ReactiveBeer beer = new ReactiveBeer(id, name, active, "");
template.save(beer).subscribe();
ReactiveBeer found = template.findById(id, ReactiveBeer.class).toBlocking().single();
assertNotNull(found);
assertEquals(id, found.getId());
assertEquals(name, found.getName());
assertEquals(active, found.getActive());
}
@Test
public void shouldLoadAndMapViewDocs() {
ViewQuery query = ViewQuery.from("reactive_test_beers", "by_name");
query.stale(Stale.FALSE);
final List<ReactiveBeer> beers = template.findByView(query, ReactiveBeer.class).toList().toBlocking().single();
assertTrue(beers.size() > 0);
for (ReactiveBeer beer : beers) {
assertNotNull(beer.getId());
assertNotNull(beer.getName());
assertNotNull(beer.getActive());
}
}
@Test
public void shouldQueryRaw() {
N1qlQuery query = N1qlQuery.simple(select("name").from(i(client.name()))
.where(x("name").isNotMissing()));
AsyncN1qlQueryResult queryResult = template.queryN1QL(query).toBlocking().single();
assertNotNull(queryResult);
assertTrue(queryResult.errors().toString(), queryResult.finalSuccess().toBlocking().single());
assertFalse(queryResult.rows().toList().toBlocking().single().isEmpty());
}
@Test
public void shouldQueryWithMapping() {
FullFragment ff1 = new FullFragment("fullFragment1", 1, "fullFragment", "test1");
FullFragment ff2 = new FullFragment("fullFragment2", 2, "fullFragment", "test2");
template.save(Arrays.asList(ff1, ff2));
N1qlQuery query = N1qlQuery.simple(select(i("value")) //"value" is a n1ql keyword apparently
.from(i(client.name()))
.where(x("type").eq(s("fullFragment"))
.and(x("criteria").gt(1))),
N1qlParams.build().consistency(ScanConsistency.REQUEST_PLUS));
List<Fragment> fragments = template.findByN1QLProjection(query, Fragment.class).toList().toBlocking().single();
assertNotNull(fragments);
assertFalse(fragments.isEmpty());
assertEquals(1, fragments.size());
assertEquals("test2", fragments.get(0).value);
}
@Test
public void shouldDeserialiseLongsAndInts() {
final long longValue = new Date().getTime();
final int intValue = new Random().nextInt();
template.save(new SimpleWithLongAndInt("simpleWithLong:simple", longValue, intValue)).toBlocking().single();
SimpleWithLongAndInt document = template.findById("simpleWithLong:simple", SimpleWithLongAndInt.class).toBlocking().single();
assertNotNull(document);
assertEquals(longValue, document.getLongValue());
assertEquals(intValue, document.getIntValue());
template.save(new SimpleWithLongAndInt("simpleWithLong:simple:other", intValue, intValue)).toBlocking().single();
document = template.findById("simpleWithLong:simple:other", SimpleWithLongAndInt.class).toBlocking().single();
assertNotNull(document);
assertEquals(intValue, document.getLongValue());
assertEquals(intValue, document.getIntValue());
}
@Test
public void shouldDeserialiseEnums() {
SimpleWithEnum simpleWithEnum = new SimpleWithEnum("simpleWithEnum:enum", SimpleWithEnum.Type.BIG);
template.save(simpleWithEnum).toBlocking().single();
simpleWithEnum = template.findById("simpleWithEnum:enum", SimpleWithEnum.class).toBlocking().single();
assertNotNull(simpleWithEnum);
assertEquals(simpleWithEnum.getType(), SimpleWithEnum.Type.BIG);
}
@Test
public void shouldDeserialiseClass() {
SimpleWithClass simpleWithClass = new SimpleWithClass("simpleWithClass:class", Integer.class);
simpleWithClass.setValue("The dish ran away with the spoon.");
template.save(simpleWithClass).toBlocking().single();
simpleWithClass = template.findById("simpleWithClass:class", SimpleWithClass.class).toBlocking().single();
assertNotNull(simpleWithClass);
assertThat(simpleWithClass.getValue(), equalTo("The dish ran away with the spoon."));
}
@Test
public void expiryWhenTouchOnReadDocument() throws InterruptedException {
String id = "simple-doc-with-update-expiry-for-read";
DocumentWithTouchOnRead doc = new DocumentWithTouchOnRead(id);
template.save(doc).subscribe();
Thread.sleep(1500);
assertNotNull(template.findById(id, DocumentWithTouchOnRead.class).toBlocking().single());
Thread.sleep(1500);
assertNotNull(template.findById(id, DocumentWithTouchOnRead.class).toBlocking().single());
Thread.sleep(3000);
assertNull(template.findById(id, DocumentWithTouchOnRead.class).toBlocking().single());
}
@Test
public void shouldRetainOrderWhenQueryingViewOrdered() {
ViewQuery q = ViewQuery.from("reactive_test_beers", "by_name");
q.descending().includeDocsOrdered(true);
String prev = null;
List<ReactiveBeer> beers = template.findByView(q, ReactiveBeer.class).toList().toBlocking().single();
assertTrue(q.isIncludeDocs());
assertTrue(q.isOrderRetained());
assertEquals(RawJsonDocument.class, q.includeDocsTarget());
for (ReactiveBeer beer : beers) {
if (prev != null) {
assertThat(beer.getName() + " not alphabetically < to " + prev, beer.getName().compareTo(prev) < 0);
}
prev = beer.getName();
}
}
/**
* A sample document with just an id and property.
*/
@Document
static class SimplePerson {
@Id
private final String id;
@Field
private final String name;
public SimplePerson(String id, String name) {
this.id = id;
this.name = name;
}
}
/**
* A sample document that expires in 2 seconds.
*/
@Document(expiry = 2)
static class DocumentWithExpiry {
@Id
private final String id;
public DocumentWithExpiry(String id) {
this.id = id;
}
}
/**
* A sample document that expires in 2 seconds and touchOnRead set.
*/
@Document(expiry = 2, touchOnRead = true)
static class DocumentWithTouchOnRead {
@Id
private final String id;
public DocumentWithTouchOnRead(String id) {
this.id = id;
}
}
@Document
static class ComplexPerson {
@Id
private final String id;
@Field
private final List<String> firstnames;
@Field
private final List<Integer> votes;
@Field
private final Map<String, Boolean> info1;
@Field
private final Map<String, Integer> info2;
public ComplexPerson(String id, List<String> firstnames,
List<Integer> votes, Map<String, Boolean> info1,
Map<String, Integer> info2) {
this.id = id;
this.firstnames = firstnames;
this.votes = votes;
this.info1 = info1;
this.info2 = info2;
}
List<String> getFirstnames() {
return firstnames;
}
List<Integer> getVotes() {
return votes;
}
Map<String, Boolean> getInfo1() {
return info1;
}
Map<String, Integer> getInfo2() {
return info2;
}
String getId() {
return id;
}
}
@Document
static class SimpleWithLongAndInt {
@Id
private String id;
private long longValue;
private int intValue;
SimpleWithLongAndInt(final String id, final long longValue, int intValue) {
this.id = id;
this.longValue = longValue;
this.intValue = intValue;
}
String getId() {
return id;
}
long getLongValue() {
return longValue;
}
void setLongValue(final long value) {
this.longValue = value;
}
public int getIntValue() {
return intValue;
}
public void setIntValue(int intValue) {
this.intValue = intValue;
}
}
static class SimpleWithEnum {
@Id
private String id;
private enum Type {
BIG
}
Type type;
SimpleWithEnum(final String id, final Type type) {
this.id = id;
this.type = type;
}
String getId() {
return id;
}
void setId(final String id) {
this.id = id;
}
Type getType() {
return type;
}
void setType(final Type type) {
this.type = type;
}
}
static class SimpleWithClass {
@Id
private String id;
private Class<Integer> integerClass;
private String value;
SimpleWithClass(final String id, final Class<Integer> integerClass) {
this.id = id;
this.integerClass = integerClass;
}
String getId() {
return id;
}
void setId(final String id) {
this.id = id;
}
Class<Integer> getIntegerClass() {
return integerClass;
}
void setIntegerClass(final Class<Integer> integerClass) {
this.integerClass = integerClass;
}
String getValue() {
return value;
}
void setValue(final String value) {
this.value = value;
}
}
static class VersionedClass {
@Id
private String id;
@Version
private long version;
private String field;
VersionedClass(String id, String field) {
this.id = id;
this.field = field;
}
public String getId() {
return id;
}
public long getVersion() {
return version;
}
public String getField() {
return field;
}
public void setField(String field) {
this.field = field;
}
@Override
public String toString() {
return "VersionedClass{" +
"id='" + id + '\'' +
", version=" + version +
", field='" + field + '\'' +
'}';
}
}
@Document
static class FullFragment {
@Id
private String id;
private long criteria;
private String type;
private String value;
public FullFragment(String id, long criteria, String type, String value) {
this.id = id;
this.criteria = criteria;
this.type = type;
this.value = value;
}
public String getId() {
return id;
}
public long getCriteria() {
return criteria;
}
public String getType() {
return type;
}
public String getValue() {
return value;
}
public void setCriteria(long criteria) {
this.criteria = criteria;
}
public void setType(String type) {
this.type = type;
}
public void setValue(String value) {
this.value = value;
}
}
static class Fragment {
public String value;
}
}

View File

@@ -134,9 +134,9 @@ public class CouchbaseRepositoryViewTests {
String highKey = "uname-11";
List<String> keys = Arrays.asList(lowKey, middleKey, highKey);
User u1 = repository.findByUsernameIs(lowKey);
User u2 = repository.findByUsernameIs(middleKey);
User u3 = repository.findByUsernameIs(highKey);
User u1 = repository.findByUsernameIs(lowKey).get(0);
User u2 = repository.findByUsernameIs(middleKey).get(0);
User u3 = repository.findByUsernameIs(highKey).get(0);
List<User> in = repository.findAllByUsernameIn(keys);

View File

@@ -46,7 +46,7 @@ public interface CustomUserRepository extends CouchbaseRepository<User, String>
long countByUsernameGreaterThanEqualAndUsernameLessThan(String lowBound, String highBound);
@View(viewName = "customFindByNameView")
User findByUsernameIs(String lowKey);
List<User> findByUsernameIs(String lowKey);
@View(viewName = "customFindByNameView")
List<User> findAllByUsernameIn(List<String> keys);

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2017 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;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.couchbase.ReactiveIntegrationTestApplicationConfig;
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.support.IndexManager;
import org.springframework.data.couchbase.repository.support.ReactiveCouchbaseRepositoryFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* This tests ReactiveSortingRepository features in the Couchbase connector.
*
* @author Subhashni Balakrishnan
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ReactiveIntegrationTestApplicationConfig.class)
@TestExecutionListeners(PartyPopulatorListener.class)
public class ReactiveN1qlCouchbaseRepositoryTests {
@Autowired
private ReactiveRepositoryOperationsMapping operationsMapping;
@Autowired
private IndexManager indexManager;
private ReactivePartySortingRepository repository;
private ReactivePartyRepository partyRepository;
private ItemRepository itemRepository;
private final String KEY_PARTY = "ReactiveParty1";
private final String KEY_ITEM = "ReactiveItem1";
@Before
public void setup() throws Exception {
ReactiveRepositoryFactorySupport factory = new ReactiveCouchbaseRepositoryFactory(operationsMapping, indexManager);
repository = factory.getRepository(ReactivePartySortingRepository.class);
partyRepository = factory.getRepository(ReactivePartyRepository.class);
itemRepository = factory.getRepository(ItemRepository.class);
}
@After
public void cleanUp() {
try { itemRepository.delete(KEY_ITEM); } catch (DataRetrievalFailureException e) {}
try { partyRepository.delete(KEY_PARTY); } catch (DataRetrievalFailureException e) {}
}
@Test
public void shouldFindAllWithSort() {
Iterable<Party> allByAttendanceDesc = repository.findAll(new Sort(Sort.Direction.DESC, "attendees")).collectList().block();
long previousAttendance = Long.MAX_VALUE;
for (Party party : allByAttendanceDesc) {
assertTrue(party.getAttendees() <= previousAttendance);
previousAttendance = party.getAttendees();
}
assertFalse("Expected to find several parties", previousAttendance == Long.MAX_VALUE);
}
@Test
public void shouldSortOnRenamedFieldIfJsonNameIsProvidedInSort() {
Iterable<Party> parties = repository.findAll(new Sort(Sort.Direction.DESC, "desc")).collectList().block();
String previousDesc = null;
for (Party party : parties) {
if (previousDesc != null) {
assertTrue(party.getDescription().compareTo(previousDesc) <= 0);
}
previousDesc = party.getDescription();
}
assertNotNull("Expected to find several parties", previousDesc);
}
@Test
public void shouldSortWithoutCaseSensitivity() {
Iterable<Party> parties = repository.findAll(new Sort(new Sort.Order(Sort.Direction.DESC, "desc").ignoreCase())).collectList().block();
String previousDesc = null;
for(Party party : parties) {
if (previousDesc != null) {
assertTrue(party.getDescription().compareToIgnoreCase(previousDesc) <= 0);
}
previousDesc = party.getDescription();
}
assertNotNull("Expected to find several parties", previousDesc);
}
}

View File

@@ -0,0 +1,60 @@
package org.springframework.data.couchbase.repository;
import java.util.Date;
import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.View;
import org.springframework.data.couchbase.core.query.ViewIndexed;
import org.springframework.data.repository.query.Param;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author Subhashni Balakrishnan
*/
@ViewIndexed(designDoc = "party", viewName = "all")
@N1qlSecondaryIndexed(indexName = "party")
public interface ReactivePartyRepository extends ReactiveCouchbaseRepository<Party, String> {
Flux<Party> findByAttendeesGreaterThanEqual(int minAttendees);
Flux<Party> findByEventDateIs(Date targetDate);
@View(designDocument = "party", viewName = "byDate")
Flux<Party> findFirst3ByEventDateGreaterThanEqual(Date targetDate);
Flux<Object> findAllByDescriptionNotNull();
Mono<Long> countAllByDescriptionNotNull();
@Query("SELECT MAX(attendees) FROM #{#n1ql.bucket} WHERE #{#n1ql.filter}")
Mono<Long> findMaxAttendees();
@Query("SELECT `desc` FROM #{#n1ql.bucket} WHERE #{#n1ql.filter}")
Mono<String> findSomeString();
@Query("SELECT count(*) + 5 FROM #{#n1ql.bucket} WHERE #{#n1ql.filter}")
Mono<Long> countCustomPlusFive();
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter}")
Mono<Long> countCustom();
@Query("SELECT 1 = 1")
Mono<Boolean> justABoolean();
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND `desc` LIKE '%' || $included || '%' AND attendees >= $min" +
" AND `desc` NOT LIKE '%' || $excluded || '%'")
Flux<Party> findAllWithNamedParams(@Param("excluded") String ex, @Param("included") String inc, @Param("min") long minimumAttendees);
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND `desc` LIKE '%' || $2 || '%' AND attendees >= $3" +
" AND `desc` NOT LIKE '%' || $1 || '%'")
Flux<Party> findAllWithPositionalParams(String ex, String inc, long minimumAttendees);
@Query("#{#n1ql.selectEntity} WHERE #{#n1ql.filter} AND `desc` LIKE '%' || $2 || '%' AND attendees >= $3" +
" AND `desc` NOT LIKE '%' || $1 || '%' AND `desc` != \"this is \\\"$excluded\\\"\"")
Flux<Party> findAllWithPositionalParamsAndQuotedNamedParams(@Param("excluded") String ex, @Param("included") String inc, @Param("min") long min);
Flux<Party> findByDescriptionOrName(String description, String name);
}

View File

@@ -0,0 +1,9 @@
package org.springframework.data.couchbase.repository;
/**
* @author Subhashni Balakrishnan
*/
public interface ReactivePartySortingRepository extends ReactiveCouchbaseSortingRepository<Party, String> {
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2017 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;
import org.springframework.data.annotation.Id;
/**
* @author Subhashni Balakrishnan
*/
public class ReactiveUser {
@Id
private final String key;
private final String username;
private final int age;
public ReactiveUser(String key, String username, int age) {
this.key = key;
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public String getKey() {
return key;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return this.key;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ReactiveUser user = (ReactiveUser) o;
return key.equals(user.key);
}
@Override
public int hashCode() {
return key.hashCode();
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2017 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;
import java.util.List;
import com.couchbase.client.java.view.ViewQuery;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.core.query.View;
import reactor.core.publisher.Flux;
/**
* @author Subhashni Balakrishnan
*/
public interface ReactiveUserRepository extends ReactiveCouchbaseRepository<ReactiveUser, String> {
@View(designDocument = "user", viewName = "all")
Flux<ReactiveUser> customViewQuery(ViewQuery query);
@Query("#{#n1ql.selectEntity} WHERE username = $1")
Flux<ReactiveUser> findByUsername(String username);
@Query("SELECT * FROM #{#n1ql.bucket} WHERE username = $1")
Flux<ReactiveUser> findByUsernameBadSelect(String username);
@Query("#{#n1ql.selectEntity} WHERE username LIKE '%-#{3 + 1}'")
Flux<ReactiveUser> findByUsernameWithSpelAndPlaceholder();
@Query
Flux<ReactiveUser> findByUsernameRegexAndUsernameIn(String regex, List<String> sample);
Flux<ReactiveUser> findByUsernameContains(String contains);
Flux<ReactiveUser> findByUsernameNear(String place);//this is to check that there's a N1QL derivation AND it fails
}

View File

@@ -0,0 +1,50 @@
package org.springframework.data.couchbase.repository;
import java.util.Collections;
import java.util.List;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.PersistTo;
import com.couchbase.client.java.ReplicateTo;
import com.couchbase.client.java.cluster.ClusterInfo;
import com.couchbase.client.java.view.DefaultView;
import com.couchbase.client.java.view.DesignDocument;
import com.couchbase.client.java.view.View;
import org.springframework.data.couchbase.config.BeanNames;
import org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
/**
* @author Subhashni Balakrishnan
*/
public class SimpleReactiveCouchbaseRepositoryListener extends DependencyInjectionTestExecutionListener {
@Override
public void beforeTestClass(final TestContext testContext) throws Exception {
Bucket client = (Bucket) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_BUCKET);
ClusterInfo clusterInfo = (ClusterInfo) testContext.getApplicationContext().getBean(BeanNames.COUCHBASE_CLUSTER_INFO);
populateTestData(client, clusterInfo);
createAndWaitForDesignDocs(client);
}
private void populateTestData(Bucket client, ClusterInfo clusterInfo) {
RxJavaCouchbaseTemplate template = new RxJavaCouchbaseTemplate(clusterInfo, client);
for (int i = 0; i < 100; i++) {
ReactiveUser u = new ReactiveUser("reactivetestuser-" + i, "reactiveuname-" + i, i);
template.save(u, PersistTo.MASTER, ReplicateTo.NONE).subscribe();
}
}
private void createAndWaitForDesignDocs(Bucket client) {
String mapFunction = "function (doc, meta) { if(doc._class == \"org.springframework.data.couchbase.repository." +
"ReactiveUser\") { emit(null, null); } }";
View view = DefaultView.create("all", mapFunction, "_count");
List<View> views = Collections.singletonList(view);
DesignDocument designDoc = DesignDocument.create("reactiveUser", views);
client.bucketManager().upsertDesignDocument(designDoc);
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2017 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;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.error.DocumentDoesNotExistException;
import com.couchbase.client.java.view.Stale;
import com.couchbase.client.java.view.ViewQuery;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.couchbase.IntegrationTestApplicationConfig;
import org.springframework.data.couchbase.ReactiveIntegrationTestApplicationConfig;
import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException;
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
import org.springframework.data.couchbase.repository.support.ReactiveCouchbaseRepositoryFactory;
import org.springframework.data.couchbase.repository.support.IndexManager;
import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
/**
* @author Subhashni Balakrishnan
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ReactiveIntegrationTestApplicationConfig.class)
@TestExecutionListeners(SimpleReactiveCouchbaseRepositoryListener.class)
public class SimpleReactiveCouchbaseRepositoryTests {
@Rule
public TestName testName = new TestName();
@Autowired
private Bucket client;
@Autowired
private ReactiveRepositoryOperationsMapping operationsMapping;
@Autowired
private IndexManager indexManager;
private ReactiveUserRepository repository;
@Before
public void setup() throws Exception {
ReactiveRepositoryFactorySupport factory = new ReactiveCouchbaseRepositoryFactory(operationsMapping, indexManager);
repository = factory.getRepository(ReactiveUserRepository.class);
}
private void remove(String key) {
try {
client.remove(key);
} catch (DocumentDoesNotExistException e) {
}
}
@Test
public void simpleCrud() {
String key = "my_unique_user_key";
ReactiveUser instance = new ReactiveUser(key, "foobar", 22);
repository.save(instance).block();
ReactiveUser found = repository.findOne(key).block();
assertEquals(instance.getKey(), found.getKey());
assertEquals(instance.getUsername(), found.getUsername());
assertTrue(repository.exists(key).block());
repository.delete(found).block();
assertNull(repository.findOne(key).block());
assertFalse(repository.exists(key).block());
}
@Test
/**
* This test uses/assumes a default viewName called "all" that is configured on Couchbase.
*/
public void shouldFindAll() {
// do a non-stale query to populate data for testing.
client.query(ViewQuery.from("reactiveUser", "all").stale(Stale.FALSE));
List<ReactiveUser> allUsers = repository.findAll().collectList().block();
int size = 0;
for (ReactiveUser u : allUsers) {
size++;
assertNotNull(u.getKey());
assertNotNull(u.getUsername());
}
assertEquals(100, size);
}
@Test
public void shouldCount() {
// do a non-stale query to populate data for testing.
client.query(ViewQuery.from("reactiveUser", "all").stale(Stale.FALSE));
assertEquals("100", repository.count().block().toString());
}
@Test
public void shouldFindByUsernameUsingN1ql() {
ReactiveUser user = repository.findByUsername("reactiveuname-1").single().block();
assertNotNull(user);
assertEquals("reactivetestuser-1", user.getKey());
assertEquals("reactiveuname-1", user.getUsername());
}
@Test
public void shouldFailFindByUsernameWithNoIdOrCas() {
try {
ReactiveUser user = repository.findByUsernameBadSelect("reactiveuname-1").single().block();
fail("shouldFailFindByUsernameWithNoIdOrCas");
} catch (CouchbaseQueryExecutionException e) {
assertTrue("_ID expected in exception " + e, e.getMessage().contains("_ID"));
assertTrue("_CAS expected in exception " + e, e.getMessage().contains("_CAS"));
} catch (Exception e) {
fail("CouchbaseQueryExecutionException expected");
}
}
@Test
public void shouldFindFromUsernameInlineWithSpelParsing() {
ReactiveUser user = repository.findByUsernameWithSpelAndPlaceholder().take(1).blockLast();
assertNotNull(user);
assert(user.getUsername().startsWith("reactive"));
assert(user.getUsername().startsWith("reactive"));
}
@Test
public void shouldFindFromDeriveQueryWithRegexpAndIn() {
ReactiveUser user = repository.findByUsernameRegexAndUsernameIn("reactiveuname-[123]", Arrays.asList("reactiveuname-2", "reactiveuname-4")).take(1).blockLast();
assertNotNull(user);
assertEquals("reactivetestuser-2", user.getKey());
assertEquals("reactiveuname-2", user.getUsername());
}
@Test
public void shouldFindContainsWithoutAnnotation() {
List<ReactiveUser> users = repository.findByUsernameContains("reactive").collectList().block();
assertNotNull(users);
assertFalse(users.isEmpty());
for (ReactiveUser user : users) {
assertTrue(user.getUsername().startsWith("reactive"));
}
}
@Test
public void shouldDefaultToN1qlQueryDerivation() {
try {
ReactiveUser u = repository.findByUsernameNear("london").single().block();
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
if (!e.getMessage().contains("N1QL")) {
fail(e.getMessage());
}
}
}
}

View File

@@ -34,13 +34,13 @@ public interface UserRepository extends CouchbaseRepository<User, String> {
@View(designDocument = "user", viewName = "all")
Iterable<User> customViewQuery(ViewQuery query);
@Query("#{#n1ql.selectEntity} WHERE username = $1")
@Query("#{#n1ql.selectEntity} WHERE username = $1 and #{#n1ql.filter}")
User findByUsername(String username);
@Query("SELECT * FROM #{#n1ql.bucket} WHERE username = $1")
@Query("SELECT * FROM #{#n1ql.bucket} WHERE username = $1 and #{#n1ql.filter} ")
User findByUsernameBadSelect(String username);
@Query("#{#n1ql.selectEntity} WHERE username LIKE '%-#{3 + 1}'")
@Query("#{#n1ql.selectEntity} WHERE username LIKE '%-#{3 + 1}' and #{#n1ql.filter}'")
User findByUsernameWithSpelAndPlaceholder();
@Query