From da3e8e0e36ed889705aa78b0bc1dd2d1a24a999a Mon Sep 17 00:00:00 2001 From: Michael Reiche <48999328+mikereiche@users.noreply.github.com> Date: Sun, 12 Sep 2021 18:38:01 -0700 Subject: [PATCH] Support N1QL Join Annotations. (#1211) * Support N1QL Join Annotations. Closes #1198. --- .../couchbase/core/CouchbaseTemplate.java | 2 +- .../core/CouchbaseTemplateSupport.java | 7 +- .../core/ReactiveCouchbaseTemplate.java | 2 +- .../ReactiveCouchbaseTemplateSupport.java | 6 +- .../core/convert/join/N1qlJoinResolver.java | 87 ++++++++++++++----- .../data/couchbase/core/query/N1QLQuery.java | 11 ++- .../data/couchbase/core/query/Query.java | 3 +- .../data/couchbase/domain/Address.java | 34 ++++++++ .../data/couchbase/domain/UserSubmission.java | 12 ++- .../domain/UserSubmissionRepository.java | 10 ++- ...chbaseRepositoryQueryIntegrationTests.java | 68 +++++++++++++-- 11 files changed, 200 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 124cab2d..6e7cc635 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -58,7 +58,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex final TranslationService translationService) { this.clientFactory = clientFactory; this.converter = converter; - this.templateSupport = new CouchbaseTemplateSupport(converter, translationService); + this.templateSupport = new CouchbaseTemplateSupport(this, converter, translationService); this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, translationService); this.mappingContext = this.converter.getMappingContext(); 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 d0b1b359..ffb70fc6 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplateSupport.java @@ -23,6 +23,7 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; @@ -53,13 +54,16 @@ class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSuppo private static final Logger LOG = LoggerFactory.getLogger(CouchbaseTemplateSupport.class); + private final CouchbaseTemplate template; private final CouchbaseConverter converter; private final MappingContext, CouchbasePersistentProperty> mappingContext; private final TranslationService translationService; private EntityCallbacks entityCallbacks; private ApplicationContext applicationContext; - public CouchbaseTemplateSupport(final CouchbaseConverter converter, final TranslationService translationService) { + public CouchbaseTemplateSupport(final CouchbaseTemplate template, final CouchbaseConverter converter, + final TranslationService translationService) { + this.template = template; this.converter = converter; this.mappingContext = converter.getMappingContext(); this.translationService = translationService; @@ -91,6 +95,7 @@ class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSuppo if (persistentEntity.getVersionProperty() != null) { accessor.setProperty(persistentEntity.getVersionProperty(), cas); } + N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id); return accessor.getBean(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java index a20e438e..838432b1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplate.java @@ -54,7 +54,7 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A this.clientFactory = clientFactory; this.converter = converter; this.exceptionTranslator = clientFactory.getExceptionTranslator(); - this.templateSupport = new ReactiveCouchbaseTemplateSupport(converter, translationService); + this.templateSupport = new ReactiveCouchbaseTemplateSupport(this, converter, translationService); } @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 a1da7923..d2f9d69a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateSupport.java @@ -24,6 +24,7 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver; import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; @@ -52,14 +53,16 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React private static final Logger LOG = LoggerFactory.getLogger(ReactiveCouchbaseTemplateSupport.class); + private final ReactiveCouchbaseTemplate template; private final CouchbaseConverter converter; private final MappingContext, CouchbasePersistentProperty> mappingContext; private final TranslationService translationService; private ReactiveEntityCallbacks reactiveEntityCallbacks; private ApplicationContext applicationContext; - public ReactiveCouchbaseTemplateSupport(final CouchbaseConverter converter, + public ReactiveCouchbaseTemplateSupport(final ReactiveCouchbaseTemplate template, final CouchbaseConverter converter, final TranslationService translationService) { + this.template = template; this.converter = converter; this.mappingContext = converter.getMappingContext(); this.translationService = translationService; @@ -92,6 +95,7 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React if (persistentEntity.getVersionProperty() != null) { accessor.setProperty(persistentEntity.getVersionProperty(), cas); } + N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id); return accessor.getBean(); }); } 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 35b0a5bf..6044a2a4 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-2020 the original author or authors + * Copyright 2018-2021 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,7 +16,8 @@ package org.springframework.data.couchbase.core.convert.join; -import static org.springframework.data.couchbase.core.support.TemplateUtils.*; +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.InvocationHandler; import java.lang.reflect.Method; @@ -24,13 +25,23 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.FetchType; import org.springframework.data.couchbase.core.query.HashSide; +import org.springframework.data.couchbase.core.query.N1QLExpression; +import org.springframework.data.couchbase.core.query.N1QLQuery; import org.springframework.data.couchbase.core.query.N1qlJoin; +import org.springframework.data.couchbase.core.query.Query; +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.java.query.QueryOptions; + /** * N1qlJoinResolver resolves by converting the join definition to query statement and executing using CouchbaseTemplate * @@ -39,7 +50,8 @@ import org.springframework.util.Assert; public class N1qlJoinResolver { private static final Logger LOGGER = LoggerFactory.getLogger(N1qlJoinResolver.class); - public static String buildQuery(CouchbaseTemplate template, N1qlJoinResolverParameters parameters) { + public static String buildQuery(ReactiveCouchbaseTemplate template, String collectionName, + N1qlJoinResolverParameters parameters) { String joinType = "JOIN"; String selectEntity = "SELECT META(rks).id AS " + SELECT_ID + ", META(rks).cas AS " + SELECT_CAS + ", (rks).* "; @@ -51,10 +63,15 @@ public class N1qlJoinResolver { String from = "FROM `" + template.getBucketName() + "` lks " + useLKS + joinType + " `" + template.getBucketName() + "` rks"; - String onLks = "lks." + template.getConverter().getTypeKey() + " = \"" - + parameters.getEntityTypeInfo().getType().getName() + "\""; - String onRks = "rks." + template.getConverter().getTypeKey() + " = \"" - + parameters.getAssociatedEntityTypeInfo().getType().getName() + "\""; + + StringBasedN1qlQueryParser.N1qlSpelValues n1qlL = Query.getN1qlSpelValues(template, collectionName, + parameters.getEntityTypeInfo().getType(), parameters.getEntityTypeInfo().getType(), false, null, null); + String onLks = "lks." + n1qlL.filter; + + StringBasedN1qlQueryParser.N1qlSpelValues n1qlR = Query.getN1qlSpelValues(template, collectionName, + parameters.getAssociatedEntityTypeInfo().getType(), parameters.getAssociatedEntityTypeInfo().getType(), false, + null, null); + String onRks = "rks." + n1qlR.filter; StringBuilder useRKSBuilder = new StringBuilder(); if (parameters.getJoinDefinition().rightIndex().length() > 0) { @@ -94,38 +111,60 @@ public class N1qlJoinResolver { return statementSb.toString(); } - public static List doResolve(CouchbaseTemplate template, N1qlJoinResolverParameters parameters, - Class associatedEntityClass) { - throw new UnsupportedOperationException(); - /* - String statement = buildQuery(template, parameters); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Join query executed " + statement); - } - - N1QLQuery query = new N1QLQuery(N1QLExpression.x(statement), QueryOptions.queryOptions()); - return template.findByN1QL(query, associatedEntityClass);*/ + public static List doResolve(ReactiveCouchbaseTemplate template, String collectionName, + N1qlJoinResolverParameters parameters, Class associatedEntityClass) { + + String statement = buildQuery(template, collectionName, parameters); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Join query executed " + statement); + } + + N1QLQuery query = new N1QLQuery(N1QLExpression.x(statement), QueryOptions.queryOptions()); + List result = template.findByQuery(associatedEntityClass).matching(query).all().collectList().block(); + return result.isEmpty() ? null : result; } public static boolean isLazyJoin(N1qlJoin joinDefinition) { return joinDefinition.fetchType().equals(FetchType.LAZY); } + public static void handleProperties(CouchbasePersistentEntity persistentEntity, + ConvertingPropertyAccessor accessor, ReactiveCouchbaseTemplate template, String id) { + 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); + 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)); + } + } + }); + } + static public class N1qlJoinProxy implements InvocationHandler { - private final CouchbaseTemplate template; + private final ReactiveCouchbaseTemplate reactiveTemplate; + private final String collectionName = null; private final N1qlJoinResolverParameters params; private List resolved = null; - public N1qlJoinProxy(CouchbaseTemplate template, N1qlJoinResolverParameters params) { - this.template = template; + public N1qlJoinProxy(ReactiveCouchbaseTemplate template, N1qlJoinResolverParameters params) { + this.reactiveTemplate = template; this.params = params; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (this.resolved == null) { - this.resolved = doResolve(this.template, this.params, this.params.associatedEntityTypeInfo.getType()); + this.resolved = doResolve(this.reactiveTemplate, collectionName, this.params, + this.params.associatedEntityTypeInfo.getType()); } return method.invoke(this.resolved, args); } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java index 1594c6d1..ba354f93 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1QLQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2021 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. @@ -15,10 +15,12 @@ */ package org.springframework.data.couchbase.core.query; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; + import com.couchbase.client.java.json.JsonObject; import com.couchbase.client.java.query.QueryOptions; -public class N1QLQuery { +public class N1QLQuery extends Query { private N1QLExpression expression; private QueryOptions options; @@ -45,4 +47,9 @@ public class N1QLQuery { return query; } + @Override + public String toN1qlSelectString(ReactiveCouchbaseTemplate template, String collectionName, Class domainClass, + Class returnClass, boolean isCount, String[] distinctFields, String[] fields) { + return expression.toString(); + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index 7942aaf3..cfcbf7d8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -349,7 +349,8 @@ public class Query { return statement.toString(); } - StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, String collectionName, + public static StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues( + ReactiveCouchbaseTemplate template, String collectionName, Class domainClass, Class returnClass, boolean isCount, String[] distinctFields, String[] fields) { String typeKey = template.getConverter().getTypeKey(); final CouchbasePersistentEntity persistentEntity = template.getConverter().getMappingContext() diff --git a/src/test/java/org/springframework/data/couchbase/domain/Address.java b/src/test/java/org/springframework/data/couchbase/domain/Address.java index 54724a56..59b5313b 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Address.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Address.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2021 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.core.mapping.Document; @@ -7,6 +22,9 @@ public class Address extends ComparableEntity { private String street; private String city; + // for N1qlJoin + private String id; + private String parentId; public Address() {} @@ -26,4 +44,20 @@ public class Address extends ComparableEntity { this.city = city; } + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java index a301b5da..929da69a 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmission.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors + * Copyright 2020-2021 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. @@ -17,12 +17,14 @@ 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.index.CompositeQueryIndex; import org.springframework.data.couchbase.core.mapping.Document; - -import java.lang.reflect.Field; -import java.util.List; +import org.springframework.data.couchbase.core.query.FetchType; +import org.springframework.data.couchbase.core.query.N1qlJoin; /** * UserSubmission entity for tests @@ -40,6 +42,8 @@ public class UserSubmission extends ComparableEntity { private String password; private List roles; private Address address; + @N1qlJoin(on = "meta(lks).id=rks.parentId", fetchType = FetchType.IMMEDIATE) List
otherAddresses; + private int credits; private List submissions; private List courses; diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java index 1a3d17b5..2f9894c1 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserSubmissionRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors + * Copyright 2020-2021 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,14 @@ 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; + /** * UserSubmission Repository for tests * @@ -27,4 +32,7 @@ import org.springframework.stereotype.Repository; @Repository public interface UserSubmissionRepository extends PagingAndSortingRepository { + @ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS) + List findByUsername(String username); + } 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 e8b160d3..ff289e75 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -33,6 +33,7 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -64,6 +65,8 @@ import org.springframework.data.couchbase.domain.PersonRepository; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserAnnotated; import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.domain.UserSubmission; +import org.springframework.data.couchbase.domain.UserSubmissionRepository; import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; @@ -83,13 +86,14 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.AmbiguousTimeoutException; import com.couchbase.client.core.error.CouchbaseException; -import com.couchbase.client.core.error.IndexExistsException; import com.couchbase.client.core.error.IndexFailureException; import com.couchbase.client.java.env.ClusterEnvironment; import com.couchbase.client.java.json.JsonArray; import com.couchbase.client.java.kv.GetResult; import com.couchbase.client.java.kv.MutationState; import com.couchbase.client.java.kv.UpsertOptions; +import com.couchbase.client.java.manager.query.CreatePrimaryQueryIndexOptions; +import com.couchbase.client.java.manager.query.CreateQueryIndexOptions; import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; @@ -110,6 +114,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr @Autowired UserRepository userRepository; + @Autowired UserSubmissionRepository userSubmissionRepository; + @Autowired CouchbaseTemplate couchbaseTemplate; String scopeName = "_default"; @@ -117,11 +123,14 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr @BeforeEach public void beforeEach() { - try { - clientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName()); - } catch (IndexExistsException ex) { - // ignore, all good. - } + clientFactory.getCluster().queryIndexes().createPrimaryIndex(bucketName(), + CreatePrimaryQueryIndexOptions.createPrimaryQueryIndexOptions().ignoreIfExists(true)); + // this is for the N1qlJoin test + List fieldList = new ArrayList<>(); + fieldList.add("parentId"); + clientFactory.getCluster().queryIndexes().createIndex(bucketName(), "parent_idx", fieldList, + CreateQueryIndexOptions.createQueryIndexOptions().ignoreIfExists(true)); + // .with("_class", "org.springframework.data.couchbase.domain.Address")); } @Test @@ -690,6 +699,53 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr } } + @Test + void findPlusN1qlJoin() throws Exception { + + // needs an index for this N1ql Join + // create index ix2 on my_bucket(parent_id) where `_class` = 'org.springframework.data.couchbase.domain.Address'; + + UserSubmission user = new UserSubmission(); + user.setId(UUID.randomUUID().toString()); + user.setUsername("dave"); + user = couchbaseTemplate.insertById(UserSubmission.class).one(user); + + Address address1 = new Address(); + address1.setId(UUID.randomUUID().toString()); + address1.setStreet("3250 Olcott Street"); + address1.setParentId(user.getId()); + Address address2 = new Address(); + address2.setId(UUID.randomUUID().toString()); + address2.setStreet("148 Castro Street"); + address2.setParentId(user.getId()); + Address address3 = new Address(); + address3.setId(UUID.randomUUID().toString()); + address3.setStreet("123 Sesame Street"); + address3.setParentId(UUID.randomUUID().toString()); // does not belong to user + address1 = couchbaseTemplate.insertById(Address.class).one(address1); + address2 = couchbaseTemplate.insertById(Address.class).one(address2); + address3 = couchbaseTemplate.insertById(Address.class).one(address3); + + List users = userSubmissionRepository.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); + } + } + + UserSubmission foundUser = userSubmissionRepository.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); + } + } + + couchbaseTemplate.removeById(Address.class) + .all(Arrays.asList(address1.getId(), address2.getId(), address3.getId(), user.getId())); + } + private void sleep(int millis) { try { Thread.sleep(millis); // so they are executed out-of-order