Skip to content

Commit 1f6b43d

Browse files
authored
Merge pull request #239 from derjust/bootchicken-master
Added Query and Scan limit features to save money on the new ondemand…
2 parents 9c2d8a7 + 709d9a2 commit 1f6b43d

20 files changed

+114
-28
lines changed

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
<spring-data.version>2.1.2.RELEASE</spring-data.version>
4242

4343
<hibernate-validator.version>6.0.9.Final</hibernate-validator.version>
44-
<aws-java-sdk.version>1.11.443</aws-java-sdk.version>
44+
<aws-java-sdk.version>1.11.515</aws-java-sdk.version>
4545
<junit.version>4.12</junit.version>
4646
<mockito.version>2.23.0</mockito.version>
4747
<cdi.version>1.2</cdi.version>

src/changes/changes.xml

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
<action dev="derjust" issue="235" type="fix" date="2019-03-10">
2828
Upgrading to 5.1.0 results in the error Bean property 'dynamoDBMapperConfig' is not writable or has an invalid setter method
2929
</action>
30+
<action dev="boostschicken" issue="226" type="add" date="2019-03-10">
31+
@Query annotation to support query limiting
32+
</action>
3033
</release>
3134
<release version="5.1.0" date="2019-01-28" description="Spring Boot 2.1 and Spring Data Lovelace-SR1 support">
3235
<action dev="boostschicken" type="add" date="2018-10-28">

src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java

+9
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ public List<FailedBatch> batchDelete(Iterable<?> entities) {
180180
@Override
181181
public <T> PaginatedQueryList<T> query(Class<T> clazz, QueryRequest queryRequest) {
182182
QueryResult queryResult = amazonDynamoDB.query(queryRequest);
183+
184+
// If a limit is set, deactivate lazy loading of (matching) items after the
185+
// limit
186+
// via
187+
// com.amazonaws.services.dynamodbv2.datamodeling.PaginatedQueryList.atEndOfResults()
188+
if (queryRequest.getLimit() != null) {
189+
queryResult.setLastEvaluatedKey(null);
190+
}
191+
183192
return new PaginatedQueryList<T>(dynamoDBMapper, clazz, amazonDynamoDB, queryRequest, queryResult,
184193
dynamoDBMapperConfig.getPaginationLoadingStrategy(), dynamoDBMapperConfig);
185194
}

src/main/java/org/socialsignin/spring/data/dynamodb/repository/Query.java

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
2323

24+
import static org.socialsignin.spring.data.dynamodb.repository.QueryConstants.QUERY_LIMIT_UNLIMITED;
25+
2426
@Retention(RetentionPolicy.RUNTIME)
2527
@Target(ElementType.METHOD)
2628
@Documented
@@ -36,4 +38,13 @@
3638
* Expressions</a>
3739
*/
3840
String fields() default "";
41+
42+
/**
43+
* An integer to limit the number of elements returned.
44+
*
45+
* @see <a href=
46+
* "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html">Projection
47+
* Expressions</a>
48+
*/
49+
int limit() default QUERY_LIMIT_UNLIMITED;
3950
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.socialsignin.spring.data.dynamodb.repository;
17+
18+
public final class QueryConstants {
19+
20+
private QueryConstants() {
21+
}
22+
23+
public static final int QUERY_LIMIT_UNLIMITED = Integer.MIN_VALUE;
24+
25+
}

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCreator.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,24 @@ public abstract class AbstractDynamoDBQueryCreator<T, ID, R>
4646
protected final DynamoDBEntityInformation<T, ID> entityMetadata;
4747
protected final DynamoDBOperations dynamoDBOperations;
4848
protected final Optional<String> projection;
49+
protected final Optional<Integer> limit;
4950

5051
public AbstractDynamoDBQueryCreator(PartTree tree, DynamoDBEntityInformation<T, ID> entityMetadata,
51-
Optional<String> projection, DynamoDBOperations dynamoDBOperations) {
52+
Optional<String> projection, Optional<Integer> limitResults, DynamoDBOperations dynamoDBOperations) {
5253
super(tree);
5354
this.entityMetadata = entityMetadata;
5455
this.projection = projection;
56+
this.limit = limitResults;
5557
this.dynamoDBOperations = dynamoDBOperations;
5658
}
5759

5860
public AbstractDynamoDBQueryCreator(PartTree tree, ParameterAccessor parameterAccessor,
5961
DynamoDBEntityInformation<T, ID> entityMetadata, Optional<String> projection,
60-
DynamoDBOperations dynamoDBOperations) {
62+
Optional<Integer> limitResults, DynamoDBOperations dynamoDBOperations) {
6163
super(tree, parameterAccessor);
6264
this.entityMetadata = entityMetadata;
6365
this.projection = projection;
66+
this.limit = limitResults;
6467
this.dynamoDBOperations = dynamoDBOperations;
6568
}
6669

@@ -89,7 +92,6 @@ protected DynamoDBQueryCriteria<T, ID> addCriteria(DynamoDBQueryCriteria<T, ID>
8992
}
9093

9194
switch (part.getType()) {
92-
9395
case IN :
9496
Object in = iterator.next();
9597
Assert.notNull(in, "Creating conditions on null parameters not supported: please specify a value for '"

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java

+8
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public abstract class AbstractDynamoDBQueryCriteria<T, ID> implements DynamoDBQu
7272
protected String globalSecondaryIndexName;
7373
protected Sort sort = Sort.unsorted();
7474
protected Optional<String> projection = Optional.empty();
75+
protected Optional<Integer> limit = Optional.empty();
7576

7677
public abstract boolean isApplicableForLoad();
7778

@@ -132,6 +133,7 @@ protected QueryRequest buildQueryRequest(String tableName, String theIndexName,
132133
queryRequest.setSelect(Select.ALL_PROJECTED_ATTRIBUTES);
133134
}
134135

136+
limit.ifPresent(queryRequest::setLimit);
135137
applySortIfSpecified(queryRequest, new ArrayList<>(new HashSet<>(allowedSortProperties)));
136138
}
137139
return queryRequest;
@@ -695,4 +697,10 @@ public DynamoDBQueryCriteria<T, ID> withProjection(Optional<String> projection)
695697
this.projection = projection;
696698
return this;
697699
}
700+
701+
@Override
702+
public DynamoDBQueryCriteria<T, ID> withLimit(Optional<Integer> limit) {
703+
this.limit = limit;
704+
return this;
705+
}
698706
}

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBCountQueryCreator.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ public class DynamoDBCountQueryCreator<T, ID> extends AbstractDynamoDBQueryCreat
3232

