DATACASS-627 - Add support for derived Between queries.

We now support derived queries using the Between keyword. Between query parts map to either two simple parameters (findByAgeBetween(int from, int to)) or a Range<T> parameter that defines inclusive/exclusive boundary comparison (findByAgeBetween(Range<Integer> age)).

Between queries use are issued by default as exclusive ranges so findByAgeBetween(int from, int to) maps to age > from AND age < to.
This commit is contained in:
Mark Paluch
2019-02-22 11:12:45 +01:00
parent 8acc6d2111
commit dd3a5a680a
5 changed files with 117 additions and 11 deletions

View File

@@ -63,7 +63,21 @@ public class Criteria implements CriteriaDefinition {
* @return a new {@link Criteria} for {@code columnName}.
*/
public static Criteria where(String columnName) {
return new Criteria(ColumnName.from(columnName));
return where(ColumnName.from(columnName));
}
/**
* Static factory method to create a {@link Criteria} using the provided {@link ColumnName}.
*
* @param columnName must not be {@literal null}.
* @return a new {@link Criteria} for {@code columnName}.
* @since 2.2
*/
public static Criteria where(ColumnName columnName) {
Assert.notNull(columnName, "ColumnName must not be null");
return new Criteria(columnName);
}
/**

View File

@@ -19,16 +19,20 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.cassandra.core.query.Criteria;
import org.springframework.data.cassandra.core.query.CriteriaDefinition;
import org.springframework.data.cassandra.core.query.Filter;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.cassandra.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
@@ -47,7 +51,7 @@ import com.datastax.driver.core.querybuilder.Clause;
* @author Mark Paluch
* @author John Blum
*/
class CassandraQueryCreator extends AbstractQueryCreator<Query, CriteriaDefinition> {
class CassandraQueryCreator extends AbstractQueryCreator<Query, Filter> {
private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class);
@@ -97,7 +101,7 @@ class CassandraQueryCreator extends AbstractQueryCreator<Query, CriteriaDefiniti
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#create(org.springframework.data.repository.query.parser.Part, java.util.Iterator)
*/
@Override
protected CriteriaDefinition create(Part part, Iterator<Object> iterator) {
protected Filter create(Part part, Iterator<Object> iterator) {
PersistentPropertyPath<CassandraPersistentProperty> path = getMappingContext()
.getPersistentPropertyPath(part.getProperty());
@@ -106,16 +110,25 @@ class CassandraQueryCreator extends AbstractQueryCreator<Query, CriteriaDefiniti
Assert.state(property != null && path.toDotPath() != null, "Leaf property must not be null");
return from(part, property, Criteria.where(path.toDotPath()), (PotentiallyConvertingIterator) iterator);
Object filterOrCriteria = from(part, property, Criteria.where(path.toDotPath()),
(PotentiallyConvertingIterator) iterator);
if (filterOrCriteria instanceof CriteriaDefinition) {
return Filter.from((CriteriaDefinition) filterOrCriteria);
}
return (Filter) filterOrCriteria;
}
/* (non-Javadoc)
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#and(org.springframework.data.repository.query.parser.Part, java.lang.Object, java.util.Iterator)
*/
@Override
protected CriteriaDefinition and(Part part, CriteriaDefinition base, Iterator<Object> iterator) {
protected Filter and(Part part, Filter base, Iterator<Object> iterator) {
getQueryBuilder().and(base);
for (CriteriaDefinition criterion : base) {
getQueryBuilder().and(criterion);
}
return create(part, iterator);
}
@@ -127,7 +140,7 @@ class CassandraQueryCreator extends AbstractQueryCreator<Query, CriteriaDefiniti
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object)
*/
@Override
protected CriteriaDefinition or(CriteriaDefinition base, CriteriaDefinition criteria) {
protected Filter or(Filter base, Filter criteria) {
throw new InvalidDataAccessApiUsageException("Cassandra does not support an OR operator");
}
@@ -135,10 +148,13 @@ class CassandraQueryCreator extends AbstractQueryCreator<Query, CriteriaDefiniti
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#complete(java.lang.Object, org.springframework.data.domain.Sort)
*/
@Override
protected Query complete(CriteriaDefinition criteria, Sort sort) {
protected Query complete(Filter criteria, Sort sort) {
if (criteria != null) {
getQueryBuilder().and(criteria);
for (CriteriaDefinition criterion : criteria) {
getQueryBuilder().and(criterion);
}
}
Query query = getQueryBuilder().create(sort);
@@ -150,7 +166,10 @@ class CassandraQueryCreator extends AbstractQueryCreator<Query, CriteriaDefiniti
return query;
}
private CriteriaDefinition from(Part part, CassandraPersistentProperty property, Criteria where,
/**
* Returns a {@link Filter} or {@link CriteriaDefinition} object representing the criterion for a {@link Part}.
*/
private Object from(Part part, CassandraPersistentProperty property, Criteria where,
PotentiallyConvertingIterator parameters) {
Type type = part.getType();
@@ -166,6 +185,8 @@ class CassandraQueryCreator extends AbstractQueryCreator<Query, CriteriaDefiniti
return where.lt(parameters.nextConverted(property));
case LESS_THAN_EQUAL:
return where.lte(parameters.nextConverted(property));
case BETWEEN:
return computeBetweenPart(where, parameters);
case IN:
return where.in(nextAsArray(property, parameters));
case LIKE:
@@ -186,6 +207,52 @@ class CassandraQueryCreator extends AbstractQueryCreator<Query, CriteriaDefiniti
}
}
/**
* Compute a {@link Type#BETWEEN} {@link Part}.
* <p/>
* In case the first {@literal value} is actually a {@link Range} the lower and upper bounds of the {@link Range} are
* used according to their {@link Range.Bound#isInclusive() inclusion} definition. Otherwise the {@literal value} is
* used for greater than and {@link Iterator#next() parameters.next()} as less than criterions.
*
* @param where must not be {@literal null}.
* @param parameters must not be {@literal null}.
* @return
* @since 2.2
*/
private static Filter computeBetweenPart(Criteria where, Iterator<Object> parameters) {
Object value = parameters.next();
if (!(value instanceof Range)) {
return Filter.from(Criteria.where(where.getColumnName()).gt(value),
Criteria.where(where.getColumnName()).lt(parameters.next()));
}
Range<?> range = (Range<?>) value;
List<CriteriaDefinition> criteria = new ArrayList<>();
Optional<?> min = range.getLowerBound().getValue();
Optional<?> max = range.getUpperBound().getValue();
min.ifPresent(it -> {
if (range.getLowerBound().isInclusive()) {
criteria.add(Criteria.where(where.getColumnName()).gte(it));
} else {
criteria.add(Criteria.where(where.getColumnName()).gt(it));
}
});
max.ifPresent(it -> {
if (range.getUpperBound().isInclusive()) {
criteria.add(Criteria.where(where.getColumnName()).lte(it));
} else {
criteria.add(Criteria.where(where.getColumnName()).lt(it));
}
});
return Filter.from(criteria);
}
private CriteriaDefinition containing(Criteria where, CassandraPersistentProperty property, Object bindableValue) {
if (property.isCollectionLike() || property.isMapLike()) {

View File

@@ -29,6 +29,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.StatementFactory;
@@ -47,6 +48,7 @@ import org.springframework.data.cassandra.core.mapping.UserTypeResolver;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.cassandra.domain.Person;
import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation;
import org.springframework.data.domain.Range;
import org.springframework.data.repository.query.parser.PartTree;
import com.datastax.driver.core.RegularStatement;
@@ -64,7 +66,7 @@ public class CassandraQueryCreatorUnitTests {
@Rule public ExpectedException exception = ExpectedException.none();
@Before
public void setUp() throws SecurityException, NoSuchMethodException {
public void setUp() {
context = new CassandraMappingContext();
context.setUserTypeResolver(mock(UserTypeResolver.class));
@@ -138,6 +140,23 @@ public class CassandraQueryCreatorUnitTests {
assertThat(query).isEqualTo("SELECT * FROM person WHERE firstname<='Walter';");
}
@Test // DATACASS-627
public void createsBetweenQueryCorrectly() {
String query = createQuery("findByFirstnameBetween", Person.class, 1, 2);
assertThat(query).isEqualTo("SELECT * FROM person WHERE firstname>1 AND firstname<2;");
}
@Test // DATACASS-627
public void createsBetweenQueryWithRangeCorrectly() {
String query = createQuery("findByFirstnameBetween", Person.class,
Range.from(Range.Bound.inclusive(1)).to(Range.Bound.exclusive(2)));
assertThat(query).isEqualTo("SELECT * FROM person WHERE firstname>=1 AND firstname<2;");
}
@Test // DATACASS-7
public void createsInQueryCorrectly() {

View File

@@ -6,6 +6,7 @@ This chapter summarizes changes and new features for each release.
[[new-features.2-2-0]]
== What's new in Spring Data for Apache Cassandra 2.2
* Read-only properties annotated with `@ReadOnlyProperty` to exclude non-writable properties from entity-bound `INSERT` and `UPDATE` operations.
* Support for derived `Between` queries.
[[new-features.2-1-0]]
== What's new in Spring Data for Apache Cassandra 2.1

View File

@@ -247,6 +247,11 @@ The following table shows short examples of the keywords that you can use in que
| `findByAgeLessThanEqual(int age)`
| `age <= age`
| `Between`
| `findByAgeBetween(int from, int to)` and `findByAgeBetween(Range<Integer> range)`
| ``age > from AND age < to`` and
lower / upper bounds (`>` / `>=` & `<` / `<=`) according to `Range`
| `In`
| `findByAgeIn(Collection ages)`
| `age IN (ages...)`