From 0535004fa3aa294efc82319301bcdc902952a8a2 Mon Sep 17 00:00:00 2001 From: Michael Reiche Date: Wed, 9 Feb 2022 20:55:13 -0800 Subject: [PATCH] Add collections support to N1qlJoin. 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 | 15 +- ...chbaseRepositoryQueryIntegrationTests.java | 6 +- ...sitoryQueryCollectionIntegrationTests.java | 132 ++++++++++++++++- .../util/ClusterAwareIntegrationTests.java | 4 +- .../util/CollectionAwareIntegrationTests.java | 24 +++- 20 files changed, 537 insertions(+), 54 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 3764cd778..07087759c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -84,7 +84,7 @@ public CouchbaseDocument encodeEntity(final Object entityToEncode) { } @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); @@ -109,9 +109,10 @@ public T decodeEntity(String id, String source, long cas, Class entityCla 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 6e64ecb3b..bee19a4a8 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 Mono encodeEntity(Object entityToEncode) { } @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 fda461de8..5cf321b45 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -84,7 +84,8 @@ public Mono encodeEntity(final Object entityToEncode) { } @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); @@ -111,7 +112,7 @@ public Mono decodeEntity(String id, String source, long cas, Class ent 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 53cce70de..ecd0945a9 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 Flux all() { 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 c23737eff..347054579 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 Mono one(final String id) { } 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 d5dd83ccf..cfa3e0f34 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 Flux all() { 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 88cdcd613..0e1372b4f 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 Mono any(final String id) { 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 5f8977202..0fa725574 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 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 bc42da3f8..084b1b718 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 @@ 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 6044a2a46..9ccd40fee 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.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 @@ 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 static String buildQuery(ReactiveCouchbaseTemplate template, String colle } 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 static String buildQuery(ReactiveCouchbaseTemplate template, String colle 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 static boolean isLazyJoin(N1qlJoin joinDefinition) { } 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 static void handleProperties(CouchbasePersistentEntity persistentEntit 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 N1qlJoinProxy(ReactiveCouchbaseTemplate template, N1qlJoinResolverParamet @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 @@ static public class N1qlJoinResolverParameters { 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 N1qlJoinResolverParameters(N1qlJoin joinDefinition, String lksId, TypeInf this.lksId = lksId; this.entityTypeInfo = entityTypeInfo; this.associatedEntityTypeInfo = associatedEntityTypeInfo; + this.scopeName = scopeName; + this.collectionName = collectionName; } public N1qlJoin getJoinDefinition() { @@ -203,5 +303,13 @@ public TypeInformation getEntityTypeInfo() { 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 000000000..5ce10d8c5 --- /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 000000000..d58c55688 --- /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 000000000..551aad1a9 --- /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 000000000..361f8cd2b --- /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 000000000..46b370e98 --- /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 41612eedd..70dc2b080 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.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. @@ -16,17 +16,18 @@ package org.springframework.data.couchbase.repository; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.UUID; -import com.couchbase.client.java.kv.GetResult; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; @@ -50,6 +51,8 @@ import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import com.couchbase.client.java.kv.GetResult; + /** * Repository KV tests * @@ -138,7 +141,7 @@ void saveAndFindByWithNestedId() { user.setCourses(Arrays.asList(new Course(UUID.randomUUID().toString(), user.getId(), "581"))); // this currently fails when using mocked in integration.properties with status "UNKNOWN" - assertFalse(userRepository.existsById(user.getId())); + assertFalse(userSubmissionRepository.existsById(user.getId())); userSubmissionRepository.save(user); @@ -146,7 +149,7 @@ void saveAndFindByWithNestedId() { assertTrue(found.isPresent()); found.ifPresent(u -> assertEquals(user, u)); - assertTrue(userRepository.existsById(user.getId())); + assertTrue(userSubmissionRepository.existsById(user.getId())); assertEquals(user.getSubmissions().get(0).getId(), found.get().getSubmissions().get(0).getId()); assertEquals(user.getCourses().get(0).getId(), found.get().getCourses().get(0).getId()); assertEquals(user, found.get()); 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 d1e1d4480..2c767be15 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -895,9 +895,6 @@ void findBySimplePropertyAudited() { @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"); @@ -936,7 +933,8 @@ void findPlusN1qlJoin() throws Exception { } 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 82d24e8d5..4d37c67d8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-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,7 +19,9 @@ 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.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 @@ @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 void beforeEach() { // 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 @@ -223,4 +237,116 @@ public void testScopeCollectionRepoWith() { } 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 42fb74a67..6e96c5e7a 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 @@ 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 fb54b55dc..ffeeb6b5d 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.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.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 static void beforeAll() { 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