diff --git a/README.md b/README.md index 2aa06191..fbdd1e1d 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,9 @@ function (doc, meta) { } ``` +Alternatively, if view creation isn't too costly, you can ask the framework to create it automatically by annotating the +repository with `@ViewIndexed(designDoc = "userInfo", viewName = "all")`. + ## N1QL and Query Derivation With the introduction of `N1QL`, Couchbase can now better support query derivation (the mechanism that allows you to add custom methods that will automatically be implemented as a N1QL query derived from the method's name). @@ -175,6 +178,10 @@ List findPatrickAndJackAmongOthers(); List findUsersWithTheirFirstnameLike(String likePattern); ``` +N1QL needs at least a generic purpose `N1QL primary index` to work with, and can make use of a more entity +type-specific `N1QL secondary index`. You can create both automatically (provided you are confident this +is not to much of a cost) by annotating a repository with `@N1qlPrimaryIndexed` and/or `@N1qlSecondaryIndexed`. + ## Using The Repository Extending `CrudRepository` causes CRUD methods being pulled into the interface so that you can easily save and find diff --git a/src/integration/java/org/springframework/data/couchbase/IntegrationTestApplicationConfig.java b/src/integration/java/org/springframework/data/couchbase/IntegrationTestApplicationConfig.java index 59abf235..37b7090e 100644 --- a/src/integration/java/org/springframework/data/couchbase/IntegrationTestApplicationConfig.java +++ b/src/integration/java/org/springframework/data/couchbase/IntegrationTestApplicationConfig.java @@ -13,7 +13,7 @@ import org.springframework.core.env.Environment; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.WriteResultChecking; -import org.springframework.data.couchbase.core.view.Consistency; +import org.springframework.data.couchbase.core.query.Consistency; @Configuration public class IntegrationTestApplicationConfig extends AbstractCouchbaseConfiguration { diff --git a/src/integration/java/org/springframework/data/couchbase/config/CouchbaseTemplateParserIntegrationTests.java b/src/integration/java/org/springframework/data/couchbase/config/CouchbaseTemplateParserIntegrationTests.java index d9edd0bd..aedda8e8 100644 --- a/src/integration/java/org/springframework/data/couchbase/config/CouchbaseTemplateParserIntegrationTests.java +++ b/src/integration/java/org/springframework/data/couchbase/config/CouchbaseTemplateParserIntegrationTests.java @@ -30,7 +30,7 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; -import org.springframework.data.couchbase.core.view.Consistency; +import org.springframework.data.couchbase.core.query.Consistency; import org.springframework.data.couchbase.repository.User; /** diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java index 6f18bc15..b3f5c853 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java @@ -38,6 +38,7 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.couchbase.IntegrationTestApplicationConfig; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @@ -58,11 +59,14 @@ public class CouchbaseRepositoryViewTests { @Autowired private RepositoryOperationsMapping operationsMapping; + @Autowired + private IndexManager indexManager; + private CustomUserRepository repository; @Before public void setup() throws Exception { - repository = new CouchbaseRepositoryFactory(operationsMapping).getRepository(CustomUserRepository.class); + repository = new CouchbaseRepositoryFactory(operationsMapping, indexManager).getRepository(CustomUserRepository.class); } @Test diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java index a3095818..fe53a396 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java @@ -20,7 +20,7 @@ import java.util.List; import com.couchbase.client.java.document.json.JsonObject; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.View; /** * @author David Harrigan diff --git a/src/integration/java/org/springframework/data/couchbase/repository/DimensionalPartyRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/DimensionalPartyRepository.java index 1204189b..4d12cf13 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/DimensionalPartyRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/DimensionalPartyRepository.java @@ -6,7 +6,7 @@ import java.util.List; import com.couchbase.client.java.document.json.JsonArray; -import org.springframework.data.couchbase.core.view.Dimensional; +import org.springframework.data.couchbase.core.query.Dimensional; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; diff --git a/src/integration/java/org/springframework/data/couchbase/repository/DimensionalQueryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/DimensionalQueryTests.java index 3d4bb9a1..27edc43d 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/DimensionalQueryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/DimensionalQueryTests.java @@ -16,6 +16,7 @@ import org.springframework.data.couchbase.IntegrationTestApplicationConfig; import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -37,11 +38,14 @@ public class DimensionalQueryTests { @Autowired private RepositoryOperationsMapping templateMapping; + @Autowired + private IndexManager indexManager; + private DimensionalPartyRepository repository; @Before public void setup() throws Exception { - RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(templateMapping); + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(templateMapping, indexManager); repository = factory.getRepository(DimensionalPartyRepository.class); } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java index 6bb1caf9..68745789 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.IntegrationTestApplicationConfig; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -49,11 +50,14 @@ public class N1qlCouchbaseRepositoryTests { @Autowired private RepositoryOperationsMapping operationsMapping; + @Autowired + private IndexManager indexManager; + private PartyPagingRepository repository; @Before public void setup() throws Exception { - RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping); + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping, indexManager); repository = factory.getRepository(PartyPagingRepository.class); } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCrudRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCrudRepositoryTests.java index 872255bd..4c121eff 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCrudRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCrudRepositoryTests.java @@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.IntegrationTestApplicationConfig; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.geo.Point; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -50,6 +51,9 @@ public class N1qlCrudRepositoryTests { @Autowired private RepositoryOperationsMapping operationsMapping; + @Autowired + private IndexManager indexManager; + private PartyRepository partyRepository; private ItemRepository itemRepository; @@ -58,8 +62,8 @@ public class N1qlCrudRepositoryTests { @Before public void setup() throws Exception { - partyRepository = new CouchbaseRepositoryFactory(operationsMapping).getRepository(PartyRepository.class); - itemRepository = new CouchbaseRepositoryFactory(operationsMapping).getRepository(ItemRepository.class); + partyRepository = new CouchbaseRepositoryFactory(operationsMapping, indexManager).getRepository(PartyRepository.class); + itemRepository = new CouchbaseRepositoryFactory(operationsMapping, indexManager).getRepository(ItemRepository.class); itemRepository.save(item); partyRepository.save(party); diff --git a/src/integration/java/org/springframework/data/couchbase/repository/PageAndSliceTests.java b/src/integration/java/org/springframework/data/couchbase/repository/PageAndSliceTests.java index 556e7ded..c773c0c6 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/PageAndSliceTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/PageAndSliceTests.java @@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.IntegrationTestApplicationConfig; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; @@ -35,11 +36,14 @@ public class PageAndSliceTests { @Autowired private RepositoryOperationsMapping operationsMapping; + @Autowired + private IndexManager indexManager; + private UserRepository repository; @Before public void setup() throws Exception { - RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping); + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping, indexManager); repository = factory.getRepository(UserRepository.class); } @Test diff --git a/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java index 905fff99..c13b9a01 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/PartyRepository.java @@ -3,7 +3,7 @@ package org.springframework.data.couchbase.repository; import java.util.Date; import java.util.List; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.View; /** * @author Simon Baslé diff --git a/src/integration/java/org/springframework/data/couchbase/repository/QueryDerivationConversionTests.java b/src/integration/java/org/springframework/data/couchbase/repository/QueryDerivationConversionTests.java index 21e3b118..52f3d829 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/QueryDerivationConversionTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/QueryDerivationConversionTests.java @@ -17,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.couchbase.IntegrationTestApplicationConfig; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @@ -36,11 +37,14 @@ public class QueryDerivationConversionTests { @Autowired private RepositoryOperationsMapping operationsMapping; + @Autowired + private IndexManager indexManager; + private PartyRepository repository; @Before public void setup() throws Exception { - RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping); + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping, indexManager); repository = factory.getRepository(PartyRepository.class); } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java index e325697a..f845d78f 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java @@ -34,6 +34,7 @@ import org.springframework.data.couchbase.IntegrationTestApplicationConfig; import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @@ -53,11 +54,14 @@ public class SimpleCouchbaseRepositoryTests { @Autowired private RepositoryOperationsMapping operationsMapping; + @Autowired + private IndexManager indexManager; + private UserRepository repository; @Before public void setup() throws Exception { - RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping); + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping, indexManager); repository = factory.getRepository(UserRepository.class); } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java index aab82796..794130c7 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java @@ -20,8 +20,8 @@ import java.util.List; import com.couchbase.client.java.view.ViewQuery; -import org.springframework.data.couchbase.core.view.Query; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.View; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; diff --git a/src/integration/java/org/springframework/data/couchbase/repository/feature/FeatureDetectionRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/feature/FeatureDetectionRepositoryTests.java index 9e7eb0f5..29786de9 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/feature/FeatureDetectionRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/feature/FeatureDetectionRepositoryTests.java @@ -36,6 +36,7 @@ import org.springframework.data.couchbase.repository.User; import org.springframework.data.couchbase.repository.UserRepository; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -52,6 +53,9 @@ public class FeatureDetectionRepositoryTests { @Autowired private RepositoryOperationsMapping operationsMapping; + @Autowired + private IndexManager indexManager; + @Autowired private ClusterInfo clusterInfo; @@ -62,7 +66,7 @@ public class FeatureDetectionRepositoryTests { @Test public void testN1qlIncompatibleClusterFailsFastForN1qlBasedRepository() throws Exception { - RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping); + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping, indexManager); try { factory.getRepository(UserRepository.class); fail("expected UnsupportedCouchbaseFeatureException"); @@ -73,7 +77,7 @@ public class FeatureDetectionRepositoryTests { @Test public void testN1qlIncompatibleClusterDoesntFailForViewBasedRepository() throws Exception { - RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping); + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(operationsMapping, indexManager); ViewOnlyUserRepository repository = factory.getRepository(ViewOnlyUserRepository.class); assertNotNull(repository); } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/index/AnotherIndexedUserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/index/AnotherIndexedUserRepository.java new file mode 100644 index 00000000..529b8d58 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/index/AnotherIndexedUserRepository.java @@ -0,0 +1,15 @@ +package org.springframework.data.couchbase.repository.index; + +import java.util.List; + +import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed; +import org.springframework.data.couchbase.core.query.ViewIndexed; +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.User; + +@N1qlSecondaryIndexed(indexName = IndexedRepositoryTests.IGNORED_SECONDARY) +public interface AnotherIndexedUserRepository extends CouchbaseRepository { + + @ViewIndexed(designDoc = IndexedRepositoryTests.VIEW_DOC, viewName = IndexedRepositoryTests.IGNORED_VIEW_NAME) + public List findByAge(int age); +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTestListener.java b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTestListener.java new file mode 100644 index 00000000..9d060549 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTestListener.java @@ -0,0 +1,24 @@ +package org.springframework.data.couchbase.repository.index; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.query.Index; +import com.couchbase.client.java.query.N1qlQuery; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +/** + * A test listener that will remove the indexes created in {@link IndexedRepositoryTests} before test case is run. + * + * @author Simon Baslé + */ +public class IndexedRepositoryTestListener extends DependencyInjectionTestExecutionListener { + + @Override + public void beforeTestClass(final TestContext testContext) throws Exception { + Bucket client = (Bucket) testContext.getApplicationContext().getBean("couchbaseBucket"); + client.bucketManager().removeDesignDocument(IndexedRepositoryTests.VIEW_DOC); + client.query(N1qlQuery.simple(Index.dropPrimaryIndex(client.name()))); + client.query(N1qlQuery.simple(Index.dropIndex(client.name(), IndexedRepositoryTests.SECONDARY))); + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTests.java new file mode 100644 index 00000000..016b0130 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedRepositoryTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2013 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 + * + * http://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.index; + +import static org.junit.Assert.*; + +import com.couchbase.client.java.query.N1qlQuery; +import com.couchbase.client.java.query.N1qlQueryResult; +import com.couchbase.client.java.view.DesignDocument; +import com.couchbase.client.java.view.View; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.IntegrationTestApplicationConfig; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; +import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * This tests automatic index creation features in the Couchbase connector. + * Automatic index creation is performed before construction of the repository implementation. + * + * @author Simon Baslé + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = IntegrationTestApplicationConfig.class) +@TestExecutionListeners(IndexedRepositoryTestListener.class) +public class IndexedRepositoryTests { + + public static final String SECONDARY = "autogeneratedIndexIndexedUserN1qlSecondary"; + public static final String VIEW_DOC = "autogeneratedIndex"; + public static final String VIEW_NAME = "IndexedUserView"; + + public static final String IGNORED_VIEW_NAME = "AnotherIndexedUserView"; + public static final String IGNORED_SECONDARY = "AnotherIndexedUserN1qlSecondary"; + + @Autowired + private RepositoryOperationsMapping operationsMapping; + + @Autowired + private IndexManager indexManager; + + private CouchbaseOperations template; + private RepositoryFactorySupport factory; + + private RepositoryFactorySupport ignoringIndexFactory; + private IndexManager ignoringIndexManager = new IndexManager(true, true, true); + + @Before + public void setup() throws Exception { + factory = new CouchbaseRepositoryFactory(operationsMapping, indexManager); + template = operationsMapping.getDefault(); + ignoringIndexFactory = new CouchbaseRepositoryFactory(operationsMapping, ignoringIndexManager); + } + + @Test + public void shouldFindN1qlPrimaryIndex() { + IndexedUserRepository repository = factory.getRepository(IndexedUserRepository.class); + + String bucket = template.getCouchbaseBucket().name(); + N1qlQuery existQuery = N1qlQuery.simple("SELECT COUNT(name) = 1 AS exist FROM system:indexes WHERE keyspace_id = \"" + bucket + + "\" AND is_primary"); + N1qlQueryResult exist = template.queryN1QL(existQuery); + + assertTrue(exist.finalSuccess()); + assertEquals(1, exist.allRows().size()); + assertEquals(true, exist.allRows().get(0).value().getBoolean("exist")); + } + + @Test + public void shouldFindN1qlSecondaryIndex() { + IndexedUserRepository repository = factory.getRepository(IndexedUserRepository.class); + + String bucket = template.getCouchbaseBucket().name(); + N1qlQuery existQuery = N1qlQuery.simple("SELECT COUNT(name) = 1 AS exist FROM system:indexes WHERE keyspace_id = \"" + bucket + + "\" AND name = \"" + SECONDARY + "\""); + N1qlQueryResult exist = template.queryN1QL(existQuery); + + assertTrue(exist.finalSuccess()); + assertEquals(1, exist.allRows().size()); + assertEquals(true, exist.allRows().get(0).value().getBoolean("exist")); + } + + @Test + public void shouldFindViewIndex() { + IndexedUserRepository repository = factory.getRepository(IndexedUserRepository.class); + + DesignDocument designDoc = template.getCouchbaseBucket() + .bucketManager() + .getDesignDocument(VIEW_DOC); + + assertNotNull(designDoc); + for (View view : designDoc.views()) { + if (view.name().equals(VIEW_NAME)) return; + } + fail("View not found"); + } + @Test + public void shouldNotFindN1qlSecondaryIndexWithIgnoringIndexManager() { + AnotherIndexedUserRepository repository = ignoringIndexFactory.getRepository(AnotherIndexedUserRepository.class); + + String bucket = template.getCouchbaseBucket().name(); + N1qlQuery existQuery = N1qlQuery.simple("SELECT COUNT(name) = 1 AS exist FROM system:indexes WHERE keyspace_id = \"" + bucket + + "\" AND name = \"" + IGNORED_SECONDARY + "\""); + N1qlQueryResult exist = template.queryN1QL(existQuery); + + assertTrue(exist.finalSuccess()); + assertEquals(1, exist.allRows().size()); + assertEquals(false, exist.allRows().get(0).value().getBoolean("exist")); + } + + @Test + public void shouldNotFindViewIndexWithIgnoringIndexManager() { + AnotherIndexedUserRepository repository = ignoringIndexFactory.getRepository(AnotherIndexedUserRepository.class); + + DesignDocument designDoc = template.getCouchbaseBucket() + .bucketManager() + .getDesignDocument(VIEW_DOC); + + if (designDoc != null) { + for (View view : designDoc.views()) { + if (view.name().equals(IGNORED_VIEW_NAME)) fail("Found unexpected " + IGNORED_VIEW_NAME); + } + } + } +} \ No newline at end of file diff --git a/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedUserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedUserRepository.java new file mode 100644 index 00000000..bc3645ee --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/index/IndexedUserRepository.java @@ -0,0 +1,13 @@ +package org.springframework.data.couchbase.repository.index; + +import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; +import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed; +import org.springframework.data.couchbase.core.query.ViewIndexed; +import org.springframework.data.couchbase.repository.CouchbaseRepository; +import org.springframework.data.couchbase.repository.User; + +@N1qlPrimaryIndexed +@N1qlSecondaryIndexed(indexName = IndexedRepositoryTests.SECONDARY) +@ViewIndexed(designDoc = IndexedRepositoryTests.VIEW_DOC, viewName = IndexedRepositoryTests.VIEW_NAME) +public interface IndexedUserRepository extends CouchbaseRepository { +} diff --git a/src/integration/resources/configurations/couchbase-repository-bean.xml b/src/integration/resources/configurations/couchbase-repository-bean.xml index 66c49c9c..ab12dd65 100644 --- a/src/integration/resources/configurations/couchbase-repository-bean.xml +++ b/src/integration/resources/configurations/couchbase-repository-bean.xml @@ -12,6 +12,8 @@ + + \ No newline at end of file diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 6a88f731..2039a4b6 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -9,7 +9,10 @@ There are three backing mechanisms in Couchbase for repositories, described in t - <> - <> -Note that you can tune the consistency you want for your queries (see <>). +CRUD operations are still mostly backed by Couchbase views (see <>). +Such views (and, for N1QL, equivalent indexes) can be automatically built, but note this is an **expensive operation** (see <>). + +Note that you can tune the consistency you want for your queries (see <>) and have different repositories backed by different buckets (see <>) [[couchbase.repository.configuration]] == Configuration @@ -228,6 +231,19 @@ Note that the important part in this map function is to only include the documen Also make sure to publish your design documents into production so that they can be picked up by the library! Also, if you are curious why we use `emit(null, null)` in the view: the document id is always sent over to the client implicitly, so we can shave off a view bytes in our view by not duplicating the id. If you use `emit(meta.id, null)` it won't hurt much too. +[[couchbase.repository.indexing]] +== Automatic Index Management +We've seen that the repositories default methods can be backed by two broad kind of features: views and N1QL (in the case of paging and sorting). +In order for the CRUD operations to work, the adequate view must have been created beforehand, and this is usually left for the user to do because view creation (and index creation) is an expensive operation that can take quite some time if the quantity of documents is high. + +In the case where the index creation cost isn't considered too high, it can be triggered automatically instead. You just need to annotate the repositories you want managed with the relevant annotation(s): + + - `@ViewIndexed` will create a view like the "all" view previously seen, to list all entities in the bucket. It can also be used on methods, allowing multiple views to be created with custom map function and reduce function. + - `@N1qlPrimaryIndexed` can be used to ensure a general-purpose PRIMARY INDEX is available in N1QL. + - `@N1qlSecondaryIndexed` will create a more specific N1QL index that does the same kind of filtering on entity type that the view does. It'll allow for efficient listing of all documents that correspond to a Repository's associated domain object. + +Note that sometimes you only want this automatic creation in certain contexts (eg. in Dev but not in Production). To that end, you can customize the `IndexManager` bean in an env-specific `AbstractCouchbaseConfiguration` (eg. via `@Profile`) to ignore certain types of index creation annotations. This is done through the `IndexManager(boolean ignoreView, boolean ignoreN1qlPrimary, boolean ignoreN1qlSecondary)` constructor. You can even declare multiple beans and differentiate them using Spring annotations. + [[couchbase.repository.views.querying]] === View based querying @@ -436,7 +452,6 @@ Provide the implementation and directly use `queryView` and `queryN1QL` methods - for example for views `ViewQuery.stale(Stale.FALSE)` - for example for N1QL `Query.simple("SELECT * FROM default", QueryParams.build().consistency(ScanConsistency.REQUEST_PLUS));` - [[couchbase.repository.multibucket]] == Working with multiple buckets The Java Config version allows you to define multiple `Bucket` and `CouchbaseTemplate`, but in order to have different 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 d0234fd5..1c40b16f 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -44,9 +44,13 @@ import org.springframework.data.couchbase.core.convert.translation.JacksonTransl import org.springframework.data.couchbase.core.convert.translation.TranslationService; import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.Document; -import org.springframework.data.couchbase.core.view.Consistency; +import org.springframework.data.couchbase.core.query.Consistency; +import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; +import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed; +import org.springframework.data.couchbase.core.query.ViewIndexed; import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; @@ -227,6 +231,20 @@ public abstract class AbstractCouchbaseConfiguration { return new CustomConversions(Collections.emptyList()); } + /** + * Register an {@link IndexManager} bean that will be used to process {@link ViewIndexed}, + * {@link N1qlPrimaryIndexed} and {@link N1qlSecondaryIndexed} annotations on repositories + * to automatically create indexes. + *

