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 a54639844..b46a81f53 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 384f8234e..e3381b568 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 79f709d84..fd8ac10fa 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -904,9 +904,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"); @@ -945,7 +942,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 2eee78b6c..748401ce3 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 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 @@ -233,4 +247,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