Support N1QL Join Annotations. (#1211)

* Support N1QL Join Annotations.

Closes #1198.
This commit is contained in:
Michael Reiche
2021-09-12 18:38:01 -07:00
committed by GitHub
parent 4bd82ea23c
commit da3e8e0e36
11 changed files with 200 additions and 42 deletions

View File

@@ -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();

View File

@@ -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<? extends CouchbasePersistentEntity<?>, 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();
}

View File

@@ -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

View File

@@ -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<? extends CouchbasePersistentEntity<?>, 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();
});
}

View File

@@ -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 <R> List<R> doResolve(CouchbaseTemplate template, N1qlJoinResolverParameters parameters,
Class<R> 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 <R> List<R> doResolve(ReactiveCouchbaseTemplate template, String collectionName,
N1qlJoinResolverParameters parameters, Class<R> 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<R> 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<CouchbasePersistentProperty>) prop -> {
if (prop.isAnnotationPresent(N1qlJoin.class)) {
N1qlJoin definition = prop.findAnnotation(N1qlJoin.class);
TypeInformation type = prop.getTypeInformation().getActualType();
Class clazz = type.getType();
N1qlJoinResolver.N1qlJoinResolverParameters parameters = new N1qlJoinResolver.N1qlJoinResolverParameters(
definition, id, persistentEntity.getTypeInformation(), type);
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);
}

View File

@@ -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();
}
}

View File

@@ -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()

View File

@@ -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;
}
}

View File

@@ -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<String> roles;
private Address address;
@N1qlJoin(on = "meta(lks).id=rks.parentId", fetchType = FetchType.IMMEDIATE) List<Address> otherAddresses;
private int credits;
private List<Submission> submissions;
private List<Course> courses;

View File

@@ -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<UserSubmission, String> {
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
List<UserSubmission> findByUsername(String username);
}

View File

@@ -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<String> 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<UserSubmission> 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