+ * If this configuration is used in a context where such automatic creations are not desired (eg. + * you want automatic index creation in Dev but not in Prod, and this configuration is the Prod one), + * override the bean and use the {@link IndexManager#IndexManager(boolean, boolean, boolean)} constructor. + */ + @Bean(name = BeanNames.COUCHBASE_INDEX_MANAGER) + public IndexManager indexManager() { + return new IndexManager(); + } + /** * Scans the mapping base package for classes annotated with {@link Document}. * diff --git a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java index c06b8964..277958d3 100644 --- a/src/main/java/org/springframework/data/couchbase/config/BeanNames.java +++ b/src/main/java/org/springframework/data/couchbase/config/BeanNames.java @@ -57,4 +57,9 @@ public class BeanNames { * The bean that stores custom mapping between repositories and their backing couchbaseOperations. */ public static final String REPO_OPERATIONS_MAPPING = "repositoryOperationsMapping"; + + /** + * The bean that drives how some indexes are automatically created. + */ + public static final String COUCHBASE_INDEX_MANAGER = "couchbaseIndexManager"; } diff --git a/src/main/java/org/springframework/data/couchbase/config/CouchbaseIndexManagerParser.java b/src/main/java/org/springframework/data/couchbase/config/CouchbaseIndexManagerParser.java new file mode 100644 index 00000000..94182bc5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/config/CouchbaseIndexManagerParser.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.data.couchbase.repository.support.IndexManager; +import org.springframework.util.StringUtils; + +/** + * The XML parser for a {@link IndexManager} definition. + * + * @author Simon Baslé + */ +public class CouchbaseIndexManagerParser extends AbstractSingleBeanDefinitionParser { + + /** + * Resolve the bean ID and assign a default if not set. + * + * @param element the XML element which contains the attributes. + * @param definition the bean definition to work with. + * @param parserContext encapsulates the parsing state and configuration. + * @return the ID to work with. + */ + @Override + protected String resolveId(final Element element, final AbstractBeanDefinition definition, final ParserContext parserContext) { + String id = super.resolveId(element, definition, parserContext); + return StringUtils.hasText(id) ? id : BeanNames.COUCHBASE_INDEX_MANAGER; + } + + /** + * Defines the bean class that will be constructed. + * + * @param element the XML element which contains the attributes. + * @return the class type to instantiate. + */ + @Override + protected Class getBeanClass(final Element element) { + return IndexManager.class; + } + + /** + * Parse the bean definition and build up the bean. + * + * @param element the XML element which contains the attributes. + * @param bean the builder which builds the bean. + */ + @Override + protected void doParse(final Element element, final BeanDefinitionBuilder bean) { + boolean ignoreViews = Boolean.parseBoolean(element.getAttribute("ignoreViews")); + boolean ignorePrimary = Boolean.parseBoolean(element.getAttribute("ignorePrimary")); + boolean ignoreSecondary = Boolean.parseBoolean(element.getAttribute("ignoreSecondary")); + + bean.addConstructorArgValue(ignoreViews); + bean.addConstructorArgValue(ignorePrimary); + bean.addConstructorArgValue(ignoreSecondary); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/config/CouchbaseNamespaceHandler.java b/src/main/java/org/springframework/data/couchbase/config/CouchbaseNamespaceHandler.java index 30ab8ccb..9c39d02e 100644 --- a/src/main/java/org/springframework/data/couchbase/config/CouchbaseNamespaceHandler.java +++ b/src/main/java/org/springframework/data/couchbase/config/CouchbaseNamespaceHandler.java @@ -44,6 +44,7 @@ public class CouchbaseNamespaceHandler extends NamespaceHandlerSupport { registerBeanDefinitionParser("jmx", new CouchbaseJmxParser()); registerBeanDefinitionParser("template", new CouchbaseTemplateParser()); registerBeanDefinitionParser("translation-service", new CouchbaseTranslationServiceParser()); + registerBeanDefinitionParser("indexManager", new CouchbaseIndexManagerParser()); } } diff --git a/src/main/java/org/springframework/data/couchbase/config/CouchbaseTemplateParser.java b/src/main/java/org/springframework/data/couchbase/config/CouchbaseTemplateParser.java index 865eab8a..db6c0531 100644 --- a/src/main/java/org/springframework/data/couchbase/config/CouchbaseTemplateParser.java +++ b/src/main/java/org/springframework/data/couchbase/config/CouchbaseTemplateParser.java @@ -25,7 +25,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.view.Consistency; +import org.springframework.data.couchbase.core.query.Consistency; import org.springframework.util.StringUtils; /** diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java index 11afe0d9..c815ece8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -35,7 +35,7 @@ import com.couchbase.client.java.view.ViewResult; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.TranslationService; -import org.springframework.data.couchbase.core.view.Consistency; +import org.springframework.data.couchbase.core.query.Consistency; /** * Defines common operations on the Couchbase data source, most commonly implemented by {@link CouchbaseTemplate}. 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 4360a044..bd8305d7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -38,8 +38,6 @@ import com.couchbase.client.java.error.TranscodingException; import com.couchbase.client.java.query.N1qlQuery; import com.couchbase.client.java.query.N1qlQueryResult; import com.couchbase.client.java.query.N1qlQueryRow; -import com.couchbase.client.java.query.N1qlParams; -import com.couchbase.client.java.query.consistency.ScanConsistency; import com.couchbase.client.java.util.features.CouchbaseFeature; import com.couchbase.client.java.view.SpatialViewQuery; import com.couchbase.client.java.view.SpatialViewResult; @@ -70,7 +68,7 @@ import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeDeleteEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent; import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent; -import org.springframework.data.couchbase.core.view.Consistency; +import org.springframework.data.couchbase.core.query.Consistency; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; diff --git a/src/main/java/org/springframework/data/couchbase/core/view/Consistency.java b/src/main/java/org/springframework/data/couchbase/core/query/Consistency.java similarity index 97% rename from src/main/java/org/springframework/data/couchbase/core/view/Consistency.java rename to src/main/java/org/springframework/data/couchbase/core/query/Consistency.java index 3f3a767f..a094ecb1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/view/Consistency.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Consistency.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.data.couchbase.core.view; +package org.springframework.data.couchbase.core.query; import com.couchbase.client.java.query.consistency.ScanConsistency; import com.couchbase.client.java.view.Stale; diff --git a/src/main/java/org/springframework/data/couchbase/core/view/Dimensional.java b/src/main/java/org/springframework/data/couchbase/core/query/Dimensional.java similarity index 96% rename from src/main/java/org/springframework/data/couchbase/core/view/Dimensional.java rename to src/main/java/org/springframework/data/couchbase/core/query/Dimensional.java index f4c4e8ae..c5b39799 100644 --- a/src/main/java/org/springframework/data/couchbase/core/view/Dimensional.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Dimensional.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.data.couchbase.core.view; +package org.springframework.data.couchbase.core.query; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1qlPrimaryIndexed.java b/src/main/java/org/springframework/data/couchbase/core/query/N1qlPrimaryIndexed.java new file mode 100644 index 00000000..2ea4c401 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1qlPrimaryIndexed.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.query; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.couchbase.repository.CouchbaseRepository; + +/** + * This annotation is targeted at {@link CouchbaseRepository Repository} interfaces, indicating that the framework + * should ensure a N1QL Primary Index is present on the repository's associated bucket when the repository is created. + * + * @author Simon Baslé + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface N1qlPrimaryIndexed { +} diff --git a/src/main/java/org/springframework/data/couchbase/core/query/N1qlSecondaryIndexed.java b/src/main/java/org/springframework/data/couchbase/core/query/N1qlSecondaryIndexed.java new file mode 100644 index 00000000..98bb45a4 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/query/N1qlSecondaryIndexed.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.query; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.couchbase.repository.CouchbaseRepository; + +/** + * This annotation is targeted at {@link CouchbaseRepository Repository} interfaces, indicating that + * the framework should ensure a N1QL Secondary Index is present when the repository is instantiated. + *

+ * Said index will relate to the "type" field (the one bearing type information) and restrict on documents + * that match the repository's entity class. + *

+ * Be sure to also use {@link N1qlPrimaryIndexed} to make sure the PRIMARY INDEX is there as well. + * + * @author Simon Baslé + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface N1qlSecondaryIndexed { + + /** + * the name of the index to be created, in the repository's associated bucket namespace. + */ + String indexName(); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/view/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java similarity index 97% rename from src/main/java/org/springframework/data/couchbase/core/view/Query.java rename to src/main/java/org/springframework/data/couchbase/core/query/Query.java index 6e4c6c95..b7bc9793 100644 --- a/src/main/java/org/springframework/data/couchbase/core/view/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.data.couchbase.core.view; +package org.springframework.data.couchbase.core.query; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/src/main/java/org/springframework/data/couchbase/core/view/View.java b/src/main/java/org/springframework/data/couchbase/core/query/View.java similarity index 93% rename from src/main/java/org/springframework/data/couchbase/core/view/View.java rename to src/main/java/org/springframework/data/couchbase/core/query/View.java index cda37872..cabdb1b1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/view/View.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/View.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.data.couchbase.core.view; +package org.springframework.data.couchbase.core.query; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -22,8 +22,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.data.annotation.QueryAnnotation; - /** * Annotation to support the use of Views with Couchbase. * diff --git a/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java b/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java new file mode 100644 index 00000000..e3274a53 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/query/ViewIndexed.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.query; + import org.springframework.data.couchbase.repository.CouchbaseRepository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is targeted at {@link CouchbaseRepository Repository} interfaces, indicating that + * the framework should ensure a View is present when the repository is instantiated. + *

+ * The view must at least be described as a design document name and view name. Default map function + * will filter documents on the type associated to the repository, and default reduce function is "_count". + *

+ * One can specify a custom reduce function as well as a non-default map function. This can be done on methods, + * allowing for multiple views per repository to be created. + * + * @author Simon Baslé + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ViewIndexed { + + /** + * The design document in which to create/look for the view. + */ + String designDoc(); + + /** + * The name of the view. + */ + String viewName(); + + /** + * The map function to use (default is empty, which will trigger a default map function filtering on the + * repository's associated entity type). + */ + String mapFunction() default ""; + + /** + * The reduce function to use (default is built in "_count" reduce function). + */ + String reduceFunction() default "_count"; +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryBean.java b/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryBean.java index 39d9e5e3..4865824a 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryBean.java +++ b/src/main/java/org/springframework/data/couchbase/repository/cdi/CouchbaseRepositoryBean.java @@ -25,6 +25,7 @@ import java.util.Set; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.data.repository.cdi.CdiRepositoryBean; import org.springframework.data.repository.config.CustomRepositoryImplementationDetector; import org.springframework.util.Assert; @@ -63,7 +64,8 @@ public class CouchbaseRepositoryBean extends CdiRepositoryBean { protected T create(CreationalContext creationalContext, Class repositoryType, Object customImplementation) { CouchbaseOperations couchbaseOperations = getDependencyInstance(couchbaseOperationsBean, CouchbaseOperations.class); RepositoryOperationsMapping couchbaseOperationsMapping = new RepositoryOperationsMapping(couchbaseOperations); - return new CouchbaseRepositoryFactory(couchbaseOperationsMapping).getRepository(repositoryType, customImplementation); + IndexManager indexManager = new IndexManager(); + return new CouchbaseRepositoryFactory(couchbaseOperationsMapping, indexManager).getRepository(repositoryType, customImplementation); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoryConfigurationExtension.java index 5965ac9e..195f328e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/couchbase/repository/config/CouchbaseRepositoryConfigurationExtension.java @@ -31,8 +31,12 @@ import org.springframework.data.repository.config.XmlRepositoryConfigurationSour */ public class CouchbaseRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { + /** The reference property to use in xml configuration to specify the template to use with a repository. */ private static final String COUCHBASE_TEMPLATE_REF = "couchbase-template-ref"; + /** The reference property to use in xml configuration to specify the index manager bean to use with a repository. */ + private static final String COUCHBASE_INDEX_MANAGER_REF = "couchbase-index-manager-ref"; + @Override protected String getModulePrefix() { return "couchbase"; @@ -46,11 +50,14 @@ public class CouchbaseRepositoryConfigurationExtension extends RepositoryConfigu public void postProcess(final BeanDefinitionBuilder builder, final XmlRepositoryConfigurationSource config) { Element element = config.getElement(); ParsingUtils.setPropertyReference(builder, element, COUCHBASE_TEMPLATE_REF, "couchbaseOperations"); + ParsingUtils.setPropertyReference(builder, element, COUCHBASE_INDEX_MANAGER_REF, "indexManager"); } @Override public void postProcess(final BeanDefinitionBuilder builder, final AnnotationRepositoryConfigurationSource config) { builder.addDependsOn(BeanNames.REPO_OPERATIONS_MAPPING); + builder.addDependsOn(BeanNames.COUCHBASE_INDEX_MANAGER); builder.addPropertyReference("couchbaseOperationsMapping", BeanNames.REPO_OPERATIONS_MAPPING); + builder.addPropertyReference("indexManager", BeanNames.COUCHBASE_INDEX_MANAGER); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java index 73325d81..a1f95865 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java @@ -21,9 +21,9 @@ import java.lang.reflect.Method; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.view.Dimensional; -import org.springframework.data.couchbase.core.view.Query; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.Dimensional; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.View; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/SpatialViewBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/SpatialViewBasedQuery.java index a3ee4aa4..bae0f607 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/SpatialViewBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/SpatialViewBasedQuery.java @@ -21,7 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.couchbase.core.view.Dimensional; +import org.springframework.data.couchbase.core.query.Dimensional; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.parser.PartTree; diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/SpatialViewQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/SpatialViewQueryCreator.java index b357b4e7..ad0b4d23 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/SpatialViewQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/SpatialViewQueryCreator.java @@ -20,18 +20,13 @@ import java.util.Iterator; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.view.SpatialViewQuery; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.view.Dimensional; +import org.springframework.data.couchbase.core.query.Dimensional; import org.springframework.data.couchbase.repository.query.support.GeoUtils; import org.springframework.data.domain.Sort; -import org.springframework.data.geo.Box; -import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; -import org.springframework.data.geo.Polygon; import org.springframework.data.geo.Shape; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.AbstractQueryCreator; diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java index c0f83c89..33bab19d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java @@ -52,7 +52,7 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { public static final String SPEL_PREFIX = "n1ql"; /** - * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.Query @Query} + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query} * annotation's inline statement. This will be replaced by the correct SELECT x FROM y clause needed * for entity mapping. Eg. "${{@value StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE}} WHERE test = true". * Note this only makes sense once, as the beginning of the statement. @@ -60,21 +60,21 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { public static final String SPEL_SELECT_FROM_CLAUSE = "#" + SPEL_PREFIX + ".selectEntity"; /** - * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.Query @Query} + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query} * annotation's inline statement. This will be replaced by the (escaped) bucket name corresponding to the repository's * entity. Eg. "SELECT * FROM ${{@value StringN1qlBasedQuery#SPEL_BUCKET}} LIMIT 3". */ public static final String SPEL_BUCKET = "#" + SPEL_PREFIX + ".bucket"; /** - * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.Query @Query} + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query} * annotation's inline statement. This will be replaced by the fields allowing to construct the repository's entity * (SELECT clause). Eg. "SELECT ${{@value StringN1qlBasedQuery#SPEL_ENTITY}} FROM test". */ public static final String SPEL_ENTITY = "#" + SPEL_PREFIX + ".fields"; /** - * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.Query @Query} + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query} * annotation's inline statement WHERE clause. This will be replaced by the expression allowing to only select * documents matching the entity's class. Eg. "SELECT * FROM test WHERE test = true AND ${{@value StringN1qlBasedQuery#SPEL_FILTER}}". */ diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java index a3d143dc..8c51a227 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java @@ -30,7 +30,7 @@ import com.couchbase.client.java.document.json.JsonObject; import com.couchbase.client.java.view.ViewQuery; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.View; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.AbstractQueryCreator; diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java index c2cb00cd..c2cf976c 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java @@ -26,8 +26,11 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.UnsupportedCouchbaseFeatureException; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.view.Query; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; +import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.View; +import org.springframework.data.couchbase.core.query.ViewIndexed; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; @@ -51,6 +54,7 @@ import org.springframework.util.Assert; * Factory to create {@link SimpleCouchbaseRepository} instances. * * @author Michael Nitschinger + * @author Simon Baslé */ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { @@ -61,6 +65,11 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { */ private final RepositoryOperationsMapping couchbaseOperationsMapping; + /** + * Holds the reference to the {@link IndexManager}. + */ + private final IndexManager indexManager; + /** * Holds the mapping context. */ @@ -76,10 +85,12 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { * * @param couchbaseOperationsMapping the template for the underlying actions. */ - public CouchbaseRepositoryFactory(final RepositoryOperationsMapping couchbaseOperationsMapping) { + public CouchbaseRepositoryFactory(final RepositoryOperationsMapping couchbaseOperationsMapping, final IndexManager indexManager) { Assert.notNull(couchbaseOperationsMapping); + Assert.notNull(indexManager); this.couchbaseOperationsMapping = couchbaseOperationsMapping; + this.indexManager = indexManager; mappingContext = this.couchbaseOperationsMapping.getDefault().getConverter().getMappingContext(); viewPostProcessor = ViewPostProcessor.INSTANCE; @@ -117,7 +128,14 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { protected Object getTargetRepository(final RepositoryInformation metadata) { CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); boolean isN1qlAvailable = couchbaseOperations.getCouchbaseClusterInfo().checkAvailable(CouchbaseFeature.N1QL); - checkFeatures(metadata, isN1qlAvailable); + + ViewIndexed viewIndexed = AnnotationUtils.findAnnotation(metadata.getRepositoryInterface(), ViewIndexed.class); + N1qlPrimaryIndexed n1qlPrimaryIndexed = AnnotationUtils.findAnnotation(metadata.getRepositoryInterface(), N1qlPrimaryIndexed.class); + N1qlSecondaryIndexed n1qlSecondaryIndexed = AnnotationUtils.findAnnotation(metadata.getRepositoryInterface(), N1qlSecondaryIndexed.class); + + checkFeatures(metadata, isN1qlAvailable, n1qlPrimaryIndexed, n1qlSecondaryIndexed); + + indexManager.buildIndexes(metadata, viewIndexed, n1qlPrimaryIndexed, n1qlSecondaryIndexed, couchbaseOperations); CouchbaseEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); if (isN1qlAvailable) { @@ -132,10 +150,14 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { } } - private void checkFeatures(RepositoryInformation metadata, boolean isN1qlAvailable) { - boolean needsN1ql = metadata.isPagingRepository(); - //paging repo will need N1QL, other repos might also if they don't have only @View methods + private void checkFeatures(RepositoryInformation metadata, boolean isN1qlAvailable, + N1qlPrimaryIndexed n1qlPrimaryIndexed, N1qlSecondaryIndexed n1qlSecondaryIndexed) { + //paging repo will always need N1QL, also check if the repository requires a N1QL index + boolean needsN1ql = metadata.isPagingRepository() || n1qlPrimaryIndexed != null || n1qlSecondaryIndexed != null; + + //for other repos, they might also need N1QL if they don't have only @View methods if (!needsN1ql) { + for (Method method : metadata.getQueryMethods()) { boolean hasN1ql = AnnotationUtils.findAnnotation(method, Query.class) != null; diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactoryBean.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactoryBean.java index c6686392..43fd3155 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactoryBean.java @@ -38,6 +38,11 @@ public class CouchbaseRepositoryFactoryBean, S, ID e */ private RepositoryOperationsMapping operationsMapping; + /** + * Contains the reference to the IndexManager. + */ + private IndexManager indexManager; + /** * Set the template reference. * @@ -52,6 +57,15 @@ public class CouchbaseRepositoryFactoryBean, S, ID e setMappingContext(operationsMapping.getDefault().getConverter().getMappingContext()); } + /** + * Set the IndexManager reference. + * + * @param indexManager the IndexManager to use. + */ + public void setIndexManager(final IndexManager indexManager) { + this.indexManager = indexManager; + } + /** * Returns a factory instance. * @@ -59,25 +73,28 @@ public class CouchbaseRepositoryFactoryBean, S, ID e */ @Override protected RepositoryFactorySupport createRepositoryFactory() { - return getFactoryInstance(operationsMapping); + return getFactoryInstance(operationsMapping, indexManager); } /** * Get the factory instance for the operations. * * @param operationsMapping the reference to the template. + * @param indexManager the reference to the {@link IndexManager}. * @return the factory instance. */ - private RepositoryFactorySupport getFactoryInstance(final RepositoryOperationsMapping operationsMapping) { - return new CouchbaseRepositoryFactory(operationsMapping); + private RepositoryFactorySupport getFactoryInstance(final RepositoryOperationsMapping operationsMapping, + IndexManager indexManager) { + return new CouchbaseRepositoryFactory(operationsMapping, indexManager); } /** - * Make sure that the template is set and not null. + * Make sure that the dependencies are set and not null. */ @Override public void afterPropertiesSet() { super.afterPropertiesSet(); Assert.notNull(operationsMapping, "operationsMapping must not be null!"); + Assert.notNull(indexManager, "indexManager must not be null!"); } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/IndexManager.java b/src/main/java/org/springframework/data/couchbase/repository/support/IndexManager.java new file mode 100644 index 00000000..fe301272 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/IndexManager.java @@ -0,0 +1,268 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.support; + +import static com.couchbase.client.java.query.dsl.Expression.s; +import static com.couchbase.client.java.query.dsl.Expression.x; + +import java.util.Collections; + +import com.couchbase.client.java.bucket.BucketManager; +import com.couchbase.client.java.document.json.JsonObject; +import com.couchbase.client.java.query.AsyncN1qlQueryResult; +import com.couchbase.client.java.query.Index; +import com.couchbase.client.java.query.Statement; +import com.couchbase.client.java.query.dsl.path.index.IndexType; +import com.couchbase.client.java.view.DefaultView; +import com.couchbase.client.java.view.DesignDocument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import rx.Observable; +import rx.exceptions.CompositeException; +import rx.functions.Action1; +import rx.functions.Func1; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException; +import org.springframework.data.couchbase.core.query.N1qlPrimaryIndexed; +import org.springframework.data.couchbase.core.query.N1qlSecondaryIndexed; +import org.springframework.data.couchbase.core.query.ViewIndexed; +import org.springframework.data.repository.core.RepositoryInformation; + +/** + * {@link IndexManager} is responsible for automatic index creation according to the provided metadata and + * various index annotations (if not null). + *

+ * Index creation will be attempted in parallel using the asynchronous APIs, but the overall process is still blocking. + * + * @author Simon Baslé + */ +public class IndexManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(IndexManager.class); + + private static final String TEMPLATE_MAP_FUNCTION = "function (doc, meta) { if(doc.%s == \"%s\") { emit(null, null); } }"; + + private static final JsonObject SUCCESS_MARKER = JsonObject.empty(); + + /** True if this index manager should ignore view creation annotations */ + private boolean ignoreViews; + /** True if this index manager should ignore N1QL PRIMARY creation annotations */ + private boolean ignoreN1qlPrimary; + /** True if this index manager should ignore N1QL SECONDARY creation annotations */ + private boolean ignoreN1qlSecondary; + + + /** + * Construct an IndexManager that can be used as a Bean in a {@link Profile @Profile} annotated configuration + * in order to ignore all or part of automatic index creations in some contexts (like activating it in Dev but + * not in Prod). + * + * @param ignoreViews true to ignore {@link ViewIndexed} annotations. + * @param ignoreN1qlPrimary true to ignore {@link N1qlPrimaryIndexed} annotations. + * @param ignoreN1qlSecondary true to ignore {@link N1qlSecondaryIndexed} annotations. + */ + public IndexManager(boolean ignoreViews, boolean ignoreN1qlPrimary, boolean ignoreN1qlSecondary) { + this.ignoreViews = ignoreViews; + this.ignoreN1qlPrimary = ignoreN1qlPrimary; + this.ignoreN1qlSecondary = ignoreN1qlSecondary; + } + + /** + * Construct a default IndexManager that process all three types of automatic index creations. + */ + public IndexManager() { + this(false, false, false); + } + + /** + * @return true if this IndexManager ignores {@link ViewIndexed} annotations. + */ + public boolean isIgnoreViews() { + return ignoreViews; + } + + /** + * @return true if this IndexManager ignores {@link N1qlPrimaryIndexed} annotations. + */ + public boolean isIgnoreN1qlPrimary() { + return ignoreN1qlPrimary; + } + + /** + * @return true if this IndexManager ignores {@link N1qlSecondaryIndexed} annotations. + */ + public boolean isIgnoreN1qlSecondary() { + return ignoreN1qlSecondary; + } + + /** + * Build the relevant indexes according to the provided annotation and repository metadata, in parallel but blocking + * until all relevant indexes are created. Existing indexes will be detected and skipped. + *

+ * Note that this IndexManager could be configured to ignore some of the annotation types. + * In case of multiple errors, a {@link CompositeException} can be raised with up to 3 causes (one per type of index). + * + * @param metadata the repository's metadata (allowing to find out the type of entity stored, the key under which type + * information is stored, etc...). + * @param viewIndexed the annotation for creation of a View-based index. + * @param n1qlPrimaryIndexed the annotation for creation of a N1QL-based primary index (generic). + * @param n1qlSecondaryIndexed the annotation for creation of a N1QL-based secondary index (specific to the repository + * stored entity). + * @param couchbaseOperations the template to use for index creation. + * @throws CompositeException when several errors (for multiple index types) have been raised. + */ + public void buildIndexes(RepositoryInformation metadata, ViewIndexed viewIndexed, N1qlPrimaryIndexed n1qlPrimaryIndexed, + N1qlSecondaryIndexed n1qlSecondaryIndexed, CouchbaseOperations couchbaseOperations) { + Observable viewAsync = Observable.empty(); + Observable n1qlPrimaryAsync = Observable.empty(); + Observable n1qlSecondaryAsync = Observable.empty(); + + if (viewIndexed != null && !ignoreViews) { + viewAsync = buildAllView(viewIndexed, metadata, couchbaseOperations); + } + + if (n1qlPrimaryIndexed != null && !ignoreN1qlPrimary) { + n1qlPrimaryAsync = buildN1qlPrimary(metadata, couchbaseOperations); + } + + if (n1qlSecondaryIndexed != null && !ignoreN1qlSecondary) { + n1qlSecondaryAsync = buildN1qlSecondary(n1qlSecondaryIndexed, metadata, couchbaseOperations); + } + + //trigger the builds, wait for the last one, throw CompositeException if errors + Observable.mergeDelayError(viewAsync, n1qlPrimaryAsync, n1qlSecondaryAsync) + .toBlocking() + .lastOrDefault(null); + } + + private Observable buildN1qlPrimary(final RepositoryInformation metadata, CouchbaseOperations couchbaseOperations) { + final String bucketName = couchbaseOperations.getCouchbaseBucket().name(); + Statement createPrimary = Index.createPrimaryIndex() + .on(bucketName) + .using(IndexType.GSI); + + LOGGER.debug("Creating N1QL primary index for repository {}", metadata.getRepositoryInterface().getSimpleName()); + return couchbaseOperations.getCouchbaseBucket().async().query(createPrimary) + .flatMap(new Func1>() { + @Override + public Observable call(AsyncN1qlQueryResult asyncN1qlQueryResult) { + return asyncN1qlQueryResult.errors(); + } + }) + .defaultIfEmpty(SUCCESS_MARKER) + .flatMap(new Func1>() { + @Override + public Observable call(JsonObject json) { + if (json == SUCCESS_MARKER) { + LOGGER.debug("N1QL primary index created for repository {}", metadata.getRepositoryInterface().getSimpleName()); + return Observable.empty(); + } else if (json.getString("msg").contains("Index #primary already exist")) { + LOGGER.debug("Primary index already exist, skipping"); + return Observable.empty(); //ignore, the index already exist + } else { + return Observable.error(new CouchbaseQueryExecutionException( + "Cannot create N1QL primary index on " + bucketName + ": " + json)); + } + } + }); + } + + private Observable buildN1qlSecondary(N1qlSecondaryIndexed config, final RepositoryInformation metadata, CouchbaseOperations couchbaseOperations) { + final String bucketName = couchbaseOperations.getCouchbaseBucket().name(); + final String indexName = config.indexName(); + String typeKey = couchbaseOperations.getConverter().getTypeKey(); + final String type = metadata.getDomainType().getName(); + + Statement createIndex = Index.createIndex(indexName) + .on(bucketName, x(typeKey)) + .where(x(typeKey).eq(s(type))) + .using(IndexType.GSI); + + LOGGER.debug("Creating N1QL secondary index for repository {}", metadata.getRepositoryInterface().getSimpleName()); + return couchbaseOperations.getCouchbaseBucket().async().query(createIndex) + .flatMap(new Func1>() { + @Override + public Observable call(AsyncN1qlQueryResult asyncN1qlQueryResult) { + return asyncN1qlQueryResult.errors(); + } + }) + .defaultIfEmpty(SUCCESS_MARKER) + .flatMap(new Func1>() { + @Override + public Observable call(JsonObject json) { + if (json == SUCCESS_MARKER) { + LOGGER.debug("N1QL secondary index created for repository {}", metadata.getRepositoryInterface().getSimpleName()); + return Observable.empty(); + } else if (json.getString("msg").contains("Index " + indexName + " already exist")) { + LOGGER.debug("Secondary index already exist, skipping"); + return Observable.empty(); //ignore, the index already exist + } else { + return Observable.error(new CouchbaseQueryExecutionException( + "Cannot create N1QL secondary index " + bucketName + "." + indexName + " for " + type + ": " + json)); + } + } + }); + } + + private Observable buildAllView(ViewIndexed config, final RepositoryInformation metadata, CouchbaseOperations couchbaseOperations) { + if (config == null) return Observable.empty(); + LOGGER.debug("Creating View index index for repository {}", metadata.getRepositoryInterface().getSimpleName()); + + BucketManager manager = couchbaseOperations.getCouchbaseBucket().bucketManager(); + String viewName = config.viewName(); + String mapFunction = config.mapFunction(); + if (mapFunction.isEmpty()) { + String typeKey = couchbaseOperations.getConverter().getTypeKey(); + String type = metadata.getDomainType().getName(); + + mapFunction = String.format(TEMPLATE_MAP_FUNCTION, typeKey, type); + } + String reduceFunction = config.reduceFunction(); + if ("".equals(reduceFunction)) { + reduceFunction = null; + } + + com.couchbase.client.java.view.View view = DefaultView.create(viewName, mapFunction, reduceFunction); + DesignDocument doc = manager.getDesignDocument(config.designDoc()); + if (doc != null) { + for (com.couchbase.client.java.view.View existingView : doc.views()) { + if (existingView.name().equals(viewName)) { + LOGGER.debug("View index {}/{} already exist, skipping", config.designDoc(), viewName); + return Observable.empty(); //abort, the view already exist + } + } + doc.views().add(view); + } else { + doc = DesignDocument.create(config.designDoc(), Collections.singletonList(view)); + } + return manager.async().upsertDesignDocument(doc) + .map(new Func1() { + @Override + public Void call(DesignDocument designDocument) { + return null; + } + }) + .doOnNext(new Action1() { + @Override + public void call(Void aVoid) { + LOGGER.debug("View index created for repository {}", metadata.getRepositoryInterface().getSimpleName()); + } + }); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java index 2b184ff8..c257c64e 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java @@ -26,12 +26,9 @@ import com.couchbase.client.java.view.ViewResult; import com.couchbase.client.java.view.ViewRow; import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.View; import org.springframework.data.couchbase.repository.CouchbaseRepository; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ViewMetadataProvider.java b/src/main/java/org/springframework/data/couchbase/repository/support/ViewMetadataProvider.java index a47384f1..07a3cc30 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ViewMetadataProvider.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ViewMetadataProvider.java @@ -15,7 +15,7 @@ */ package org.springframework.data.couchbase.repository.support; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.View; /** * Interface to abstract {@link ViewMetadataProvider} that provides {@link View}s to be used for query execution. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ViewPostProcessor.java b/src/main/java/org/springframework/data/couchbase/repository/support/ViewPostProcessor.java index 4dbe0f69..66050bb4 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ViewPostProcessor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ViewPostProcessor.java @@ -22,7 +22,7 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.core.NamedThreadLocal; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.data.couchbase.core.view.View; +import org.springframework.data.couchbase.core.query.View; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; diff --git a/src/main/resources/org/springframework/data/couchbase/config/spring-couchbase-2.0.xsd b/src/main/resources/org/springframework/data/couchbase/config/spring-couchbase-2.0.xsd index 4d6d747a..02036c3c 100644 --- a/src/main/resources/org/springframework/data/couchbase/config/spring-couchbase-2.0.xsd +++ b/src/main/resources/org/springframework/data/couchbase/config/spring-couchbase-2.0.xsd @@ -119,7 +119,7 @@ The reference to a CouchbaseConverter instance. - + @@ -178,6 +178,18 @@ The name of the CouchbaseBucket object that determines what connection to monito + + + + + + + + + + + + @@ -186,6 +198,13 @@ The name of the CouchbaseBucket object that determines what connection to monito + + + + The reference to an IndexManager. Will default to 'couchbaseIndexManager'. + + + @@ -220,4 +239,15 @@ The name of the CouchbaseBucket object that determines what connection to monito + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/UnitTestApplicationConfig.java b/src/test/java/org/springframework/data/couchbase/UnitTestApplicationConfig.java index aeab03e2..7275fc5a 100644 --- a/src/test/java/org/springframework/data/couchbase/UnitTestApplicationConfig.java +++ b/src/test/java/org/springframework/data/couchbase/UnitTestApplicationConfig.java @@ -20,7 +20,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.WriteResultChecking; -import org.springframework.data.couchbase.core.view.Consistency; +import org.springframework.data.couchbase.core.query.Consistency; @Configuration public class UnitTestApplicationConfig extends AbstractCouchbaseConfiguration { diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index c3a47c8f..710d7c16 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -17,8 +17,11 @@ + - log details of the parsing of SpEL in N1QL inline queries + - log additional debug info during automatic index creation + --> + \ No newline at end of file