From 2e497416804a9b52eec23f32253c413134a7654e Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Tue, 15 Feb 2022 11:37:54 -0800 Subject: [PATCH] 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. --- .../core/CouchbaseTemplateSupport.java | 5 +- .../core/NonReactiveSupportWrapper.java | 7 +- .../ReactiveCouchbaseTemplateSupport.java | 5 +- ...activeFindByAnalyticsOperationSupport.java | 4 +- .../ReactiveFindByIdOperationSupport.java | 6 +- .../ReactiveFindByQueryOperationSupport.java | 4 +- ...eFindFromReplicasByIdOperationSupport.java | 5 +- .../core/ReactiveTemplateSupport.java | 8 +- .../data/couchbase/core/TemplateSupport.java | 8 +- .../core/convert/join/N1qlJoinResolver.java | 136 ++++++++++++++++-- .../couchbase/domain/AddressAnnotated.java | 28 ++++ .../domain/UserSubmissionAnnotated.java | 60 ++++++++ .../UserSubmissionAnnotatedRepository.java | 37 +++++ .../domain/UserSubmissionUnannotated.java | 59 ++++++++ .../UserSubmissionUnannotatedRepository.java | 38 +++++ ...aseRepositoryKeyValueIntegrationTests.java | 1 + ...chbaseRepositoryQueryIntegrationTests.java | 6 +- ...sitoryQueryCollectionIntegrationTests.java | 130 ++++++++++++++++- .../util/ClusterAwareIntegrationTests.java | 4 +- .../util/CollectionAwareIntegrationTests.java | 24 +++- 20 files changed, 528 insertions(+), 47 deletions(-) create mode 100644 src/test/java/org/springframework/data/couchbase/domain/AddressAnnotated.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotated.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotatedRepository.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotated.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotatedRepository.java diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java index 1181bc89..5295dec0 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -85,7 +85,7 @@ class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSuppo } @Override - public T decodeEntity(String id, String source, long cas, Class entityClass) { + public T decodeEntity(String id, String source, long cas, Class 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; diff --git a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java index 6e64ecb3..bee19a4a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/NonReactiveSupportWrapper.java @@ -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 Mono decodeEntity(String id, String source, long cas, Class entityClass) { - return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass)); + public Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, + String collection) { + return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection)); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java index a36b6859..7a500d46 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -85,7 +85,8 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React } @Override - public Mono decodeEntity(String id, String source, long cas, Class entityClass) { + public Mono decodeEntity(String id, String source, long cas, Class 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(); }); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java index a5463984..b46a81f5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByAnalyticsOperationSupport.java @@ -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); }); }); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index c23737ef..34705457 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -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(); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index d5dd83cc..cfa3e0f3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -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()); })); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java index 88cdcd61..0e1372b4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindFromReplicasByIdOperationSupport.java @@ -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); diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java index 5f897720..0fa72557 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveTemplateSupport.java @@ -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 encodeEntity(Object entityToEncode); - Mono decodeEntity(String id, String source, long cas, Class entityClass); + Mono decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection); Mono applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); diff --git a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java index bc42da3f..084b1b71 100644 --- a/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/TemplateSupport.java @@ -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 decodeEntity(String id, String source, long cas, Class entityClass); + T decodeEntity(String id, String source, long cas, Class entityClass, String scope, String collection); T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas); diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java b/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java index 6044a2a4..9ccd40fe 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/join/N1qlJoinResolver.java @@ -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 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 List 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 List doResolve(ReactiveCouchbaseTemplate template, String scopeName, String collectionName, N1qlJoinResolverParameters parameters, Class 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) 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; + } } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/AddressAnnotated.java b/src/test/java/org/springframework/data/couchbase/domain/AddressAnnotated.java new file mode 100644 index 00000000..5ce10d8c --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AddressAnnotated.java @@ -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 {} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotated.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotated.java new file mode 100644 index 00000000..d58c5568 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotated.java @@ -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 roles; + @N1qlJoin(on = "meta(lks).id=rks.parentId", fetchType = FetchType.IMMEDIATE) List otherAddresses; + private Address address; + private int credits; + private List submissions; + private List courses; + + public void setSubmissions(List submissions) { + this.submissions = submissions; + } + + public void setCourses(List courses) { + this.courses = courses; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotatedRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotatedRepository.java new file mode 100644 index 00000000..551aad1a --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionAnnotatedRepository.java @@ -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 { + + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + List findByUsername(String username); +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotated.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotated.java new file mode 100644 index 00000000..361f8cd2 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotated.java @@ -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 roles; + @N1qlJoin(on = "meta(lks).id=rks.parentId", fetchType = FetchType.IMMEDIATE) List otherAddresses; + private Address address; + private int credits; + private List submissions; + private List courses; + + public void setSubmissions(List submissions) { + this.submissions = submissions; + } + + public void setCourses(List courses) { + this.courses = courses; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotatedRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotatedRepository.java new file mode 100644 index 00000000..46b370e9 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionUnannotatedRepository.java @@ -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 { + + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + List findByUsername(String username); +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index 384f8234..e3381b56 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -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; diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index 9933d929..32badefb 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -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 diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index 2eee78b6..748401ce 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -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 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 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()); + } + } + } diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 42fb74a6..6e96c5e7 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -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) { diff --git a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java index fb54b55d..ffeeb6b5 100644 --- a/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/CollectionAwareIntegrationTests.java @@ -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 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