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:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)`
|
||||
|
||||
Reference in New Issue
Block a user