3333
public DynamoDBCountQueryCreator(PartTree tree, DynamoDBEntityInformation<T, ID> entityMetadata,
3434
DynamoDBOperations dynamoDBOperations, boolean pageQuery) {
35-
super(tree, entityMetadata, Optional.empty(), dynamoDBOperations);
35+
super(tree, entityMetadata, Optional.empty(), Optional.empty(), dynamoDBOperations);
3636
this.pageQuery = pageQuery;
3737
}
3838

3939
public DynamoDBCountQueryCreator(PartTree tree, ParameterAccessor parameterAccessor,
4040
DynamoDBEntityInformation<T, ID> entityMetadata, DynamoDBOperations dynamoDBOperations, boolean pageQuery) {
41-
super(tree, parameterAccessor, entityMetadata, Optional.empty(), dynamoDBOperations);
41+
super(tree, parameterAccessor, entityMetadata, Optional.empty(), Optional.empty(), dynamoDBOperations);
4242
this.pageQuery = pageQuery;
4343

4444
}

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashAndRangeKeyCriteria.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ public DynamoDBQueryExpression<T> buildQueryExpression() {
190190
queryExpression.setSelect(Select.SPECIFIC_ATTRIBUTES);
191191
queryExpression.setProjectionExpression(projection.get());
192192
}
193-
193+
limit.ifPresent(queryExpression::setLimit);
194194
return queryExpression;
195195
}
196196

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashKeyOnlyCriteria.java

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public DynamoDBScanExpression buildScanExpression() {
116116
scanExpression.setSelect(Select.SPECIFIC_ATTRIBUTES);
117117
scanExpression.setProjectionExpression(projection.get());
118118
}
119+
limit.ifPresent(scanExpression::setLimit);
119120
return scanExpression;
120121
}
121122

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCreator.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
public class DynamoDBQueryCreator<T, ID> extends AbstractDynamoDBQueryCreator<T, ID, T> {
3030

3131
public DynamoDBQueryCreator(PartTree tree, ParameterAccessor parameterAccessor,
32-
DynamoDBEntityInformation<T, ID> entityMetadata, Optional<String> projection,
32+
DynamoDBEntityInformation<T, ID> entityMetadata, Optional<String> projection, Optional<Integer> limit,
3333
DynamoDBOperations dynamoDBOperations) {
34-
super(tree, parameterAccessor, entityMetadata, projection, dynamoDBOperations);
34+
super(tree, parameterAccessor, entityMetadata, projection, limit, dynamoDBOperations);
3535
}
3636

3737
@Override
@@ -41,7 +41,7 @@ protected Query<T> complete(@Nullable DynamoDBQueryCriteria<T, ID> criteria, Sor
4141
} else {
4242
criteria.withSort(sort);
4343
criteria.withProjection(projection);
44-
44+
criteria.withLimit(limit);
4545
return criteria.buildQuery(dynamoDBOperations);
4646
}
4747
}

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCriteria.java

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ DynamoDBQueryCriteria<T, ID> withSingleValueCriteria(String propertyName, Compar
4343

4444
DynamoDBQueryCriteria<T, ID> withProjection(Optional<String> projection);
4545

46+
DynamoDBQueryCriteria<T, ID> withLimit(Optional<Integer> limit);
47+
4648
Query<T> buildQuery(DynamoDBOperations dynamoDBOperations);
4749

4850
Query<Long> buildCountQuery(DynamoDBOperations dynamoDBOperations, boolean pageQuery);

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryMethod.java

+13
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.lang.reflect.Method;
2929
import java.util.Optional;
3030

31+
import static org.socialsignin.spring.data.dynamodb.repository.QueryConstants.QUERY_LIMIT_UNLIMITED;
32+
3133
/**
3234
* @author Michael Lavelle
3335
* @author Sebastian Just
@@ -38,6 +40,7 @@ public class DynamoDBQueryMethod<T, ID> extends QueryMethod {
3840
private final boolean scanEnabledForRepository;
3941
private final boolean scanCountEnabledForRepository;
4042
private final Optional<String> projectionExpression;
43+
private final Optional<Integer> limitResults;
4144

4245
public DynamoDBQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) {
4346
super(method, metadata, factory);
@@ -54,8 +57,15 @@ public DynamoDBQueryMethod(Method method, RepositoryMetadata metadata, Projectio
5457
} else {
5558
this.projectionExpression = Optional.empty();
5659
}
60+
int limit = query.limit();
61+
if (limit != QUERY_LIMIT_UNLIMITED) {
62+
this.limitResults = Optional.of(query.limit());
63+
} else {
64+
this.limitResults = Optional.empty();
65+
}
5766
} else {
5867
this.projectionExpression = Optional.empty();
68+
this.limitResults = Optional.empty();
5969
}
6070
}
6171

@@ -98,4 +108,7 @@ public Optional<String> getProjectionExpression() {
98108
return this.projectionExpression;
99109
}
100110

111+
public Optional<Integer> getLimitResults() {
112+
return this.limitResults;
113+
}
101114
}

src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQuery.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public PartTreeDynamoDBQuery(DynamoDBOperations dynamoDBOperations, DynamoDBQuer
3939

4040
protected DynamoDBQueryCreator<T, ID> createQueryCreator(ParametersParameterAccessor accessor) {
4141
return new DynamoDBQueryCreator<>(tree, accessor, getQueryMethod().getEntityInformation(),
42-
getQueryMethod().getProjectionExpression(), dynamoDBOperations);
42+
getQueryMethod().getProjectionExpression(), getQueryMethod().getLimitResults(), dynamoDBOperations);
4343
}
4444

4545
protected DynamoDBCountQueryCreator<T, ID> createCountQueryCreator(ParametersParameterAccessor accessor,

src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBEntityInformation.java

+2
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,6 @@ default Object getRangeKey(ID id) {
5050
}
5151

5252
Optional<String> getProjection();
53+
54+
Optional<Integer> getLimit();
5355
}

src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashAndRangeKeyEntityInformationImpl.java

+6
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class DynamoDBIdIsHashAndRangeKeyEntityInformationImpl<T, ID> extends Ref
4141
private DynamoDBHashAndRangeKeyExtractingEntityMetadata<T, ID> metadata;
4242
private HashAndRangeKeyExtractor<ID, ?> hashAndRangeKeyExtractor;
4343
private Optional<String> projection = Optional.empty();
44+
private Optional<Integer> limit = Optional.empty();
4445

4546
public DynamoDBIdIsHashAndRangeKeyEntityInformationImpl(Class<T> domainClass,
4647
DynamoDBHashAndRangeKeyExtractingEntityMetadata<T, ID> metadata) {
@@ -54,6 +55,11 @@ public Optional<String> getProjection() {
5455
return projection;
5556
}
5657

58+
@Override
59+
public Optional<Integer> getLimit() {
60+
return limit;
61+
}
62+
5763
@Override
5864
public boolean isRangeKeyAware() {
5965
return true;

src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashKeyEntityInformationImpl.java

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public class DynamoDBIdIsHashKeyEntityInformationImpl<T, ID> extends FieldAndGet
4747
private DynamoDBHashKeyExtractingEntityMetadata<T> metadata;
4848
private HashKeyExtractor<ID, ID> hashKeyExtractor;
4949
private Optional<String> projection = Optional.empty();
50+
private Optional<Integer> limit = Optional.empty();
5051

5152
public DynamoDBIdIsHashKeyEntityInformationImpl(Class<T> domainClass,
5253
DynamoDBHashKeyExtractingEntityMetadata<T> metadata) {
@@ -60,6 +61,11 @@ public Optional<String> getProjection() {
6061
return projection;
6162
}
6263

64+
@Override
65+
public Optional<Integer> getLimit() {
66+
return limit;
67+
}
68+
6369
@Override
6470
public Object getHashKey(final ID id) {
6571
Assert.isAssignable(getIdType(), id.getClass(),

src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java

+14-4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public void testProjection() {
8383
String postCode = "postCode";
8484
String user1 = "projection1" + ThreadLocalRandom.current().nextLong();
8585
String user2 = "projection2" + ThreadLocalRandom.current().nextLong();
86+
String user3 = "projection2" + ThreadLocalRandom.current().nextLong();
8687

8788
User u1 = new User();
8889
u1.setId("Id1" + ThreadLocalRandom.current().nextLong());
@@ -91,6 +92,13 @@ public void testProjection() {
9192
u1.setPostCode(postCode);
9293
u1.setNumberOfPlaylists(1);
9394

95+
User u3 = new User();
96+
u3.setId("Id3" + ThreadLocalRandom.current().nextLong());
97+
u3.setName(user3);
98+
u3.setLeaveDate(Instant.now());
99+
u3.setPostCode(postCode);
100+
u3.setNumberOfPlaylists(1);
101+
94102
User u2 = new User();
95103
u2.setId("Id2" + ThreadLocalRandom.current().nextLong());
96104
u2.setName(user2);
@@ -100,11 +108,13 @@ public void testProjection() {
100108

101109
userRepository.save(u1);
102110
userRepository.save(u2);
111+
userRepository.save(u3);
103112

104113
List<User> actualList = new ArrayList<>();
105114
userRepository.findAll().forEach(actualList::add);
106115

107116
List<User> projectedActuals = userRepository.findByPostCode(postCode);
117+
// 2 matches but should be limited to 1 by @Query
108118
assertEquals(1, projectedActuals.size());
109119
User projectedActual = projectedActuals.get(0);
110120
assertNull("Attribute not projected", projectedActual.getName());
@@ -113,11 +123,11 @@ public void testProjection() {
113123
assertNull("Key not projected", projectedActual.getId());
114124
assertNotNull("LeaveDate is projected", projectedActual.getLeaveDate());
115125

116-
List<User> fullActuals = userRepository.findByNameIn(Arrays.asList(user1, user2));
117-
assertEquals(2, fullActuals.size());
126+
List<User> fullActuals = userRepository.findByNameIn(Arrays.asList(user1, user2, user3));
127+
assertEquals(3, fullActuals.size());
118128
User fullActual = fullActuals.get(0);
119-
assertThat(Arrays.asList(user1, user2), hasItems(fullActual.getName()));
120-
assertThat(Arrays.asList(user1, user2), hasItems(fullActuals.get(1).getName()));
129+
assertThat(Arrays.asList(user1, user2, user3), hasItems(fullActual.getName()));
130+
assertThat(Arrays.asList(user1, user2, user3), hasItems(fullActuals.get(1).getName()));
121131
assertNotNull(fullActual.getPostCode());
122132
assertNotNull(fullActual.getNumberOfPlaylists());
123133
assertNotNull(fullActual.getId());

src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public interface UserRepository extends CrudRepository<User, String> {
5252
@EnableScan
5353
void deleteByIdAndName(String id, String name);
5454

55-
@Query(fields = "leaveDate")
55+
@Query(fields = "leaveDate", limit = 1)
5656
List<User> findByPostCode(String postCode);
5757

5858
@EnableScan

src/test/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQueryUnitTest.java

-12
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,6 @@ private <T, ID extends Serializable> void setupCommonMocksForThisRepositoryMetho
139139
Mockito.when(mockEntityMetadata.isRangeKeyAware()).thenReturn(true);
140140
}
141141

142-
try {
143-
Field unwrappedReturnTypeField = mockDynamoDBQueryMethod.getClass() // org.socialsignin.spring.data.dynamodb.repository.query.DynamoDBQueryMethod
144-
.getSuperclass() // org.springframework.data.repository.query.QueryMethod
145-
.getDeclaredField("unwrappedReturnType");
146-
unwrappedReturnTypeField.setAccessible(true); // It's final therefore unlocking the field
147-
unwrappedReturnTypeField.set(mockDynamoDBQueryMethod, clazz);
148-
} catch (Exception e) {
149-
// There is little we can and want do if it fails - Aborting the whole test is
150-
// fine
151-
throw new RuntimeException(e);
152-
}
153-
154142
Mockito.when(mockDynamoDBQueryMethod.getEntityType()).thenReturn(clazz);
155143
Mockito.when(mockDynamoDBQueryMethod.getName()).thenReturn(repositoryMethodName);
156144
Mockito.when(mockDynamoDBQueryMethod.getParameters()).thenReturn(mockParameters);

0 commit comments

Comments
 (0)