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;