Add collections support to N1qlJoin. (#1333)

The scope for the entity can come from an option, a method annotation,
an annotation on the repository or an annotation on the entity class.
All these possibilities are handle by PseudoArgs in the
OperationSupport implementation. That scope/collection are passed
into decodeEntity(). The scope/collection of the child can only come
from an annotation on the entity class. The scope/collection of the
parent and child are uses as follows:

1) Both the parent and the chold have non-default collections
It's possible that the scope for the parent was set with an annotation on
a repository method, the entity class or the repository class or a query
option. Since there is no means to set the scope of the child class by the
method, repository class or query option (only the annotation) we assume
that the (possibly) dynamic scope of the entity would be a better choice
as it is logical to put collections to be joined in the same scope.

2) The parent has a collection (and therefore a scope as well), but the
child  does not have a collection. Use the lhScope and lhCollection for
the entity.  The child is just the bucket.

3) The parent does not have a collection (or scope), but child does have a
collection. Using the same (default) scope for the child would mean
specifying a non-default collection in a default scope - which is not
allowed. So use the scope and collection from the child class.

4) Neither have collections, just use the bucket.

Closes #1325.
This commit is contained in:
Michael Reiche
2022-02-15 11:37:54 -08:00
committed by Michael Reiche
parent b04b95a027
commit 2e49741680
20 changed files with 528 additions and 47 deletions

View File

@@ -85,7 +85,7 @@ class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSuppo
}
@Override
public <T> T decodeEntity(String id, String source, long cas, Class<T> entityClass) {
public <T> T decodeEntity(String id, String source, long cas, Class<T> entityClass, String scope, String collection) {
final CouchbaseDocument converted = new CouchbaseDocument(id);
converted.setId(id);
CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass);
@@ -110,9 +110,10 @@ class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSuppo
if (persistentEntity.getVersionProperty() != null) {
accessor.setProperty(persistentEntity.getVersionProperty(), cas);
}
N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id);
N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id, scope, collection);
return accessor.getBean();
}
CouchbasePersistentEntity couldBePersistentEntity(Class<?> entityClass) {
if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) {
return null;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors.
* Copyright 2021-2022 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.
@@ -40,8 +40,9 @@ public class NonReactiveSupportWrapper implements ReactiveTemplateSupport {
}
@Override
public <T> Mono<T> decodeEntity(String id, String source, long cas, Class<T> entityClass) {
return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass));
public <T> Mono<T> decodeEntity(String id, String source, long cas, Class<T> entityClass, String scope,
String collection) {
return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection));
}
@Override

View File

