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 6691315d..d9edd0bd 100644 --- a/src/integration/java/org/springframework/data/couchbase/config/CouchbaseTemplateParserIntegrationTests.java +++ b/src/integration/java/org/springframework/data/couchbase/config/CouchbaseTemplateParserIntegrationTests.java @@ -92,7 +92,7 @@ public class CouchbaseTemplateParserIntegrationTests { assertEquals("javaXmlClass", converter.getTypeKey()); - User u = new User("specialSaveUser", "John Locke"); + User u = new User("specialSaveUser", "John Locke", 46); template.save(u); JsonDocument uJsonDoc = template.getCouchbaseBucket().get("specialSaveUser"); template.getCouchbaseBucket().remove("specialSaveUser"); diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java index 510ac4de..7ad4a4ec 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java @@ -49,7 +49,7 @@ public class CouchbaseRepositoryViewListener extends DependencyInjectionTestExec private void populateTestData(final Bucket client, ClusterInfo clusterInfo) { CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client); for (int i = 0; i < 100; i++) { - template.save(new User("testuser-" + i, "uname-" + i), PersistTo.MASTER, ReplicateTo.NONE); + template.save(new User("testuser-" + i, "uname-" + i, i), PersistTo.MASTER, ReplicateTo.NONE); } } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java new file mode 100644 index 00000000..17df466c --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/N1qlCouchbaseRepositoryTests.java @@ -0,0 +1,91 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.couchbase.client.java.Bucket; +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.CouchbaseTemplate; +import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +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 PaginAndSortingRepository features in the Couchbase connector. + * + * @author Simon Baslé + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = IntegrationTestApplicationConfig.class) +@TestExecutionListeners(QueryDerivationConversionListener.class) +public class N1qlCouchbaseRepositoryTests { + + @Autowired + private CouchbaseTemplate template; + + private PartyPagingRepository repository; + + @Before + public void setup() throws Exception { + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(template); + repository = factory.getRepository(PartyPagingRepository.class); + } + + @Test + public void shouldFindAllWithSort() { + Iterable allByAttendanceDesc = repository.findAll(new Sort(Sort.Direction.DESC, "attendees")); + long previousAttendance = Long.MAX_VALUE; + for (Party party : allByAttendanceDesc) { + assertTrue(party.getAttendees() <= previousAttendance); + previousAttendance = party.getAttendees(); + } + } + + @Test + public void shouldSortOnRenamedFieldIfJsonNameIsProvidedInSort() { + Iterable parties = repository.findAll(new Sort(Sort.Direction.DESC, "desc")); + String previousDesc = null; + for (Party party : parties) { + if (previousDesc != null) { + assertTrue(party.getDescription().compareTo(previousDesc) <= 0); + } + previousDesc = party.getDescription(); + } + } + + @Test + public void shouldPageThroughEntities() { + Pageable pageable = new PageRequest(0, 8); + + Page page1 = repository.findAll(pageable); + assertEquals(14, page1.getTotalElements()); //13 generated parties + 1 specifically crafted party + assertEquals(8, page1.getNumberOfElements()); + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/PageAndSliceTests.java b/src/integration/java/org/springframework/data/couchbase/repository/PageAndSliceTests.java new file mode 100644 index 00000000..26edd4f8 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/PageAndSliceTests.java @@ -0,0 +1,89 @@ +package org.springframework.data.couchbase.repository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import com.couchbase.client.java.Bucket; +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.CouchbaseTemplate; +import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +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; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = IntegrationTestApplicationConfig.class) +@TestExecutionListeners(SimpleCouchbaseRepositoryListener.class) +public class PageAndSliceTests { + + @Autowired + private Bucket client; + + @Autowired + private CouchbaseTemplate template; + + private UserRepository repository; + + @Before + public void setup() throws Exception { + RepositoryFactorySupport factory = new CouchbaseRepositoryFactory(template); + repository = factory.getRepository(UserRepository.class); + } + @Test + public void shouldPageThroughResults() { + Page page1 = repository.findByAgeGreaterThan(9, new PageRequest(0, 40)); //there are 90 matching users + Page page2 = repository.findByAgeGreaterThan(9, page1.nextPageable()); + Page page3 = repository.findByAgeGreaterThan(9, page2.nextPageable()); + + assertEquals(90, page1.getTotalElements()); + assertEquals(3, page1.getTotalPages()); + assertTrue(page1.hasContent()); + assertTrue(page1.hasNext()); + assertEquals(40, page1.getNumberOfElements()); + + assertTrue(page2.hasContent()); + assertTrue(page2.hasNext()); + assertEquals(40, page2.getNumberOfElements()); + + assertTrue(page3.hasContent()); + assertFalse(page3.hasNext()); + assertEquals(10, page3.getNumberOfElements()); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowWhenPageableIsNullInPageQuery() { + repository.findByAgeGreaterThan(9, null); + } + + @Test + public void shouldSliceThroughResults() { + int count = 0; + List allMatching = new ArrayList(10); + Slice slice = repository.findByAgeLessThan(9, new PageRequest(0, 3)); //9 matching users (ages 0-8) + allMatching.addAll(slice.getContent()); + while(slice.hasNext()) { + slice = repository.findByAgeLessThan(9, slice.nextPageable()); + allMatching.addAll(slice.getContent()); + assertEquals(3, slice.getContent().size()); + } + assertEquals(9, allMatching.size()); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldThrowWhenPageableIsNullSliceQuery() { + repository.findByAgeLessThan(9, null); + } +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/PartyPagingRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/PartyPagingRepository.java new file mode 100644 index 00000000..26c6458b --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/repository/PartyPagingRepository.java @@ -0,0 +1,4 @@ +package org.springframework.data.couchbase.repository; + +public interface PartyPagingRepository extends CouchbasePagingAndSortingRepository { +} diff --git a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryListener.java b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryListener.java index b0aa2b0e..334f4756 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryListener.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryListener.java @@ -48,7 +48,7 @@ public class SimpleCouchbaseRepositoryListener extends DependencyInjectionTestEx CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client); for (int i = 0; i < 100; i++) { - User u = new User("testuser-" + i, "uname-" + i); + User u = new User("testuser-" + i, "uname-" + i, i); template.save(u, PersistTo.MASTER, ReplicateTo.NONE); } 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 06a187b7..d5edf346 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,8 @@ import org.springframework.data.couchbase.IntegrationTestApplicationConfig; import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @@ -64,7 +66,7 @@ public class SimpleCouchbaseRepositoryTests { @Test public void simpleCrud() { String key = "my_unique_user_key"; - User instance = new User(key, "foobar"); + User instance = new User(key, "foobar", 22); repository.save(instance); User found = repository.findOne(key); @@ -168,5 +170,4 @@ public class SimpleCouchbaseRepositoryTests { } } } - } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/User.java b/src/integration/java/org/springframework/data/couchbase/repository/User.java index adbfef81..39b0d4ce 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/User.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/User.java @@ -29,9 +29,12 @@ public class User { private final String username; - public User(String key, String username) { + private final int age; + + public User(String key, String username, int age) { this.key = key; this.username = username; + this.age = age; } public String getUsername() { @@ -42,6 +45,10 @@ public class User { return key; } + public int getAge() { + return age; + } + @Override public String toString() { return this.key; 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 f8d46c0c..c4a11623 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java @@ -22,6 +22,9 @@ 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.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; /** * @author Michael Nitschinger @@ -43,4 +46,8 @@ public interface UserRepository extends CouchbaseRepository { List findByUsernameContains(String contains); User findByUsernameNear(String place);//this is to check that there's a N1QL derivation AND it fails + + Page findByAgeGreaterThan(int minAge, Pageable pageable); + + Slice findByAgeLessThan(int maxAge, Pageable pageable); } diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 4d84a33c..8fdf367e 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -92,8 +92,6 @@ Now, let's imagine we `@Autowire` the `UserRepository` to a class that makes use Now that's awesome! Just by defining an interface we get full CRUD functionality on top of our managed entity. All methods suffixed with (*) in the table are backed by Views, which is explained later. -If you are coming from other datastore implementations, you might want to implement the `PagingAndSortingRepository` as well. Note that as of now, it is not supported but will be in the future. - While the exposed methods provide you with a great variety of access patterns, very often you need to define custom ones. You can do this by adding method declarations to your interface, which will be automatically resolved to requests in the background, as we'll see in the next two sections. [[couchbase.repository.n1ql]] @@ -165,6 +163,23 @@ Most Spring-Data keywords are supported: You can use both counting queries and <> features with this approach. +With N1QL, another possible interface for the repository is the `PagingAndSortingRepository` one (which extends CRUDRepository). +It adds two methods: +[cols="2", options="header"] +.Exposed methods on the PagingAndSortingRepository +|=== +| Method +| Description + +| Iterable findAll(Sort sort); +| Allows to retrieve all relevant entities while sorting on one of their attributes. + +| Page findAll(Pageable pageable); +| Allows to retrieve your entities in pages. The returned `Page` allows to easily get the next page's `Pageable` as well as the list of items. For the first call, use `new PageRequest(0, pageSize)` as Pageable. +|=== + +TIP: You can also use `Page` and `Slice` as method return types as well with a N1QL backed repository. + The second way of querying, supported also in older versions of Couchbase Server, is the View-backed one that we'll see in the next section. [[couchbase.repository.views]] diff --git a/src/main/java/org/springframework/data/couchbase/repository/CouchbasePagingAndSortingRepository.java b/src/main/java/org/springframework/data/couchbase/repository/CouchbasePagingAndSortingRepository.java new file mode 100644 index 00000000..de2c4ee5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/CouchbasePagingAndSortingRepository.java @@ -0,0 +1,31 @@ +/* + * 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; + +import java.io.Serializable; + +import org.springframework.data.repository.PagingAndSortingRepository; + +/** + * Couchbase specific {@link org.springframework.data.repository.Repository} interface that is + * a {@link PagingAndSortingRepository}. + * + * @author Simon Baslé + */ +public interface CouchbasePagingAndSortingRepository + extends CouchbaseRepository, PagingAndSortingRepository { +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java index d9ebf9e4..ea9cce74 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java +++ b/src/main/java/org/springframework/data/couchbase/repository/CouchbaseRepository.java @@ -16,10 +16,10 @@ package org.springframework.data.couchbase.repository; -import org.springframework.data.repository.CrudRepository; - import java.io.Serializable; +import org.springframework.data.repository.CrudRepository; + /** * Couchbase specific {@link org.springframework.data.repository.Repository} interface. * diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java index e1660e54..4628bba5 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java @@ -21,17 +21,23 @@ import java.util.List; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.query.Query; import com.couchbase.client.java.query.QueryParams; +import com.couchbase.client.java.query.QueryResult; import com.couchbase.client.java.query.Statement; import com.couchbase.client.java.query.consistency.ScanConsistency; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.util.StreamUtils; +import org.springframework.util.Assert; /** * Abstract base for all Couchbase {@link RepositoryQuery}. It is in charge of inspecting the parameters @@ -49,6 +55,8 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { this.couchbaseOperations = couchbaseOperations; } + protected abstract Statement getCount(ParameterAccessor accessor); + protected abstract Statement getStatement(ParameterAccessor accessor); protected abstract JsonArray getPlaceholderValues(ParameterAccessor accessor); @@ -59,10 +67,18 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { Statement statement = getStatement(accessor); JsonArray queryPlaceholderValues = getPlaceholderValues(accessor); + //prepare the final query Query query = buildQuery(statement, queryPlaceholderValues, getCouchbaseOperations().getDefaultConsistency().n1qlConsistency()); - return executeDependingOnType(query, queryMethod, queryMethod.isPageQuery(), queryMethod.isModifyingQuery(), - queryMethod.isSliceQuery()); + + //prepare a count query + //TODO only do that when necessary (isPageQuery or isSliceQuery) + Statement countStatement = getCount(accessor); + Query countQuery = buildQuery(countStatement, queryPlaceholderValues, + getCouchbaseOperations().getDefaultConsistency().n1qlConsistency()); + + return executeDependingOnType(query, countQuery, queryMethod, accessor.getPageable(), + queryMethod.isPageQuery(), queryMethod.isSliceQuery(), queryMethod.isModifyingQuery()); } protected static Query buildQuery(Statement statement, JsonArray queryPlaceholderValues, ScanConsistency scanConsistency) { @@ -74,21 +90,20 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { else { query = Query.simple(statement, queryParams); } - - if (LOG.isDebugEnabled()) { - LOG.debug("Executing N1QL query: " + query.n1ql()); - } - return query; } - protected Object executeDependingOnType(Query query, QueryMethod queryMethod, + protected Object executeDependingOnType(Query query, Query countQuery, QueryMethod queryMethod, Pageable pageable, boolean isPage, boolean isSlice, boolean isModifying) { - if (isPage || isSlice || isModifying) { - throw new UnsupportedOperationException("Slice, page and modifying queries not yet supported"); + if (isModifying) { + throw new UnsupportedOperationException("Modifying queries not yet supported"); } - if (queryMethod.isCollectionQuery()) { + if (isPage) { + return executePaged(query, countQuery, pageable); + } else if (isSlice) { + return executeSliced(query, countQuery, pageable); + } else if (queryMethod.isCollectionQuery()) { return executeCollection(query); } else if (queryMethod.isQueryForEntity()) { return executeEntity(query); @@ -97,20 +112,53 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { } } + private void logIfNecessary(Query query) { + if (LOG.isDebugEnabled()) { + LOG.debug("Executing N1QL query: " + query.n1ql()); + } + } + protected List executeCollection(Query query) { + logIfNecessary(query); List result = couchbaseOperations.findByN1QL(query, queryMethod.getEntityInformation().getJavaType()); return result; } protected Object executeEntity(Query query) { + logIfNecessary(query); List result = executeCollection(query); return result.isEmpty() ? null : result.get(0); } protected Object executeStream(Query query) { + logIfNecessary(query); return StreamUtils.createStreamFromIterator(executeCollection(query).iterator()); } + protected Object executePaged(Query query, Query countQuery, Pageable pageable) { + Assert.notNull(pageable); + long total = 0L; + logIfNecessary(countQuery); + List countResult = couchbaseOperations.findByN1QLProjection(countQuery, CountFragment.class); + if (countResult != null && !countResult.isEmpty()) { + total = countResult.get(0).count; + } + + logIfNecessary(query); + List result = couchbaseOperations.findByN1QL(query, queryMethod.getEntityInformation().getJavaType()); + return new PageImpl(result, pageable, total); + } + + protected Object executeSliced(Query query, Query countQuery, Pageable pageable) { + Assert.notNull(pageable); + logIfNecessary(query); + List result = couchbaseOperations.findByN1QL(query, queryMethod.getEntityInformation().getJavaType()); + int pageSize = pageable.getPageSize(); + boolean hasNext = result.size() > pageSize; + + return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); + } + @Override public CouchbaseQueryMethod getQueryMethod() { return this.queryMethod; diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CountFragment.java b/src/main/java/org/springframework/data/couchbase/repository/query/CountFragment.java new file mode 100644 index 00000000..89f5352c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CountFragment.java @@ -0,0 +1,40 @@ +/* + * 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.query; + +/** + * An utility entity that allows to extract total row count out of a COUNT(*) N1QL query. + *

+ * The query should use the COUNT_ALIAS, eg.: SELECT COUNT(*) AS count FROM default; + *

+ * This ensures that the framework will be able to map the JSON result to this {@link CountFragment} class + * so that it can be used. + */ +public class CountFragment { + + /** + * Use this alias for the COUNT part of a N1QL query so that the framework can extract the count result. + * Eg.: "SELECT A.COUNT(*) AS " + COUNT_ALIAS + " FROM A"; + */ + public static final String COUNT_ALIAS = "count"; + + /** + * The value for a COUNT that used {@link #COUNT_ALIAS} as an alias. + */ + public long count; + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java index 0a0d71c5..885d1808 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java @@ -16,14 +16,11 @@ package org.springframework.data.couchbase.repository.query; -import static com.couchbase.client.java.query.dsl.Expression.i; import static com.couchbase.client.java.query.dsl.Expression.s; import static com.couchbase.client.java.query.dsl.Expression.x; -import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; -import java.util.List; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.query.dsl.Expression; @@ -33,9 +30,10 @@ import com.couchbase.client.java.query.dsl.path.LimitPath; import com.couchbase.client.java.query.dsl.path.OrderByPath; import com.couchbase.client.java.query.dsl.path.WherePath; -import org.springframework.core.convert.converter.Converter; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.repository.query.support.N1qlUtils; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.context.PersistentPropertyPath; import org.springframework.data.repository.query.ParameterAccessor; @@ -95,6 +93,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator cbSortList = new ArrayList(); - for (Sort.Order order : sort) { - if (order.isAscending()) { - cbSortList.add(com.couchbase.client.java.query.dsl.Sort.asc(order.getProperty())); - } else { - cbSortList.add(com.couchbase.client.java.query.dsl.Sort.desc(order.getProperty())); - } - } - com.couchbase.client.java.query.dsl.Sort[] cbSorts = - cbSortList.toArray(new com.couchbase.client.java.query.dsl.Sort[cbSortList.size()]); + com.couchbase.client.java.query.dsl.Sort[] cbSorts = N1qlUtils.createSort(sort, converter); return selectFromWhere.orderBy(cbSorts); } - - return selectFrom.where(criteria); + return selectFromWhere; } protected Expression prepareExpression(Part part, Iterator iterator) { - PersistentPropertyPath path = converter.getMappingContext() - .getPersistentPropertyPath(part.getProperty()); + PersistentPropertyPath path = N1qlUtils.getPathWithAlternativeFieldNames( + this.converter, part.getProperty()); ConvertingIterator parameterValues = new ConvertingIterator(iterator, converter); //get the whole doted path with fieldNames instead of potentially wrong propNames - String fieldNamePath = path.toDotPath(FIELD_NAME_ESCAPED); + String fieldNamePath = N1qlUtils.getDottedPathWithAlternativeFieldNames(path); //deal with ignore case boolean ignoreCase = false; @@ -311,15 +301,4 @@ public class N1qlQueryCreator extends AbstractQueryCreator FIELD_NAME_ESCAPED = new Converter() { - @Override - public String convert(CouchbasePersistentProperty source) { - return "`" + source.getFieldName() + "`"; - } - }; - } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java index 5e8eb404..68bc2dab 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java @@ -30,8 +30,11 @@ import com.couchbase.client.java.query.dsl.path.LimitPath; import com.couchbase.client.java.query.dsl.path.WherePath; import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.repository.query.support.N1qlUtils; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.util.Assert; public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { @@ -48,16 +51,25 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { } @Override - protected Statement getStatement(ParameterAccessor accessor) { + protected Statement getCount(ParameterAccessor accessor) { Expression bucket = i(getCouchbaseOperations().getCouchbaseBucket().name()); - Expression metaId = path(meta(bucket), "id").as(CouchbaseOperations.SELECT_ID); - Expression metaCas = path(meta(bucket), "cas").as(CouchbaseOperations.SELECT_CAS); + WherePath countFrom = select(count("*").as(CountFragment.COUNT_ALIAS)).from(bucket); + + N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, countFrom, + getCouchbaseOperations().getConverter(), getQueryMethod()); + return queryCreator.createQuery(); + } + @Override + protected Statement getStatement(ParameterAccessor accessor) { + String bucketName = getCouchbaseOperations().getCouchbaseBucket().name(); + Expression bucket = N1qlUtils.escapedBucket(bucketName); + FromPath select; if (partTree.isCountProjection()) { select = select(count("*")); } else { - select = select(metaId, metaCas, path(bucket, "*")); + select = N1qlUtils.createSelectClauseForEntity(bucketName); } WherePath selectFrom = select.from(bucket); @@ -65,7 +77,15 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { getCouchbaseOperations().getConverter(), getQueryMethod()); LimitPath selectFromWhereOrderBy = queryCreator.createQuery(); - if (partTree.isLimiting()) { + if (queryMethod.isPageQuery()) { + Pageable pageable = accessor.getPageable(); + Assert.notNull(pageable); + return selectFromWhereOrderBy.limit(pageable.getPageSize()).offset(pageable.getOffset()); + } else if (queryMethod.isSliceQuery() && accessor.getPageable() != null) { + Pageable pageable = accessor.getPageable(); + Assert.notNull(pageable); + return selectFromWhereOrderBy.limit(pageable.getPageSize() + 1).offset(pageable.getOffset()); + } else if (partTree.isLimiting()) { return selectFromWhereOrderBy.limit(partTree.getMaxResults()); } else { return selectFromWhereOrderBy; 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 6b2aa1d4..5dfdc55a 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 @@ -60,19 +60,27 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { public static final String PLACEHOLDER_FILTER_TYPE = "$FILTER_TYPE$"; private final Statement statement; + private final Statement countStatement; public StringN1qlBasedQuery(String statement, CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) { super(queryMethod, couchbaseOperations); String typeField = getCouchbaseOperations().getConverter().getTypeKey(); Class typeValue = getQueryMethod().getEntityInformation().getJavaType(); - this.statement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name(), typeField, typeValue); + this.statement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name(), typeField, typeValue, false); + this.countStatement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name(), typeField, typeValue, true); } - protected static Statement prepare(String statement, String bucketName, String typeField, Class typeValue) { + protected static Statement prepare(String statement, String bucketName, String typeField, Class typeValue, boolean isCount) { String b = "`" + bucketName + "`"; String entity = "META(" + b + ").id AS " + CouchbaseOperations.SELECT_ID + ", META(" + b + ").cas AS " + CouchbaseOperations.SELECT_CAS; - String selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b; + String count = "COUNT(*) AS " + CountFragment.COUNT_ALIAS; + String selectEntity; + if (isCount) { + selectEntity = "SELECT " + count + " FROM " + b; + } else { + selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b; + } String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\""; String result = statement; @@ -108,4 +116,8 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { return this.statement; } + @Override + protected Statement getCount(ParameterAccessor accessor) { + return this.countStatement; + } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java new file mode 100644 index 00000000..12075577 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java @@ -0,0 +1,170 @@ +/* + * 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.query.support; + +import static com.couchbase.client.java.query.Select.select; +import static com.couchbase.client.java.query.dsl.Expression.i; +import static com.couchbase.client.java.query.dsl.Expression.path; +import static com.couchbase.client.java.query.dsl.Expression.s; +import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count; +import static com.couchbase.client.java.query.dsl.functions.MetaFunctions.meta; + +import java.util.ArrayList; +import java.util.List; + +import com.couchbase.client.java.query.Statement; +import com.couchbase.client.java.query.dsl.Expression; +import com.couchbase.client.java.query.dsl.path.FromPath; +import com.couchbase.client.java.query.dsl.path.WherePath; +import com.couchbase.client.java.repository.annotation.Field; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.query.CountFragment; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.data.repository.core.EntityMetadata; + +/** + * Utility class to deal with constructing well formed N1QL queries around Spring Data entities, so that + * the framework can use N1QL to find such entities (eg. restrict the bucket search to a particular type). + */ +public class N1qlUtils { + + /** + * A converter that can be used to extract the {@link CouchbasePersistentProperty#getFieldName() fieldName}, + * eg. when one wants a path from {@link PersistentPropertyPath#toDotPath(Converter)} made of escaped field names. + */ + public static final Converter FIELD_NAME_ESCAPED = + new Converter() { + @Override + public String convert(CouchbasePersistentProperty source) { + return "`" + source.getFieldName() + "`"; + } + }; + + /** + * Escape the given bucketName and produce an {@link Expression}. + */ + public static Expression escapedBucket(String bucketName) { + return i(bucketName); + } + + /** + * Produce a {@link Statement} that corresponds to the SELECT clause for looking for Spring Data entities + * stored in Couchbase. Notably it will select the content of the document AND its id and cas. + * + * @param bucketName the bucket that stores the entity documents (will be escaped). + * @return the needed SELECT clause of the statement. + */ + public static FromPath createSelectClauseForEntity(String bucketName) { + Expression bucket = escapedBucket(bucketName); + Expression metaId = path(meta(bucket), "id").as(CouchbaseOperations.SELECT_ID); + Expression metaCas = path(meta(bucket), "cas").as(CouchbaseOperations.SELECT_CAS); + + return select(metaId, metaCas, path(bucket, "*")); + } + + /** + * Produce a {@link Statement} that corresponds to the SELECT...FROM clauses for looking for Spring Data entities + * stored in Couchbase. Notably it will select the content of the document AND its id and cas FROM the given bucket. + * + * @param bucketName the bucket that stores the entity documents (will be escaped). + * @return the needed SELECT...FROM clauses of the statement. + */ + public static WherePath createSelectFromForEntity(String bucketName) { + return createSelectClauseForEntity(bucketName).from(escapedBucket(bucketName)); + } + + /** + * Produces an {@link Expression} that can serve as a WHERE clause criteria to only select documents in a bucket + * that matches a particular Spring Data entity (as given by the {@link EntityMetadata} parameter). + * + * @param baseWhereCriteria the other criteria of the WHERE clause, or null if none. + * @param converter the {@link CouchbaseConverter} giving the attribute storing the type information can be extracted. + * @param entityInformation the expected type information. + * @return an {@link Expression} to be used as a WHERE clause, that additionally restricts on the given type. + */ + public static Expression createWhereFilterForEntity(Expression baseWhereCriteria, CouchbaseConverter converter, + EntityMetadata entityInformation) { + //add part that filters on type key + String typeKey = converter.getTypeKey(); + String typeValue = entityInformation.getJavaType().getName(); + Expression typeSelector = i(typeKey).eq(s(typeValue)); + if (baseWhereCriteria == null) { + baseWhereCriteria = typeSelector; + } else { + baseWhereCriteria = baseWhereCriteria.and(typeSelector); + } + return baseWhereCriteria; + } + + /** + * Given a common {@link PropertyPath}, returns the corresponding {@link PersistentPropertyPath} + * of {@link CouchbasePersistentProperty} which will allow to discover alternative naming for fields. + */ + public static PersistentPropertyPath getPathWithAlternativeFieldNames( + CouchbaseConverter converter, PropertyPath property) { + PersistentPropertyPath path = converter.getMappingContext() + .getPersistentPropertyPath(property); + return path; + } + + /** + * Given a {@link PersistentPropertyPath} of {@link CouchbasePersistentProperty} + * (see {@link #getPathWithAlternativeFieldNames(CouchbaseConverter, PropertyPath)}), + * obtain a String representation of the path, separated with dots and using alternative field names. + */ + public static String getDottedPathWithAlternativeFieldNames(PersistentPropertyPath path) { + return path.toDotPath(FIELD_NAME_ESCAPED); + } + + /** + * Create a N1QL {@link com.couchbase.client.java.query.dsl.Sort} out of a Spring Data {@link Sort}. Note that the later + * must use alternative field names as declared by the {@link Field} annotation on the entity, if any. + */ + public static com.couchbase.client.java.query.dsl.Sort[] createSort(Sort sort, CouchbaseConverter converter) { + List cbSortList = new ArrayList(); + for (Sort.Order order : sort) { + String orderProperty = order.getProperty(); + //FIXME the order property should be converted to its corresponding fieldName + Expression orderFieldName = i(orderProperty); + if (order.isAscending()) { + cbSortList.add(com.couchbase.client.java.query.dsl.Sort.asc(orderFieldName)); + } else { + cbSortList.add(com.couchbase.client.java.query.dsl.Sort.desc(orderFieldName)); + } + } + return cbSortList.toArray(new com.couchbase.client.java.query.dsl.Sort[cbSortList.size()]); + } + + /** + * Creates a full N1QL query that counts total number of the given entity in the bucket. + * + * @param bucketName the name of the bucket where data is stored (will be escaped). + * @param converter the {@link CouchbaseConverter} giving the attribute storing the type information can be extracted. + * @param entityInformation the counted entity type. + * @return the N1QL query that counts number of documents matching this entity type. + */ + public static Statement createCountQueryForEntity(String bucketName, CouchbaseConverter converter, CouchbaseEntityInformation entityInformation) { + return select(count("*").as(CountFragment.COUNT_ALIAS)).from(escapedBucket(bucketName)).where(createWhereFilterForEntity(null, converter, entityInformation)); + } +} 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 86cb0d15..3bf70679 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 @@ -66,6 +66,11 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { */ private final ViewPostProcessor viewPostProcessor; + /** + * Flag indicating if N1QL is available on the underlying cluster (at the time of the factory's creation). + */ + private final boolean isN1qlAvailable; + /** * Create a new factory. * @@ -79,6 +84,8 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { viewPostProcessor = ViewPostProcessor.INSTANCE; addRepositoryProxyPostProcessor(viewPostProcessor); + + this.isN1qlAvailable = couchbaseOperations.getCouchbaseClusterInfo().checkAvailable(CouchbaseFeature.N1QL); } /** @@ -113,24 +120,35 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { checkFeatures(metadata); CouchbaseEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); - final SimpleCouchbaseRepository simpleCouchbaseRepository = new SimpleCouchbaseRepository(entityInformation, couchbaseOperations); - simpleCouchbaseRepository.setViewMetadataProvider(viewPostProcessor.getViewMetadataProvider()); - return simpleCouchbaseRepository; + if (this.isN1qlAvailable) { + //this implementation also conforms to PagingAndSortingRepository + N1qlCouchbaseRepository n1qlRepository = new N1qlCouchbaseRepository(entityInformation, couchbaseOperations); + n1qlRepository.setViewMetadataProvider(viewPostProcessor.getViewMetadataProvider()); + return n1qlRepository; + } else { + final SimpleCouchbaseRepository simpleCouchbaseRepository = new SimpleCouchbaseRepository(entityInformation, couchbaseOperations); + simpleCouchbaseRepository.setViewMetadataProvider(viewPostProcessor.getViewMetadataProvider()); + return simpleCouchbaseRepository; + } } private void checkFeatures(RepositoryInformation metadata) { - boolean needsN1ql = false; - for (Method method : metadata.getQueryMethods()) { + boolean needsN1ql = metadata.isPagingRepository(); + //paging repo will need N1QL, other repos might also if they don't have only @View methods + if (!needsN1ql) { + for (Method method : metadata.getQueryMethods()) { - boolean hasN1ql = AnnotationUtils.findAnnotation(method, Query.class) != null; - boolean hasView = AnnotationUtils.findAnnotation(method, View.class) != null; + boolean hasN1ql = AnnotationUtils.findAnnotation(method, Query.class) != null; + boolean hasView = AnnotationUtils.findAnnotation(method, View.class) != null; - if (hasN1ql || !hasView) { - needsN1ql = true; - break; + if (hasN1ql || !hasView) { + needsN1ql = true; + break; + } } } - if (needsN1ql && !couchbaseOperations.getCouchbaseClusterInfo().checkAvailable(CouchbaseFeature.N1QL)) { + + if (needsN1ql && !this.isN1qlAvailable) { throw new UnsupportedCouchbaseFeatureException("Repository uses N1QL", CouchbaseFeature.N1QL); } } @@ -144,6 +162,9 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { */ @Override protected Class getRepositoryBaseClass(final RepositoryMetadata repositoryMetadata) { + if (isN1qlAvailable) { + return N1qlCouchbaseRepository.class; + } return SimpleCouchbaseRepository.class; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/N1qlCouchbaseRepository.java b/src/main/java/org/springframework/data/couchbase/repository/support/N1qlCouchbaseRepository.java new file mode 100644 index 00000000..bbb5a55f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/N1qlCouchbaseRepository.java @@ -0,0 +1,103 @@ +/* + * 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 java.io.Serializable; +import java.util.List; + +import com.couchbase.client.java.query.Query; +import com.couchbase.client.java.query.Statement; +import com.couchbase.client.java.query.dsl.Expression; +import com.couchbase.client.java.query.dsl.path.WherePath; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.repository.CouchbasePagingAndSortingRepository; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.query.CountFragment; +import org.springframework.data.couchbase.repository.query.support.N1qlUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.util.Assert; + +/** + * A {@link CouchbasePagingAndSortingRepository} implementation. It uses N1QL for its {@link PagingAndSortingRepository} + * method implementation. + */ +public class N1qlCouchbaseRepository + extends SimpleCouchbaseRepository + implements CouchbasePagingAndSortingRepository { + + /** + * Create a new Repository. + * + * @param metadata the Metadata for the entity. + * @param couchbaseOperations the reference to the template used. + */ + public N1qlCouchbaseRepository(CouchbaseEntityInformation metadata, CouchbaseOperations couchbaseOperations) { + super(metadata, couchbaseOperations); + } + + @Override + public Iterable findAll(Sort sort) { + Assert.notNull(sort); + + //prepare elements of the query + WherePath selectFrom = N1qlUtils.createSelectFromForEntity(getCouchbaseOperations().getCouchbaseBucket().name()); + Expression whereCriteria = N1qlUtils.createWhereFilterForEntity(null, getCouchbaseOperations().getConverter(), + getEntityInformation()); + + //apply the sort + com.couchbase.client.java.query.dsl.Sort[] orderings = N1qlUtils.createSort(sort, getCouchbaseOperations().getConverter()); + Statement st = selectFrom.where(whereCriteria).orderBy(orderings); + + //fire the query + Query query = Query.simple(st); + return getCouchbaseOperations().findByN1QL(query, getEntityInformation().getJavaType()); + } + + @Override + public Page findAll(Pageable pageable) { + Assert.notNull(pageable); + + //prepare the count total query + Statement countStatement = N1qlUtils.createCountQueryForEntity(getCouchbaseOperations().getCouchbaseBucket().name(), + getCouchbaseOperations().getConverter(), getEntityInformation()); + + //TODO how to avoid to do that more than once? + //fire the count query and get total count + List countResult = getCouchbaseOperations().findByN1QLProjection(Query.simple(countStatement), CountFragment.class); + long totalCount = countResult == null || countResult.isEmpty() ? 0 : countResult.get(0).count; + + //prepare elements of the data query + WherePath selectFrom = N1qlUtils.createSelectFromForEntity(getCouchbaseOperations().getCouchbaseBucket().name()); + Expression whereCriteria = N1qlUtils.createWhereFilterForEntity(null, getCouchbaseOperations().getConverter(), + getEntityInformation()); + + //apply the paging + Statement pageStatement = selectFrom.where(whereCriteria).limit(pageable.getPageSize()).offset(pageable.getOffset()); + + //fire the query + Query query = Query.simple(pageStatement); + List pageContent = getCouchbaseOperations().findByN1QL(query, getEntityInformation().getJavaType()); + + //return the list as a Page + return new PageImpl(pageContent, pageable, totalCount); + } +} 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 2e4183be..2b184ff8 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 @@ -29,6 +29,9 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.view.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/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java index 1f7126f8..ab3b5238 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java @@ -16,11 +16,9 @@ import com.couchbase.client.java.query.SimpleQuery; import com.couchbase.client.java.query.Statement; import com.couchbase.client.java.query.consistency.ScanConsistency; import org.junit.Test; -import org.mockito.Matchers; import org.mockito.Mockito; -import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.domain.Pageable; import org.springframework.data.repository.query.QueryMethod; public class AbstractN1qlBasedQueryTest { @@ -79,60 +77,111 @@ public class AbstractN1qlBasedQueryTest { public void shouldChooseCollectionExecutionWhenCollectionType() { CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); Query query = Mockito.mock(Query.class); + Pageable pageable = Mockito.mock(Pageable.class); AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); - when(mock.executeDependingOnType(any(Query.class), any(QueryMethod.class), anyBoolean(), anyBoolean(), anyBoolean())) + when(mock.executeDependingOnType(any(Query.class), any(Query.class), any(QueryMethod.class), any(Pageable.class), + anyBoolean(), anyBoolean(), anyBoolean())) .thenCallRealMethod(); when(queryMethod.isCollectionQuery()).thenReturn(true); - mock.executeDependingOnType(query, queryMethod, false, false, false); + mock.executeDependingOnType(query, query, queryMethod, pageable, false, false, false); verify(mock).executeCollection(any(Query.class)); verify(mock, never()).executeEntity(any(Query.class)); verify(mock, never()).executeStream(any(Query.class)); + verify(mock, never()).executePaged(any(Query.class), any(Query.class), any(Pageable.class)); + verify(mock, never()).executeSliced(any(Query.class), any(Query.class), any(Pageable.class)); } @Test public void shouldChooseEntityExecutionWhenEntityType() { CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); Query query = Mockito.mock(Query.class); + Pageable pageable = Mockito.mock(Pageable.class); AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); - when(mock.executeDependingOnType(any(Query.class), any(QueryMethod.class), anyBoolean(), anyBoolean(), anyBoolean())) + when(mock.executeDependingOnType(any(Query.class), any(Query.class), any(QueryMethod.class), any(Pageable.class), + anyBoolean(), anyBoolean(), anyBoolean())) .thenCallRealMethod(); when(queryMethod.isQueryForEntity()).thenReturn(true); - mock.executeDependingOnType(query, queryMethod, false, false, false); + mock.executeDependingOnType(query, query, queryMethod, pageable, false, false, false); verify(mock, never()).executeCollection(any(Query.class)); verify(mock).executeEntity(any(Query.class)); verify(mock, never()).executeStream(any(Query.class)); + verify(mock, never()).executePaged(any(Query.class), any(Query.class), any(Pageable.class)); + verify(mock, never()).executeSliced(any(Query.class), any(Query.class), any(Pageable.class)); } @Test public void shouldChooseStreamExecutionWhenStreamType() { CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); Query query = Mockito.mock(Query.class); + Pageable pageable = Mockito.mock(Pageable.class); AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); - when(mock.executeDependingOnType(any(Query.class), any(QueryMethod.class), anyBoolean(), anyBoolean(), anyBoolean())) + when(mock.executeDependingOnType(any(Query.class), any(Query.class), any(QueryMethod.class), any(Pageable.class), + anyBoolean(), anyBoolean(), anyBoolean())) .thenCallRealMethod(); when(queryMethod.isStreamQuery()).thenReturn(true); - mock.executeDependingOnType(query, queryMethod, false, false, false); + mock.executeDependingOnType(query, query, queryMethod, pageable, false, false, false); verify(mock, never()).executeCollection(any(Query.class)); verify(mock, never()).executeEntity(any(Query.class)); verify(mock).executeStream(any(Query.class)); + verify(mock, never()).executePaged(any(Query.class), any(Query.class), any(Pageable.class)); + verify(mock, never()).executeSliced(any(Query.class), any(Query.class), any(Pageable.class)); + } + + @Test + public void shouldChoosePagedExecutionWhenPageType() { + CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); + Query query = Mockito.mock(Query.class); + Pageable pageable = Mockito.mock(Pageable.class); + AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); + when(mock.executeDependingOnType(any(Query.class), any(Query.class), any(QueryMethod.class), any(Pageable.class), + anyBoolean(), anyBoolean(), anyBoolean())) + .thenCallRealMethod(); + + mock.executeDependingOnType(query, query, queryMethod, pageable, true, false, false); + verify(mock, never()).executeCollection(any(Query.class)); + verify(mock, never()).executeEntity(any(Query.class)); + verify(mock, never()).executeStream(any(Query.class)); + verify(mock).executePaged(any(Query.class), any(Query.class), any(Pageable.class)); + verify(mock, never()).executeSliced(any(Query.class), any(Query.class), any(Pageable.class)); + } + + @Test + public void shouldChooseSlicedExecutionWhenSliceType() { + CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); + Query query = Mockito.mock(Query.class); + Pageable pageable = Mockito.mock(Pageable.class); + AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); + when(mock.executeDependingOnType(any(Query.class), any(Query.class), any(QueryMethod.class), any(Pageable.class), + anyBoolean(), anyBoolean(), anyBoolean())) + .thenCallRealMethod(); + when(queryMethod.isSliceQuery()).thenReturn(true); + + mock.executeDependingOnType(query, query, queryMethod, pageable, false, true, false); + verify(mock, never()).executeCollection(any(Query.class)); + verify(mock, never()).executeEntity(any(Query.class)); + verify(mock, never()).executeStream(any(Query.class)); + verify(mock, never()).executePaged(any(Query.class), any(Query.class), any(Pageable.class)); + verify(mock).executeSliced(any(Query.class), any(Query.class), any(Pageable.class)); } @Test public void shouldThrowWhenUnsupportedType() throws NoSuchMethodException { CouchbaseQueryMethod queryMethod = Mockito.mock(CouchbaseQueryMethod.class); Query query = Mockito.mock(Query.class); + Pageable pageable = Mockito.mock(Pageable.class); AbstractN1qlBasedQuery mock = mock(AbstractN1qlBasedQuery.class); - when(mock.executeDependingOnType(any(Query.class), any(QueryMethod.class), anyBoolean(), anyBoolean(), anyBoolean())) + when(mock.executeDependingOnType(any(Query.class), any(Query.class), any(QueryMethod.class), any(Pageable.class), + anyBoolean(), anyBoolean(), anyBoolean())) .thenCallRealMethod(); - try { mock.executeDependingOnType(query, queryMethod, true, false, false); } catch (UnsupportedOperationException e) { } - try { mock.executeDependingOnType(query, queryMethod, false, true, false); } catch (UnsupportedOperationException e) { } - try { mock.executeDependingOnType(query, queryMethod, false, false, true); } catch (UnsupportedOperationException e) { } + try { mock.executeDependingOnType(query, query, queryMethod, pageable, false, false, true); fail(); } catch (UnsupportedOperationException e) { } verify(mock, never()).executeCollection(any(Query.class)); verify(mock, never()).executeEntity(any(Query.class)); verify(mock, never()).executeStream(any(Query.class)); + verify(mock, never()).executePaged(any(Query.class), any(Query.class), any(Pageable.class)); + verify(mock, never()).executeSliced(any(Query.class), any(Query.class), any(Pageable.class)); } -} \ No newline at end of file +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java index 4d773a77..1ec8edc4 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java @@ -11,7 +11,7 @@ public class StringN1QlBasedQueryTest { @Test public void testReplaceFullSelectPlaceholderOnce() throws Exception { String statement = PLACEHOLDER_SELECT_FROM + " where " + PLACEHOLDER_SELECT_FROM; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class); + Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class, false); assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B` where " + PLACEHOLDER_SELECT_FROM, parsed.toString()); @@ -20,7 +20,7 @@ public class StringN1QlBasedQueryTest { @Test public void testReplaceAllBucketPlaceholder() throws Exception { String statement = "SELECT * FROM " + PLACEHOLDER_BUCKET + " WHERE " + PLACEHOLDER_BUCKET + ".test = 1"; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class); + Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class, false); assertEquals("SELECT * FROM `B` WHERE `B`.test = 1", parsed.toString()); } @@ -28,7 +28,7 @@ public class StringN1QlBasedQueryTest { @Test public void testReplaceFirstEntityPlaceholder() throws Exception { String statement = "SELECT " + PLACEHOLDER_ENTITY + " FROM b where b.test = 1 and " + PLACEHOLDER_ENTITY; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "_class", String.class); + Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "_class", String.class, false); assertEquals("SELECT META(`A`).id AS _ID, META(`A`).cas AS _CAS FROM b where b.test = 1 and " + PLACEHOLDER_ENTITY, parsed.toString()); @@ -37,9 +37,17 @@ public class StringN1QlBasedQueryTest { @Test public void testReplaceTypePlaceholder() throws Exception { String statement = "SELECT " + PLACEHOLDER_ENTITY + " FROM b WHERE b.test = 1 AND " + PLACEHOLDER_FILTER_TYPE; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "@class", String.class); + Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "@class", String.class, false); assertEquals("SELECT META(`A`).id AS _ID, META(`A`).cas AS _CAS FROM b WHERE b.test = 1 AND `@class` = " + "\"java.lang.String\"", parsed.toString()); } + + @Test + public void testReplaceSelectFromPlaceholderWithCountIfCountTrue() { + String statement = PLACEHOLDER_SELECT_FROM + " WHERE true"; + Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "_class", String.class, true); + + assertEquals("SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `A` WHERE true", parsed.toString()); + } } \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/support/N1qlUtilsTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/support/N1qlUtilsTest.java new file mode 100644 index 00000000..f1f4a165 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/support/N1qlUtilsTest.java @@ -0,0 +1,123 @@ +package org.springframework.data.couchbase.repository.query.support; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.stubbing.Answer; + +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +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.couchbase.repository.Party; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.query.CountFragment; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.data.repository.core.EntityMetadata; + +public class N1qlUtilsTest { + + @Test + public void testEscapedBucket() throws Exception { + String bucketName = "b"; + String expected = "`b`"; + + String real = N1qlUtils.escapedBucket(bucketName).toString(); + + assertEquals(expected, real); + } + + @Test + public void testCreateSelectClauseForEntity() throws Exception { + String expected = "SELECT META(`b`).id AS _ID, META(`b`).cas AS _CAS, `b`.*"; + String real = N1qlUtils.createSelectClauseForEntity("b").toString(); + + assertEquals(expected, real); + } + + @Test + public void testCreateSelectFromForEntity() throws Exception { + String expected = "SELECT META(`b`).id AS _ID, META(`b`).cas AS _CAS, `b`.* FROM `b`"; + String real = N1qlUtils.createSelectFromForEntity("b").toString(); + + assertEquals(expected, real); + } + + @Test + public void testCreateWhereFilterForEntity() throws Exception { + String expected = "`_class` = \"java.lang.String\""; + CouchbaseConverter converter = mock(CouchbaseConverter.class); + when(converter.getTypeKey()).thenReturn("_class"); + EntityMetadata metadata = mock(EntityMetadata.class); + when(metadata.getJavaType()).thenReturn(String.class); + + String real = N1qlUtils.createWhereFilterForEntity(null, converter, metadata).toString(); + + assertEquals(expected, real); + } + + @Test + public void testCreateWhereFilterForEntityTakesTypeKeyIntoAccount() throws Exception { + String expected = "`hohoho` = \"java.lang.String\""; + CouchbaseConverter converter = mock(CouchbaseConverter.class); + when(converter.getTypeKey()).thenReturn("hohoho"); + EntityMetadata metadata = mock(EntityMetadata.class); + when(metadata.getJavaType()).thenReturn(String.class); + + String real = N1qlUtils.createWhereFilterForEntity(null, converter, metadata).toString(); + + assertEquals(expected, real); + } + + @Test + public void testGetPathWithAlternativeFieldNames() throws Exception { + CouchbaseConverter converter = mock(CouchbaseConverter.class); + PropertyPath partyDescPropertyPath = PropertyPath.from("description", Party.class); + MappingContext mockMapping = mock(CouchbaseMappingContext.class); + when(converter.getMappingContext()).thenReturn(mockMapping); + + N1qlUtils.getPathWithAlternativeFieldNames(converter, partyDescPropertyPath); + verify(mockMapping).getPersistentPropertyPath(partyDescPropertyPath); + verifyNoMoreInteractions(mockMapping); + } + + @Test + @Ignore("rather test the dotted path converter") + public void testGetDottedPathWithAlternativeFieldNames() throws Exception { + + } + + @Test + public void testCreateSortUsesPropertiesAsIsWithEscaping() throws Exception { + CouchbaseConverter converter = mock(CouchbaseConverter.class); + com.couchbase.client.java.query.dsl.Sort[] realSort = + N1qlUtils.createSort(new Sort("description", "attendees"), converter); + + assertEquals(2, realSort.length); + assertEquals(com.couchbase.client.java.query.dsl.Sort.asc("`description`").toString(), realSort[0].toString()); + assertEquals(com.couchbase.client.java.query.dsl.Sort.asc("`attendees`").toString(), realSort[1].toString()); + + verifyZeroInteractions(converter); + } + + @Test + public void testCreateCountQueryForEntity() throws Exception { + CouchbaseConverter converter = mock(CouchbaseConverter.class); + when(converter.getTypeKey()).thenReturn("_class", "otherField"); + CouchbaseEntityInformation entityInformation = mock(CouchbaseEntityInformation.class); + when(entityInformation.getJavaType()).thenReturn(String.class); + + String expectedDefault = "SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `b` WHERE `_class` = \"java.lang.String\""; + String expectedTypeKey = "SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `b` WHERE `otherField` = \"java.lang.String\""; + String real = N1qlUtils.createCountQueryForEntity("b", converter, entityInformation).toString(); + String realWithTypeKey = N1qlUtils.createCountQueryForEntity("b", converter, entityInformation).toString(); + + assertEquals(expectedDefault, real); + assertEquals(expectedTypeKey, realWithTypeKey); + } +} \ No newline at end of file