From dd3a5a680af29e1bbba3903d52277c22c9ce3a2f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Feb 2019 11:12:45 +0100 Subject: [PATCH] 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 parameter that defines inclusive/exclusive boundary comparison (findByAgeBetween(Range age)). Between queries use are issued by default as exclusive ranges so findByAgeBetween(int from, int to) maps to age > from AND age < to. --- .../data/cassandra/core/query/Criteria.java | 16 +++- .../query/CassandraQueryCreator.java | 85 +++++++++++++++++-- .../query/CassandraQueryCreatorUnitTests.java | 21 ++++- src/main/asciidoc/new-features.adoc | 1 + .../reference/cassandra-repositories.adoc | 5 ++ 5 files changed, 117 insertions(+), 11 deletions(-) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Criteria.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Criteria.java index e22af5b6b..7d0497e12 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Criteria.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/query/Criteria.java @@ -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); } /** diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java index 668048854..5411740a2 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java @@ -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 { +class CassandraQueryCreator extends AbstractQueryCreator { private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class); @@ -97,7 +101,7 @@ class CassandraQueryCreator extends AbstractQueryCreator iterator) { + protected Filter create(Part part, Iterator iterator) { PersistentPropertyPath path = getMappingContext() .getPersistentPropertyPath(part.getProperty()); @@ -106,16 +110,25 @@ class CassandraQueryCreator extends AbstractQueryCreator iterator) { + protected Filter and(Part part, Filter base, Iterator iterator) { - getQueryBuilder().and(base); + for (CriteriaDefinition criterion : base) { + getQueryBuilder().and(criterion); + } return create(part, iterator); } @@ -127,7 +140,7 @@ class CassandraQueryCreator extends AbstractQueryCreator + * 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 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 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()) { diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java index 74d026142..cf498f193 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java @@ -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() { diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 1053550ee..50fa2cbbc 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -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 diff --git a/src/main/asciidoc/reference/cassandra-repositories.adoc b/src/main/asciidoc/reference/cassandra-repositories.adoc index e9af5ef1b..f516968a3 100644 --- a/src/main/asciidoc/reference/cassandra-repositories.adoc +++ b/src/main/asciidoc/reference/cassandra-repositories.adoc @@ -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 range)` +| ``age > from AND age < to`` and +lower / upper bounds (`>` / `>=` & `<` / `<=`) according to `Range` + | `In` | `findByAgeIn(Collection ages)` | `age IN (ages...)`