From f950e4648af311dd55e7189e1438155df5a7dc19 Mon Sep 17 00:00:00 2001 From: Michael Nitschinger Date: Tue, 10 Mar 2020 15:04:40 +0100 Subject: [PATCH] DATACOUCH-504 - Reintroduce query index generation This changeset reintroduces automatic n1ql index generation but through new annotations: QueryIndexed and CompositeQueryIndex (as well as CompositeQueryIndexes to supply more than one). They are translated into create index statements and executed at startup if configured in the config. Note that automatic index management is disabled by default but can be overidden with a simple flag on the config override. More features to come later. --- .../AbstractCouchbaseConfiguration.java | 10 + .../couchbase/core/CouchbaseTemplate.java | 42 +++- .../core/index/CompositeQueryIndex.java | 20 ++ .../core/index/CompositeQueryIndexes.java | 16 ++ ...CouchbasePersistentEntityIndexCreator.java | 129 ++++++++++++ ...ouchbasePersistentEntityIndexResolver.java | 188 ++++++++++++++++++ .../couchbase/core/index/IndexDefinition.java | 34 ++++ .../core/index/QueryIndexDirection.java | 20 ++ .../core/index/QueryIndexResolver.java | 72 +++++++ .../couchbase/core/index/QueryIndexed.java | 34 ++++ .../core/mapping/CouchbaseMappingContext.java | 10 + .../data/couchbase/domain/Airline.java | 31 +++ .../couchbase/domain/AirlineRepository.java | 25 +++ ...ositoryAutoQueryIndexIntegrationTests.java | 103 ++++++++++ ...chbaseRepositoryQueryIntegrationTests.java | 4 + 15 files changed, 737 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndex.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndexes.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexCreator.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexResolver.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/index/IndexDefinition.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/index/QueryIndexDirection.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/index/QueryIndexResolver.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/index/QueryIndexed.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/Airline.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java create mode 100644 src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryAutoQueryIndexIntegrationTests.java diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index e38e0a0e..d2efd6c2 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -208,9 +208,19 @@ public abstract class AbstractCouchbaseConfiguration { mappingContext.setInitialEntitySet(getInitialEntitySet()); mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); mappingContext.setFieldNamingStrategy(fieldNamingStrategy()); + mappingContext.setAutoIndexCreation(autoIndexCreation()); + return mappingContext; } + /** + * Configure whether to automatically create indices for domain types by deriving the + * from the entity or not. + */ + protected boolean autoIndexCreation() { + return false; + } + /** * Register custom Converters in a {@link CustomConversions} object if required. These {@link CustomConversions} will * be registered with the {@link #mappingCouchbaseConverter(CouchbaseMappingContext, CouchbaseCustomConversions)} )} 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 fa600aba..3e4a6fcf 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -16,20 +16,33 @@ package org.springframework.data.couchbase.core; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import com.couchbase.client.java.Collection; +import org.springframework.data.couchbase.core.index.CouchbasePersistentEntityIndexCreator; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.lang.Nullable; -public class CouchbaseTemplate implements CouchbaseOperations { +public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContextAware { private final CouchbaseClientFactory clientFactory; private final CouchbaseConverter converter; private final PersistenceExceptionTranslator exceptionTranslator; private final CouchbaseTemplateSupport templateSupport; + private final MappingContext, CouchbasePersistentProperty> mappingContext; private final ReactiveCouchbaseTemplate reactiveCouchbaseTemplate; + private @Nullable CouchbasePersistentEntityIndexCreator indexCreator; + public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) { this.clientFactory = clientFactory; @@ -37,6 +50,14 @@ public class CouchbaseTemplate implements CouchbaseOperations { this.exceptionTranslator = clientFactory.getExceptionTranslator(); this.templateSupport = new CouchbaseTemplateSupport(converter); this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter); + + this.mappingContext = this.converter.getMappingContext(); + if (mappingContext instanceof CouchbaseMappingContext) { + CouchbaseMappingContext cmc = (CouchbaseMappingContext) mappingContext; + if (cmc.isAutoIndexCreation()) { + indexCreator = new CouchbasePersistentEntityIndexCreator(cmc, this); + } + } } @Override @@ -138,4 +159,23 @@ public class CouchbaseTemplate implements CouchbaseOperations { return resolved == null ? ex : resolved; } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + prepareIndexCreator(applicationContext); + } + + private void prepareIndexCreator(ApplicationContext context) { + String[] indexCreators = context.getBeanNamesForType(CouchbasePersistentEntityIndexCreator.class); + + for (String creator : indexCreators) { + CouchbasePersistentEntityIndexCreator creatorBean = context.getBean(creator, CouchbasePersistentEntityIndexCreator.class); + if (creatorBean.isIndexCreatorFor(mappingContext)) { + return; + } + } + + if (context instanceof ConfigurableApplicationContext && indexCreator != null) { + ((ConfigurableApplicationContext) context).addApplicationListener(indexCreator); + } + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndex.java b/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndex.java new file mode 100644 index 00000000..d3e5c0d7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndex.java @@ -0,0 +1,20 @@ +package org.springframework.data.couchbase.core.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE }) +@Documented +@Repeatable(CompositeQueryIndexes.class) +@Retention(RetentionPolicy.RUNTIME) +public @interface CompositeQueryIndex { + + String[] fields(); + + String name() default ""; + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndexes.java b/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndexes.java new file mode 100644 index 00000000..9535f2c7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/index/CompositeQueryIndexes.java @@ -0,0 +1,16 @@ +package org.springframework.data.couchbase.core.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE }) +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface CompositeQueryIndexes { + + CompositeQueryIndex[] value(); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexCreator.java b/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexCreator.java new file mode 100644 index 00000000..63a8e7e9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexCreator.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2020 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.core.index; + +import com.couchbase.client.core.error.IndexExistsException; +import com.couchbase.client.java.Cluster; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.MappingContextEvent; +import org.springframework.data.couchbase.core.index.CouchbasePersistentEntityIndexResolver.IndexDefinitionHolder; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class CouchbasePersistentEntityIndexCreator implements ApplicationListener> { + + private static final Logger LOGGER = LoggerFactory.getLogger(CouchbasePersistentEntityIndexCreator.class); + + private final Map, Boolean> classesSeen = new ConcurrentHashMap<>(); + private final CouchbaseMappingContext mappingContext; + private final QueryIndexResolver indexResolver; + private final CouchbaseOperations couchbaseOperations; + + public CouchbasePersistentEntityIndexCreator(final CouchbaseMappingContext mappingContext, + final CouchbaseOperations operations) { + this.mappingContext = mappingContext; + this.couchbaseOperations = operations; + this.indexResolver = QueryIndexResolver.create(mappingContext, operations); + } + + @Override + public void onApplicationEvent(final MappingContextEvent event) { + if (!event.wasEmittedBy(mappingContext)) { + return; + } + + PersistentEntity entity = event.getPersistentEntity(); + + // Double check type as Spring infrastructure does not consider nested generics + if (entity instanceof CouchbasePersistentEntity) { + checkForIndexes((CouchbasePersistentEntity) entity); + } + } + + private void checkForIndexes(final CouchbasePersistentEntity entity) { + Class type = entity.getType(); + + if (!classesSeen.containsKey(type)) { + this.classesSeen.put(type, Boolean.TRUE); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Analyzing class " + type + " for index information."); + } + + checkForAndCreateIndexes(entity); + } + } + + private void checkForAndCreateIndexes(final CouchbasePersistentEntity entity) { + if (entity.isAnnotationPresent(Document.class)) { + + for (IndexDefinition indexDefinition : indexResolver.resolveIndexFor(entity.getTypeInformation())) { + IndexDefinitionHolder indexToCreate = indexDefinition instanceof IndexDefinitionHolder + ? (IndexDefinitionHolder) indexDefinition + : new IndexDefinitionHolder(indexDefinition.getIndexFields(), indexDefinition.getIndexName(), indexDefinition.getIndexPredicate()); + + createIndex(indexToCreate); + } + } + } + + private void createIndex(final IndexDefinitionHolder indexToCreate) { + Cluster cluster = couchbaseOperations.getCouchbaseClientFactory().getCluster(); + + StringBuilder statement = new StringBuilder("CREATE INDEX ") + .append(indexToCreate.getIndexName()) + .append(" ON `") + .append(couchbaseOperations.getBucketName()) + .append("` (") + .append(String.join(",", indexToCreate.getIndexFields())) + .append(")"); + + if (indexToCreate.getIndexPredicate() != null && !indexToCreate.getIndexPredicate().isEmpty()) { + statement.append(" WHERE ").append(indexToCreate.getIndexPredicate()); + } + + try { + cluster.query(statement.toString()); + } catch (IndexExistsException ex) { + // ignored on purpose, rest is propagated + LOGGER.debug("Index \"" + indexToCreate.getIndexName() + "\" already exists, ignoring."); + } catch (Exception ex) { + throw new DataIntegrityViolationException("Could not auto-create index with statement: " + + statement.toString(), ex); + } + } + + /** + * Returns whether the current index creator was registered for the given {@link MappingContext}. + */ + public boolean isIndexCreatorFor(final MappingContext context) { + return this.mappingContext.equals(context); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexResolver.java b/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexResolver.java new file mode 100644 index 00000000..b1742745 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/index/CouchbasePersistentEntityIndexResolver.java @@ -0,0 +1,188 @@ +/* + * Copyright 2012-2020 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.core.index; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class CouchbasePersistentEntityIndexResolver implements QueryIndexResolver { + + private final MappingContext, CouchbasePersistentProperty> mappingContext; + private final CouchbaseOperations operations; + + public CouchbasePersistentEntityIndexResolver( + final MappingContext, CouchbasePersistentProperty> mappingContext, + CouchbaseOperations operations) { + this.mappingContext = mappingContext; + this.operations = operations; + } + + @Override + public Iterable resolveIndexFor(final TypeInformation typeInformation) { + return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(typeInformation)); + } + + public List resolveIndexForEntity(final CouchbasePersistentEntity root) { + Assert.notNull(root, "CouchbasePersistentEntity must not be null!"); + Document document = root.findAnnotation(Document.class); + Assert.notNull(document, () -> String + .format("Entity %s is not a collection root. Make sure to annotate it with @Document!", root.getName())); + + List indexInformation = new ArrayList<>(); + + root.doWithProperties((PropertyHandler) property -> this + .potentiallyAddIndexForProperty(root, property, indexInformation)); + + return indexInformation; + } + + private void potentiallyAddIndexForProperty(final CouchbasePersistentEntity root, + final CouchbasePersistentProperty persistentProperty, + final List indexes) { + List indexDefinitions = createIndexDefinitionHolderForProperty( + persistentProperty.getFieldName(), root, persistentProperty); + if (!indexDefinitions.isEmpty()) { + indexes.addAll(indexDefinitions); + } + } + + private List createIndexDefinitionHolderForProperty(final String dotPath, + final CouchbasePersistentEntity persistentEntity, + final CouchbasePersistentProperty persistentProperty) { + + List indices = new ArrayList<>(); + + if (persistentProperty.isAnnotationPresent(QueryIndexed.class)) { + indices.add(createFieldQueryIndexDefinition(persistentEntity, persistentProperty)); + } + + if (persistentEntity.isAnnotationPresent(CompositeQueryIndex.class) + || persistentEntity.isAnnotationPresent(CompositeQueryIndexes.class)) { + indices.addAll(createCompositeQueryIndexDefinitions(persistentEntity, persistentProperty)); + } + + return indices; + } + + @Nullable + protected IndexDefinitionHolder createFieldQueryIndexDefinition(final CouchbasePersistentEntity entity, + final CouchbasePersistentProperty property) { + QueryIndexed index = property.findAnnotation(QueryIndexed.class); + if (index == null) { + return null; + } + + MappingCouchbaseEntityInformation entityInfo = new MappingCouchbaseEntityInformation<>(entity); + + List fields = new ArrayList<>(); + String fieldName = index.name().isEmpty() ? property.getFieldName() : index.name(); + fields.add(fieldName + (index.direction() == QueryIndexDirection.DESCENDING ? " DESC" : "")); + + String indexName = "idx_" + StringUtils.uncapitalize(entity.getType().getSimpleName()) + "_" + fieldName; + + return new IndexDefinitionHolder(fields, indexName, getPredicate(entityInfo)); + } + + protected List createCompositeQueryIndexDefinitions(final CouchbasePersistentEntity entity, + final CouchbasePersistentProperty property) { + + List indexAnnotations = new ArrayList<>(); + + if (entity.isAnnotationPresent(CompositeQueryIndex.class)) { + indexAnnotations.add(entity.findAnnotation(CompositeQueryIndex.class)); + } + if (entity.isAnnotationPresent(CompositeQueryIndexes.class)) { + indexAnnotations.addAll(Arrays.asList(entity.findAnnotation(CompositeQueryIndexes.class).value())); + } + + MappingCouchbaseEntityInformation entityInfo = new MappingCouchbaseEntityInformation<>(entity); + + String predicate = getPredicate(entityInfo); + + + return indexAnnotations + .stream() + .map(ann -> { + List fields = Arrays.asList(ann.fields()); + String fieldsIndexName = String + .join("_", fields) + .toLowerCase() + .replace(" ", "") + .replace("asc", "") + .replace("desc", ""); + + String indexName = "idx_" + + StringUtils.uncapitalize(entity.getType().getSimpleName()) + + "_" + + fieldsIndexName; + return new IndexDefinitionHolder(fields, indexName, predicate); + }) + .collect(Collectors.toList()); + } + + private String getPredicate(final MappingCouchbaseEntityInformation entityInfo) { + String typeKey = operations.getConverter().getTypeKey(); + String typeValue = entityInfo.getJavaType().getName(); + return "`" + typeKey + "` = \"" + typeValue + "\""; + } + + + public static class IndexDefinitionHolder implements IndexDefinition { + + private final List fields; + private final String indexName; + private final String indexPredicate; + + public IndexDefinitionHolder(List fields, String indexName, String indexPredicate) { + this.fields = fields; + this.indexName = indexName; + this.indexPredicate = indexPredicate; + } + + @Override + public List getIndexFields() { + return fields; + } + + @Override + public String getIndexName() { + return indexName; + } + + @Override + public String getIndexPredicate() { + return indexPredicate; + } + + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/index/IndexDefinition.java b/src/main/java/org/springframework/data/couchbase/core/index/IndexDefinition.java new file mode 100644 index 00000000..f8bb4412 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/index/IndexDefinition.java @@ -0,0 +1,34 @@ +/* + * Copyright 2011-2020 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.core.index; + +import java.util.List; +import java.util.Map; + +/** + * @author Jon Brisbin + * @author Christoph Strobl + * @author Mark Paluch + */ +public interface IndexDefinition { + + String getIndexName(); + + List getIndexFields(); + + String getIndexPredicate(); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexDirection.java b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexDirection.java new file mode 100644 index 00000000..e6894f12 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexDirection.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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.core.index; + +public enum QueryIndexDirection { + ASCENDING, DESCENDING +} diff --git a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexResolver.java b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexResolver.java new file mode 100644 index 00000000..112e529f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexResolver.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2020 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.core.index; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +/** + * {@link QueryIndexResolver} finds those {@link IndexDefinition}s to be created for a given class. + * + * @author Christoph Strobl + * @author Thomas Darimont + * @author Mark Paluch + * @since 1.5 + */ +public interface QueryIndexResolver { + + /** + * Creates a new {@link QueryIndexResolver} given {@link CouchbaseMappingContext}. + * + * @param mappingContext must not be {@literal null}. + * @return the new {@link QueryIndexResolver}. + * @since 2.2 + */ + static QueryIndexResolver create( + MappingContext, CouchbasePersistentProperty> mappingContext, + CouchbaseOperations operations) { + Assert.notNull(mappingContext, "CouchbaseMappingContext must not be null!"); + return new CouchbasePersistentEntityIndexResolver(mappingContext, operations); + } + + /** + * Find and create {@link IndexDefinition}s for properties of given {@link TypeInformation}. {@link IndexDefinition}s + * are created for properties and types with {@link QueryIndexed}. + * + * @param typeInformation + * @return Empty {@link Iterable} in case no {@link IndexDefinition} could be resolved for type. + */ + Iterable resolveIndexFor(TypeInformation typeInformation); + + /** + * Find and create {@link IndexDefinition}s for properties of given {@link TypeInformation}. {@link IndexDefinition}s + * are created for properties and types with {@link QueryIndexed}. + * + * @param entityType + * @return Empty {@link Iterable} in case no {@link IndexDefinition} could be resolved for type. + * @see 2.2 + */ + default Iterable resolveIndexFor(Class entityType) { + return resolveIndexFor(ClassTypeInformation.from(entityType)); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexed.java b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexed.java new file mode 100644 index 00000000..506c8233 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/index/QueryIndexed.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2020 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.core.index; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark a field to be indexed by the query engine. + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface QueryIndexed { + + QueryIndexDirection direction() default QueryIndexDirection.ASCENDING; + + String name() default ""; + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java index 800cbb50..1becd978 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java @@ -49,6 +49,8 @@ public class CouchbaseMappingContext */ private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY; + private boolean autoIndexCreation = true; + /** * Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied. * Defaults to a strategy using the plain property name. @@ -101,4 +103,12 @@ public class CouchbaseMappingContext context = applicationContext; } + public boolean isAutoIndexCreation() { + return autoIndexCreation; + } + + public void setAutoIndexCreation(boolean autoCreateIndexes) { + this.autoIndexCreation = autoCreateIndexes; + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airline.java b/src/test/java/org/springframework/data/couchbase/domain/Airline.java new file mode 100644 index 00000000..04817ee7 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/Airline.java @@ -0,0 +1,31 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.couchbase.core.index.CompositeQueryIndex; +import org.springframework.data.couchbase.core.index.QueryIndexed; +import org.springframework.data.couchbase.core.mapping.Document; + +@Document +@CompositeQueryIndex(fields = {"id", "name desc"}) +public class Airline { + @Id + String id; + + @QueryIndexed + String name; + + @PersistenceConstructor + public Airline(String id, String name) { + this.id = id; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java new file mode 100644 index 00000000..d4e027c8 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017-2019 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.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AirlineRepository extends PagingAndSortingRepository { + +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryAutoQueryIndexIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryAutoQueryIndexIntegrationTests.java new file mode 100644 index 00000000..cfddd87e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryAutoQueryIndexIntegrationTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017-2019 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.repository; + +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.manager.query.QueryIndex; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringJUnitConfig(CouchbaseRepositoryAutoQueryIndexIntegrationTests.Config.class) +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +public class CouchbaseRepositoryAutoQueryIndexIntegrationTests extends ClusterAwareIntegrationTests { + + @Autowired + private Cluster cluster; + + /** + * Since the index creation happens at startup, the only way to properly check is by querying the + * index list and making sure it is present. + */ + @Test + void createsSingleFieldIndex() { + Optional foundIndex = cluster + .queryIndexes() + .getAllIndexes(bucketName()) + .stream() + .filter(i -> i.name().equals("idx_airline_name")) + .findFirst(); + + assertTrue(foundIndex.isPresent()); + assertTrue(foundIndex.get().condition().get().contains("_class")); + } + + @Test + void createsCompositeIndex() { + Optional foundIndex = cluster + .queryIndexes() + .getAllIndexes(bucketName()) + .stream() + .filter(i -> i.name().equals("idx_airline_id_name")) + .findFirst(); + + assertTrue(foundIndex.isPresent()); + assertTrue(foundIndex.get().condition().get().contains("_class")); + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Override + protected boolean autoIndexCreation() { + return true; + } + } + +} 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 8c2b0fde..5974b050 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -31,12 +31,16 @@ import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportRepository; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.couchbase.client.core.error.IndexExistsException; @SpringJUnitConfig(CouchbaseRepositoryQueryIntegrationTests.Config.class) +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegrationTests { @Autowired CouchbaseClientFactory clientFactory;