DATACOUCH-138 - Add Page/Slice/PagingAndSortingRepo
Add support for Page / Slice in N1QL query derived methods. Add PagingAndSortingRepository support through a N1qlCouchbaseRepository implementation. It will only be used as a base if N1QL is actually available on the cluster. Refactor out some steps of entity related query creation in a N1qlUtils class, so that they can be also done eg. in repository base classes. Add tests for both N1qlUtils and the N1qlCouchbaseRepository. Document that PagingAndSortingRepository is supported provided N1QL is available on the Couchbase cluster.
This commit is contained in:
@@ -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<T, ID extends Serializable>
|
||||
extends CouchbaseRepository<T, ID>, PagingAndSortingRepository<T, ID> {
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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<CountFragment> 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;
|
||||
|
||||
@@ -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.
|
||||
* <p/>
|
||||
* The query should use the COUNT_ALIAS, eg.: SELECT COUNT(*) AS count FROM default;
|
||||
* <p/>
|
||||
* 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;
|
||||
|
||||
}
|
||||
@@ -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<LimitPath, Expression
|
||||
private final WherePath selectFrom;
|
||||
private final CouchbaseConverter converter;
|
||||
private final CouchbaseQueryMethod queryMethod;
|
||||
private final ParameterAccessor accessor;
|
||||
|
||||
public N1qlQueryCreator(PartTree tree, ParameterAccessor parameters, WherePath selectFrom,
|
||||
CouchbaseConverter converter, CouchbaseQueryMethod queryMethod) {
|
||||
@@ -102,6 +101,7 @@ public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression
|
||||
this.selectFrom = selectFrom;
|
||||
this.converter = converter;
|
||||
this.queryMethod = queryMethod;
|
||||
this.accessor = parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,42 +125,32 @@ public class N1qlQueryCreator extends AbstractQueryCreator<LimitPath, Expression
|
||||
|
||||
@Override
|
||||
protected LimitPath complete(Expression criteria, Sort sort) {
|
||||
//add part that filters on type key
|
||||
String typeKey = converter.getTypeKey();
|
||||
String typeValue = queryMethod.getEntityInformation().getJavaType().getName();
|
||||
Expression typeSelector = i(typeKey).eq(s(typeValue));
|
||||
if (criteria == null) {
|
||||
criteria = typeSelector;
|
||||
} else {
|
||||
criteria = criteria.and(typeSelector);
|
||||
}
|
||||
Expression whereCriteria = N1qlUtils.createWhereFilterForEntity(criteria, this.converter, this.queryMethod.getEntityInformation());
|
||||
|
||||
OrderByPath selectFromWhere = selectFrom.where(criteria);
|
||||
OrderByPath selectFromWhere = selectFrom.where(whereCriteria);
|
||||
|
||||
//sort of the Pageable takes precedence over the sort in the query name
|
||||
if ((queryMethod.isPageQuery() || queryMethod.isSliceQuery()) && accessor.getPageable() != null) {
|
||||
Pageable pageable = accessor.getPageable();
|
||||
if (pageable.getSort() != null) {
|
||||
sort = pageable.getSort();
|
||||
}
|
||||
}
|
||||
|
||||
if (sort != null) {
|
||||
List<com.couchbase.client.java.query.dsl.Sort> cbSortList = new ArrayList<com.couchbase.client.java.query.dsl.Sort>();
|
||||
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<Object> iterator) {
|
||||
PersistentPropertyPath<CouchbasePersistentProperty> path = converter.getMappingContext()
|
||||
.getPersistentPropertyPath(part.getProperty());
|
||||
PersistentPropertyPath<CouchbasePersistentProperty> 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<LimitPath, Expression
|
||||
return JsonArray.from(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
Converter<? super CouchbasePersistentProperty,String> FIELD_NAME_ESCAPED = new Converter<CouchbasePersistentProperty, String>() {
|
||||
@Override
|
||||
public String convert(CouchbasePersistentProperty source) {
|
||||
return "`" + source.getFieldName() + "`";
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<? super CouchbasePersistentProperty,String> FIELD_NAME_ESCAPED =
|
||||
new Converter<CouchbasePersistentProperty, String>() {
|
||||
@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<CouchbasePersistentProperty> getPathWithAlternativeFieldNames(
|
||||
CouchbaseConverter converter, PropertyPath property) {
|
||||
PersistentPropertyPath<CouchbasePersistentProperty> 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<CouchbasePersistentProperty> 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<com.couchbase.client.java.query.dsl.Sort> cbSortList = new ArrayList<com.couchbase.client.java.query.dsl.Sort>();
|
||||
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 <T> Statement createCountQueryForEntity(String bucketName, CouchbaseConverter converter, CouchbaseEntityInformation<T, String> entityInformation) {
|
||||
return select(count("*").as(CountFragment.COUNT_ALIAS)).from(escapedBucket(bucketName)).where(createWhereFilterForEntity(null, converter, entityInformation));
|
||||
}
|
||||
}
|
||||
@@ -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<?, Serializable> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T, ID extends Serializable>
|
||||
extends SimpleCouchbaseRepository<T, ID>
|
||||
implements CouchbasePagingAndSortingRepository<T, ID> {
|
||||
|
||||
/**
|
||||
* Create a new Repository.
|
||||
*
|
||||
* @param metadata the Metadata for the entity.
|
||||
* @param couchbaseOperations the reference to the template used.
|
||||
*/
|
||||
public N1qlCouchbaseRepository(CouchbaseEntityInformation<T, String> metadata, CouchbaseOperations couchbaseOperations) {
|
||||
super(metadata, couchbaseOperations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<T> 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<T> 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<CountFragment> 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<T> pageContent = getCouchbaseOperations().findByN1QL(query, getEntityInformation().getJavaType());
|
||||
|
||||
//return the list as a Page
|
||||
return new PageImpl<T>(pageContent, pageable, totalCount);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user