@@ -85,7 +85,8 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React
}
@Override
public <T> Mono<T> decodeEntity(String id, String source, long cas, Class<T> entityClass) {
public <T> Mono<T> decodeEntity(String id, String source, long cas, Class<T> entityClass, String scope,
String collection) {
return Mono.fromSupplier(() -> {
final CouchbaseDocument converted = new CouchbaseDocument(id);
converted.setId(id);
@@ -112,7 +113,7 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React
if (persistentEntity.getVersionProperty() != null) {
accessor.setProperty(persistentEntity.getVersionProperty(), cas);
}
N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id);
N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id, scope, collection);
return accessor.getBean();
});
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 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.
@@ -133,7 +133,7 @@ public class ReactiveFindByAnalyticsOperationSupport implements ReactiveFindByAn
cas = row.getLong(TemplateUtils.SELECT_CAS);
row.removeKey(TemplateUtils.SELECT_ID);
row.removeKey(TemplateUtils.SELECT_CAS);
return support.decodeEntity(id, row.toString(), cas, returnType);
return support.decodeEntity(id, row.toString(), cas, returnType, null, null);
});
});
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 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.
@@ -90,8 +90,8 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati
} else {
return reactive.get(docId, (GetOptions) pArgs.getOptions());
}
}).flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType))
.onErrorResume(throwable -> {
}).flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType,
pArgs.getScope(), pArgs.getCollection())).onErrorResume(throwable -> {
if (throwable instanceof RuntimeException) {
if (throwable instanceof DocumentNotFoundException) {
return Mono.empty();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 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.
@@ -202,7 +202,7 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
row.removeKey(TemplateUtils.SELECT_ID);
row.removeKey(TemplateUtils.SELECT_CAS);
}
return support.decodeEntity(id, row.toString(), cas, returnType);
return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection());
}));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 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.
@@ -77,7 +77,8 @@ public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFin
return Mono.just(id)
.flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope())
.getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions()))
.flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType))
.flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType,
pArgs.getScope(), pArgs.getCollection()))
.onErrorMap(throwable -> {
if (throwable instanceof RuntimeException) {
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors
* Copyright 2021-2022 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.
@@ -20,11 +20,15 @@ import reactor.core.publisher.Mono;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
/**
*
* @author Michael Reiche
*/
public interface ReactiveTemplateSupport {
Mono<CouchbaseDocument> encodeEntity(Object entityToEncode);
<T> Mono<T> decodeEntity(String id, String source, long cas, Class<T> entityClass);
<T> Mono<T> decodeEntity(String id, String source, long cas, Class<T> entityClass, String scope, String collection);
<T> Mono<T> applyUpdatedCas(T entity, CouchbaseDocument converted, long cas);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors
* Copyright 2021-2022 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.
@@ -18,11 +18,15 @@ package org.springframework.data.couchbase.core;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
/**
*
* @author Michael Reiche
*/
public interface TemplateSupport {
CouchbaseDocument encodeEntity(Object entityToEncode);
<T> T decodeEntity(String id, String source, long cas, Class<T> entityClass);
<T> T decodeEntity(String id, String source, long cas, Class<T> entityClass, String scope, String collection);
<T> T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018-2021 the original author or authors
* Copyright 2018-2022 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.
@@ -16,9 +16,12 @@
package org.springframework.data.couchbase.core.convert.join;
import static org.springframework.data.couchbase.core.query.N1QLExpression.i;
import static org.springframework.data.couchbase.core.query.N1QLExpression.x;
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_CAS;
import static org.springframework.data.couchbase.core.support.TemplateUtils.SELECT_ID;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
@@ -33,13 +36,17 @@ import org.springframework.data.couchbase.core.query.HashSide;
import org.springframework.data.couchbase.core.query.N1QLExpression;
import org.springframework.data.couchbase.core.query.N1QLQuery;
import org.springframework.data.couchbase.core.query.N1qlJoin;
import org.springframework.data.couchbase.core.query.OptionsBuilder;
import org.springframework.data.couchbase.core.query.Query;
import org.springframework.data.couchbase.repository.Collection;
import org.springframework.data.couchbase.repository.Scope;
import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import com.couchbase.client.core.io.CollectionIdentifier;
import com.couchbase.client.java.query.QueryOptions;
/**
@@ -50,7 +57,7 @@ import com.couchbase.client.java.query.QueryOptions;
public class N1qlJoinResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(N1qlJoinResolver.class);
public static String buildQuery(ReactiveCouchbaseTemplate template, String collectionName,
public static <L, R> String buildQuery(ReactiveCouchbaseTemplate template, String scope, String collection,
N1qlJoinResolverParameters parameters) {
String joinType = "JOIN";
String selectEntity = "SELECT META(rks).id AS " + SELECT_ID + ", META(rks).cas AS " + SELECT_CAS + ", (rks).* ";
@@ -61,14 +68,16 @@ public class N1qlJoinResolver {
}
String useLKS = useLKSBuilder.length() > 0 ? "USE " + useLKSBuilder.toString() + " " : "";
String from = "FROM `" + template.getBucketName() + "` lks " + useLKS + joinType + " `" + template.getBucketName()
+ "` rks";
KeySpacePair keySpacePair = getKeySpacePair(template.getBucketName(), scope, collection, parameters);
StringBasedN1qlQueryParser.N1qlSpelValues n1qlL = Query.getN1qlSpelValues(template, collectionName,
String from = "FROM " + keySpacePair.lhs.keyspace + " lks " + useLKS + joinType + " " + keySpacePair.rhs.keyspace
+ " rks";
StringBasedN1qlQueryParser.N1qlSpelValues n1qlL = Query.getN1qlSpelValues(template, keySpacePair.lhs.collection,
parameters.getEntityTypeInfo().getType(), parameters.getEntityTypeInfo().getType(), false, null, null);
String onLks = "lks." + n1qlL.filter;
StringBasedN1qlQueryParser.N1qlSpelValues n1qlR = Query.getN1qlSpelValues(template, collectionName,
StringBasedN1qlQueryParser.N1qlSpelValues n1qlR = Query.getN1qlSpelValues(template, keySpacePair.rhs.collection,
parameters.getAssociatedEntityTypeInfo().getType(), parameters.getAssociatedEntityTypeInfo().getType(), false,
null, null);
String onRks = "rks." + n1qlR.filter;
@@ -111,10 +120,92 @@ public class N1qlJoinResolver {
return statementSb.toString();
}
public static <R> List<R> doResolve(ReactiveCouchbaseTemplate template, String collectionName,
static KeySpacePair getKeySpacePair(String bucketName, String scope, String collection,
N1qlJoinResolverParameters parameters) {
Class<?> lhsClass = parameters.getEntityTypeInfo().getActualType().getType();
String lhScope = scope != null ? scope : getScope(lhsClass);
String lhCollection = collection != null ? collection : getCollection(lhsClass);
Class<?> rhsClass = parameters.getAssociatedEntityTypeInfo().getActualType().getType();
String rhScope = getScope(rhsClass);
String rhCollection = getCollection(rhsClass);
if (lhCollection != null && rhCollection != null) {
// they both have non-default collections
// It's possible that the scope for the lhs was set with an annotation on a repository method,
// the entity class or the repository class or a query option. Since there is no means to set
// the scope of the associated class by the method, repository class or query option (only
// the annotation) we assume that the (possibly) dynamic scope of the entity would be a better
// choice as it is logical to put collections to be joined in the same scope. Note that lhScope
// is used for both keyspaces.
return new KeySpacePair(lhCollection, x(i(bucketName) + "." + i(lhScope) + "." + i(lhCollection)), //
rhCollection, x(i(bucketName) + "." + i(lhScope) + "." + i(rhCollection)));
} else if (lhCollection != null && rhCollection == null) {
// the lhs has a collection (and therefore a scope as well), but the rhs does not have a collection.
// Use the lhScope and lhCollection for the entity. The rhs is just the bucket.
return new KeySpacePair(lhCollection, x(i(bucketName) + "." + i(lhScope) + "." + i(lhCollection)), //
null, i(bucketName));
} else if (lhCollection != null && rhCollection == null) {
// the lhs does not have a collection (or scope), but rhs does have a collection
// Using the same (default) scope for the rhs would mean specifying a
// non-default collection in a default scope - which is not allowed.
// So use the scope and collection from the associated class.
return new KeySpacePair(null, i(bucketName), //
rhCollection, x(i(bucketName) + "." + i(rhScope) + "." + i(rhCollection)));
} else { // neither have collections, just use the bucket.
return new KeySpacePair(null, i(bucketName), null, i(bucketName));
}
}
static class KeySpacePair {
KeySpaceInfo lhs;
KeySpaceInfo rhs;
public KeySpacePair(String lhsCollection, N1QLExpression lhsKeyspace, String rhsCollection,
N1QLExpression rhsKeyspace) {
this.lhs = new KeySpaceInfo(lhsCollection, lhsKeyspace);
this.rhs = new KeySpaceInfo(rhsCollection, rhsKeyspace);
}
static class KeySpaceInfo {
String collection;
N1QLExpression keyspace;
public KeySpaceInfo(String collection, N1QLExpression keyspace) {
this.collection = collection;
this.keyspace = keyspace;
}
}
}
/**
* from CouchbaseQueryMethod.getCollection()
*
* @param targetClass
* @return
*/
static String getCollection(Class<?> targetClass) {
// Could try the repository method, then the targetClass, then the repository class, then the entity class
// but we don't have the repository method nor the repositoryMetdata at this point.
AnnotatedElement[] annotated = new AnnotatedElement[] { targetClass };
return OptionsBuilder.annotationString(Collection.class, CollectionIdentifier.DEFAULT_COLLECTION, annotated);
}
/**
* from CouchbaseQueryMethod.getScope()
*
* @param targetClass
* @return
*/
static String getScope(Class<?> targetClass) {
// Could try the repository method, then the targetClass, then the repository class, then the entity class
// but we don't have the repository method nor the repositoryMetdata at this point.
AnnotatedElement[] annotated = new AnnotatedElement[] { targetClass };
return OptionsBuilder.annotationString(Scope.class, CollectionIdentifier.DEFAULT_SCOPE, annotated);
}
public static <R> List<R> doResolve(ReactiveCouchbaseTemplate template, String scopeName, String collectionName,
N1qlJoinResolverParameters parameters, Class<R> associatedEntityClass) {
String statement = buildQuery(template, collectionName, parameters);
String statement = buildQuery(template, scopeName, collectionName, parameters);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Join query executed " + statement);
@@ -130,20 +221,24 @@ public class N1qlJoinResolver {
}
public static void handleProperties(CouchbasePersistentEntity<?> persistentEntity,
ConvertingPropertyAccessor<?> accessor, ReactiveCouchbaseTemplate template, String id) {
ConvertingPropertyAccessor<?> accessor, ReactiveCouchbaseTemplate template, String id, String scope,
String collection) {
persistentEntity.doWithProperties((PropertyHandler<CouchbasePersistentProperty>) prop -> {
if (prop.isAnnotationPresent(N1qlJoin.class)) {
N1qlJoin definition = prop.findAnnotation(N1qlJoin.class);
TypeInformation type = prop.getTypeInformation().getActualType();
Class clazz = type.getType();
N1qlJoinResolver.N1qlJoinResolverParameters parameters = new N1qlJoinResolver.N1qlJoinResolverParameters(
definition, id, persistentEntity.getTypeInformation(), type);
definition, id, persistentEntity.getTypeInformation(), type, scope, collection);
if (N1qlJoinResolver.isLazyJoin(definition)) {
N1qlJoinResolver.N1qlJoinProxy proxy = new N1qlJoinResolver.N1qlJoinProxy(template, parameters);
accessor.setProperty(prop,
java.lang.reflect.Proxy.newProxyInstance(List.class.getClassLoader(), new Class[] { List.class }, proxy));
} else {
accessor.setProperty(prop, N1qlJoinResolver.doResolve(template, null, parameters, clazz));
// clazz needs to be passes instead of just using
// parameters.associatedType.getTypeInformation().getActualType().getType
// to keep the compiler happy for the call template.findByQuery(associatedEntityClass)
accessor.setProperty(prop, N1qlJoinResolver.doResolve(template, scope, collection, parameters, clazz));
}
}
});
@@ -152,6 +247,7 @@ public class N1qlJoinResolver {
static public class N1qlJoinProxy implements InvocationHandler {
private final ReactiveCouchbaseTemplate reactiveTemplate;
private final String collectionName = null;
private final String scopeName = null;
private final N1qlJoinResolverParameters params;
private List<?> resolved = null;
@@ -163,8 +259,8 @@ public class N1qlJoinResolver {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (this.resolved == null) {
this.resolved = doResolve(this.reactiveTemplate, collectionName, this.params,
this.params.associatedEntityTypeInfo.getType());
this.resolved = doResolve(this.reactiveTemplate, this.params.getScopeName(), this.params.getCollectionName(),
this.params, this.params.associatedEntityTypeInfo.getType());
}
return method.invoke(this.resolved, args);
}
@@ -175,9 +271,11 @@ public class N1qlJoinResolver {
private String lksId;
private TypeInformation<?> entityTypeInfo;
private TypeInformation<?> associatedEntityTypeInfo;
private String scopeName;
private String collectionName;
public N1qlJoinResolverParameters(N1qlJoin joinDefinition, String lksId, TypeInformation<?> entityTypeInfo,
TypeInformation<?> associatedEntityTypeInfo) {
TypeInformation<?> associatedEntityTypeInfo, String scopeName, String collectionName) {
Assert.notNull(joinDefinition, "The join definition is required");
Assert.notNull(entityTypeInfo, "The entity type information is required");
Assert.notNull(associatedEntityTypeInfo, "The associated entity type information is required");
@@ -186,6 +284,8 @@ public class N1qlJoinResolver {
this.lksId = lksId;
this.entityTypeInfo = entityTypeInfo;
this.associatedEntityTypeInfo = associatedEntityTypeInfo;
this.scopeName = scopeName;
this.collectionName = collectionName;
}
public N1qlJoin getJoinDefinition() {
@@ -203,5 +303,13 @@ public class N1qlJoinResolver {
public TypeInformation getAssociatedEntityTypeInfo() {
return associatedEntityTypeInfo;
}
public String getScopeName() {
return scopeName;
}
public String getCollectionName() {
return collectionName;
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2020-2022 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
*
* https://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.domain;
import org.springframework.data.couchbase.repository.Collection;
import org.springframework.data.couchbase.repository.Scope;
/**
* @author Michael Reiche
*/
@Scope("dummy_scope") // set to non-existing scope. To use, scope must be determined by other means
// a different collection
@Collection("my_collection2")
public class AddressAnnotated extends Address {}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2020-2022 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
*
* https://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.domain;
import lombok.Data;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.core.mapping.Field;
import org.springframework.data.couchbase.core.query.FetchType;
import org.springframework.data.couchbase.core.query.N1qlJoin;
import org.springframework.data.couchbase.repository.Collection;
import org.springframework.data.couchbase.repository.Scope;
import java.util.List;
/**
* UserSubmissionAnnotated entity for tests
*
* @author Michael Reiche
*/
@Data
@Document
@TypeAlias("user")
@Scope("my_scope")
@Collection("my_collection")
public class UserSubmissionAnnotated extends ComparableEntity {
private String id;
private String username;
private String email;
private String password;
private List<String> roles;
@N1qlJoin(on = "meta(lks).id=rks.parentId", fetchType = FetchType.IMMEDIATE) List<AddressAnnotated> otherAddresses;
private Address address;
private int credits;
private List<Submission> submissions;
private List<Course> courses;
public void setSubmissions(List<Submission> submissions) {
this.submissions = submissions;
}
public void setCourses(List<Course> courses) {
this.courses = courses;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2022 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
*
* https://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.domain;
import java.util.List;
import org.springframework.data.couchbase.repository.ScanConsistency;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* UserSubmissionAnnotatedRepository for tests
*
* @author Michael Reiche
*/
@Repository
public interface UserSubmissionAnnotatedRepository extends PagingAndSortingRepository<UserSubmissionAnnotated, String> {
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<UserSubmissionAnnotated> findByUsername(String username);
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2020-2022 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
*
* https://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.domain;
import lombok.Data;
import java.util.List;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.core.query.FetchType;
import org.springframework.data.couchbase.core.query.N1qlJoin;
import org.springframework.data.couchbase.repository.Collection;
/**
* UserSubmissionAnnotated entity for tests
*
* @author Michael Reiche
*/
@Data
@Document
// there is no @Scope annotation on this entity
@Collection("my_collection")
@TypeAlias("user")
public class UserSubmissionUnannotated extends ComparableEntity {
private String id;
private String username;
private String email;
private String password;
private List<String> roles;
@N1qlJoin(on = "meta(lks).id=rks.parentId", fetchType = FetchType.IMMEDIATE) List<AddressAnnotated> otherAddresses;
private Address address;
private int credits;
private List<Submission> submissions;
private List<Course> courses;
public void setSubmissions(List<Submission> submissions) {
this.submissions = submissions;
}
public void setCourses(List<Course> courses) {
this.courses = courses;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2020-2022 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
*
* https://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.domain;
import java.util.List;
import org.springframework.data.couchbase.repository.ScanConsistency;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* UserSubmissionAnnotatedRepository for tests
*
* @author Michael Reiche
*/
@Repository
public interface UserSubmissionUnannotatedRepository
extends PagingAndSortingRepository<UserSubmissionUnannotated, String> {
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<UserSubmissionUnannotated> findByUsername(String username);
}

View File

@@ -17,6 +17,7 @@
package org.springframework.data.couchbase.repository;
import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

View File

@@ -903,9 +903,6 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
@Test
void findPlusN1qlJoin() throws Exception {
// needs an index for this N1ql Join
// create index ix2 on my_bucket(parent_id) where `_class` = 'org.springframework.data.couchbase.domain.Address';
UserSubmission user = new UserSubmission();
user.setId(UUID.randomUUID().toString());
user.setUsername("dave");
@@ -944,7 +941,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
}
couchbaseTemplate.removeById(Address.class)
.all(Arrays.asList(address1.getId(), address2.getId(), address3.getId(), user.getId()));
.all(Arrays.asList(address1.getId(), address2.getId(), address3.getId()));
couchbaseTemplate.removeById(UserSubmission.class).one(user.getId());
}
@Test

View File

@@ -19,7 +19,9 @@ import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_SCOPE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
@@ -30,12 +32,18 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.AddressAnnotated;
import org.springframework.data.couchbase.domain.Airport;
import org.springframework.data.couchbase.domain.AirportRepository;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserCol;
import org.springframework.data.couchbase.domain.UserColRepository;
import org.springframework.data.couchbase.domain.UserSubmissionAnnotated;
import org.springframework.data.couchbase.domain.UserSubmissionAnnotatedRepository;
import org.springframework.data.couchbase.domain.UserSubmissionUnannotated;
import org.springframework.data.couchbase.domain.UserSubmissionUnannotatedRepository;
import org.springframework.data.couchbase.util.Capabilities;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.CollectionAwareIntegrationTests;
@@ -50,8 +58,10 @@ import com.couchbase.client.java.query.QueryScanConsistency;
@IgnoreWhen(missesCapabilities = { Capabilities.QUERY, Capabilities.COLLECTIONS }, clusterTypes = ClusterType.MOCKED)
public class CouchbaseRepositoryQueryCollectionIntegrationTests extends CollectionAwareIntegrationTests {
@Autowired AirportRepository airportRepository;
@Autowired UserColRepository userColRepository;
@Autowired AirportRepository airportRepository; // initialized in beforeEach()
@Autowired UserColRepository userColRepository; // initialized in beforeEach()
@Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository; // initialized in beforeEach()
@Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository; // initialized in beforeEach()
@BeforeAll
public static void beforeAll() {
@@ -80,6 +90,10 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
// seems that @Autowired is not adequate, so ...
airportRepository = (AirportRepository) ac.getBean("airportRepository");
userColRepository = (UserColRepository) ac.getBean("userColRepository");
userSubmissionAnnotatedRepository = (UserSubmissionAnnotatedRepository) ac
.getBean("userSubmissionAnnotatedRepository");
userSubmissionUnannotatedRepository = (UserSubmissionUnannotatedRepository) ac
.getBean("userSubmissionUnannotatedRepository");
}
@AfterEach
@@ -233,4 +247,116 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
} catch (DataRetrievalFailureException drfe) {}
}
}
@Test
void findPlusN1qlJoinBothAnnotated() throws Exception {
// UserSubmissionAnnotated has scope=my_scope, collection=my_collection
UserSubmissionAnnotated user = new UserSubmissionAnnotated();
user.setId(UUID.randomUUID().toString());
user.setUsername("dave");
user = userSubmissionAnnotatedRepository.save(user);
// AddressesAnnotated has scope=dummy_scope, collection=my_collection2
// scope must be explicitly set on template insertById, findByQuery and removeById
// For userSubmissionAnnotatedRepository.findByUsername(), scope will be taken from UserSubmissionAnnotated
AddressAnnotated address1 = new AddressAnnotated();
address1.setId(UUID.randomUUID().toString());
address1.setStreet("3250 Olcott Street");
address1.setParentId(user.getId());
AddressAnnotated address2 = new AddressAnnotated();
address2.setId(UUID.randomUUID().toString());
address2.setStreet("148 Castro Street");
address2.setParentId(user.getId());
AddressAnnotated address3 = new AddressAnnotated();
address3.setId(UUID.randomUUID().toString());
address3.setStreet("123 Sesame Street");
address3.setParentId(UUID.randomUUID().toString()); // does not belong to user
try {
address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1);
address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2);
address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3);
couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inScope(scopeName).all();
// scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository.
List<UserSubmissionAnnotated> users = userSubmissionAnnotatedRepository.findByUsername(user.getUsername());
assertEquals(2, users.get(0).getOtherAddresses().size());
for (Address a : users.get(0).getOtherAddresses()) {
if (!(a.getStreet().equals(address1.getStreet()) || a.getStreet().equals(address2.getStreet()))) {
throw new Exception("street does not match : " + a);
}
}
UserSubmissionAnnotated foundUser = userSubmissionAnnotatedRepository.findById(user.getId()).get();
assertEquals(2, foundUser.getOtherAddresses().size());
for (Address a : foundUser.getOtherAddresses()) {
if (!(a.getStreet().equals(address1.getStreet()) || a.getStreet().equals(address2.getStreet()))) {
throw new Exception("street does not match : " + a);
}
}
} finally {
couchbaseTemplate.removeById(AddressAnnotated.class).inScope(scopeName)
.all(Arrays.asList(address1.getId(), address2.getId(), address3.getId()));
couchbaseTemplate.removeById(UserSubmissionAnnotated.class).one(user.getId());
}
}
@Test
void findPlusN1qlJoinUnannotated() throws Exception {
// UserSubmissionAnnotated has scope=my_scope, collection=my_collection
UserSubmissionUnannotated user = new UserSubmissionUnannotated();
user.setId(UUID.randomUUID().toString());
user.setUsername("dave");
user = userSubmissionUnannotatedRepository.save(user);
// AddressesAnnotated has scope=dummy_scope, collection=my_collection2
// scope must be explicitly set on template insertById, findByQuery and removeById
// For userSubmissionAnnotatedRepository.findByUsername(), scope will be taken from UserSubmissionAnnotated
AddressAnnotated address1 = new AddressAnnotated();
address1.setId(UUID.randomUUID().toString());
address1.setStreet("3250 Olcott Street");
address1.setParentId(user.getId());
AddressAnnotated address2 = new AddressAnnotated();
address2.setId(UUID.randomUUID().toString());
address2.setStreet("148 Castro Street");
address2.setParentId(user.getId());
AddressAnnotated address3 = new AddressAnnotated();
address3.setId(UUID.randomUUID().toString());
address3.setStreet("123 Sesame Street");
address3.setParentId(UUID.randomUUID().toString()); // does not belong to user
try {
address1 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address1);
address2 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address2);
address3 = couchbaseTemplate.insertById(AddressAnnotated.class).inScope(scopeName).one(address3);
couchbaseTemplate.findByQuery(AddressAnnotated.class).withConsistency(QueryScanConsistency.REQUEST_PLUS)
.inScope(scopeName).all();
// scope for AddressesAnnotated in N1qlJoin comes from userSubmissionAnnotatedRepository.
List<UserSubmissionUnannotated> users = userSubmissionUnannotatedRepository.findByUsername(user.getUsername());
assertEquals(2, users.get(0).getOtherAddresses().size());
for (Address a : users.get(0).getOtherAddresses()) {
if (!(a.getStreet().equals(address1.getStreet()) || a.getStreet().equals(address2.getStreet()))) {
throw new Exception("street does not match : " + a);
}
}
UserSubmissionUnannotated foundUser = userSubmissionUnannotatedRepository.findById(user.getId()).get();
assertEquals(2, foundUser.getOtherAddresses().size());
for (Address a : foundUser.getOtherAddresses()) {
if (!(a.getStreet().equals(address1.getStreet()) || a.getStreet().equals(address2.getStreet()))) {
throw new Exception("street does not match : " + a);
}
}
} finally {
couchbaseTemplate.removeById(AddressAnnotated.class).inScope(scopeName)
.all(Arrays.asList(address1.getId(), address2.getId(), address3.getId()));
couchbaseTemplate.removeById(UserSubmissionUnannotated.class).one(user.getId());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors
* Copyright 2012-2022 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.
@@ -51,7 +51,7 @@ import com.couchbase.client.java.manager.query.CreateQueryIndexOptions;
public abstract class ClusterAwareIntegrationTests {
private static TestClusterConfig testClusterConfig;
private static final Logger LOGGER = LoggerFactory.getLogger(ClusterAwareIntegrationTests.class);
public static final Logger LOGGER = LoggerFactory.getLogger(ClusterAwareIntegrationTests.class);
@BeforeAll
static void setup(TestClusterConfig config) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2021 the original author or authors
* Copyright 2021-2022 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.
@@ -19,8 +19,8 @@ import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMP
import static org.springframework.data.couchbase.config.BeanNames.REACTIVE_COUCHBASE_TEMPLATE;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
@@ -30,14 +30,13 @@ import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.domain.Config;
import com.couchbase.client.core.error.IndexExistsException;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.Cluster;
import com.couchbase.client.java.ClusterOptions;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.manager.collection.CollectionManager;
import com.couchbase.client.java.manager.collection.CollectionSpec;
import com.couchbase.client.java.manager.collection.ScopeSpec;
/**
* Provides Collection support for integration tests
@@ -49,6 +48,7 @@ public class CollectionAwareIntegrationTests extends JavaIntegrationTests {
public static String scopeName = "my_scope";// + randomString();
public static String otherScope = "other_scope";
public static String collectionName = "my_collection";// + randomString();
public static String collectionName2 = "my_collection2";// + randomString();
public static String otherCollection = "other_collection";// + randomString();
@BeforeAll
@@ -64,11 +64,25 @@ public class CollectionAwareIntegrationTests extends JavaIntegrationTests {
CollectionManager collectionManager = bucket.collections();
setupScopeCollection(cluster, scopeName, collectionName, collectionManager);
setupScopeCollection(cluster, scopeName, collectionName2, collectionManager);
if (otherScope != null || otherCollection != null) {
// afterAll should be undoing the creation of scope etc
setupScopeCollection(cluster, otherScope, otherCollection, collectionManager);
}
try {
// needs an index for this N1ql Join
// create index ix2 on my_bucket(parent_id) where `_class` = 'org.springframework.data.couchbase.domain.Address';
List<String> fieldList = new ArrayList<>();
fieldList.add("parentId");
cluster.query("CREATE INDEX `parent_idx` ON default:" + bucketName() + "." + scopeName + "." + collectionName2
+ "(parentId)");
} catch (IndexExistsException ife) {
LOGGER.warn("IndexFailureException occurred - ignoring: ", ife.toString());
}
Config.setScopeName(scopeName);
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
// the Config class has been modified, these need to be loaded again