diff --git a/pom.xml b/pom.xml index eeee1f1b..3995eb0e 100755 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 2.1.2.RELEASE 6.0.9.Final - 1.11.443 + 1.11.515 4.12 2.23.0 1.2 diff --git a/src/changes/changes.xml b/src/changes/changes.xml index f01b9bd0..38b70890 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -27,6 +27,9 @@ Upgrading to 5.1.0 results in the error Bean property 'dynamoDBMapperConfig' is not writable or has an invalid setter method + + @Query annotation to support query limiting + diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java b/src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java index f1f47bdf..a31d0cd7 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java @@ -180,6 +180,15 @@ public List batchDelete(Iterable entities) { @Override public PaginatedQueryList query(Class clazz, QueryRequest queryRequest) { QueryResult queryResult = amazonDynamoDB.query(queryRequest); + + // If a limit is set, deactivate lazy loading of (matching) items after the + // limit + // via + // com.amazonaws.services.dynamodbv2.datamodeling.PaginatedQueryList.atEndOfResults() + if (queryRequest.getLimit() != null) { + queryResult.setLastEvaluatedKey(null); + } + return new PaginatedQueryList(dynamoDBMapper, clazz, amazonDynamoDB, queryRequest, queryResult, dynamoDBMapperConfig.getPaginationLoadingStrategy(), dynamoDBMapperConfig); } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/Query.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/Query.java index a34bc48c..02284822 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/Query.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/Query.java @@ -21,6 +21,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import static org.socialsignin.spring.data.dynamodb.repository.QueryConstants.QUERY_LIMIT_UNLIMITED; + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented @@ -36,4 +38,13 @@ * Expressions */ String fields() default ""; + + /** + * An integer to limit the number of elements returned. + * + * @see Projection + * Expressions + */ + int limit() default QUERY_LIMIT_UNLIMITED; } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/QueryConstants.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/QueryConstants.java new file mode 100644 index 00000000..313a7094 --- /dev/null +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/QueryConstants.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * 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.socialsignin.spring.data.dynamodb.repository; + +public final class QueryConstants { + + private QueryConstants() { + } + + public static final int QUERY_LIMIT_UNLIMITED = Integer.MIN_VALUE; + +} diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCreator.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCreator.java index 5590300e..610bf683 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCreator.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCreator.java @@ -46,21 +46,24 @@ public abstract class AbstractDynamoDBQueryCreator protected final DynamoDBEntityInformation entityMetadata; protected final DynamoDBOperations dynamoDBOperations; protected final Optional projection; + protected final Optional limit; public AbstractDynamoDBQueryCreator(PartTree tree, DynamoDBEntityInformation entityMetadata, - Optional projection, DynamoDBOperations dynamoDBOperations) { + Optional projection, Optional limitResults, DynamoDBOperations dynamoDBOperations) { super(tree); this.entityMetadata = entityMetadata; this.projection = projection; + this.limit = limitResults; this.dynamoDBOperations = dynamoDBOperations; } public AbstractDynamoDBQueryCreator(PartTree tree, ParameterAccessor parameterAccessor, DynamoDBEntityInformation entityMetadata, Optional projection, - DynamoDBOperations dynamoDBOperations) { + Optional limitResults, DynamoDBOperations dynamoDBOperations) { super(tree, parameterAccessor); this.entityMetadata = entityMetadata; this.projection = projection; + this.limit = limitResults; this.dynamoDBOperations = dynamoDBOperations; } @@ -89,7 +92,6 @@ protected DynamoDBQueryCriteria addCriteria(DynamoDBQueryCriteria } switch (part.getType()) { - case IN : Object in = iterator.next(); Assert.notNull(in, "Creating conditions on null parameters not supported: please specify a value for '" diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java index 5ea4087c..a14ec0bc 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java @@ -72,6 +72,7 @@ public abstract class AbstractDynamoDBQueryCriteria implements DynamoDBQu protected String globalSecondaryIndexName; protected Sort sort = Sort.unsorted(); protected Optional projection = Optional.empty(); + protected Optional limit = Optional.empty(); public abstract boolean isApplicableForLoad(); @@ -132,6 +133,7 @@ protected QueryRequest buildQueryRequest(String tableName, String theIndexName, queryRequest.setSelect(Select.ALL_PROJECTED_ATTRIBUTES); } + limit.ifPresent(queryRequest::setLimit); applySortIfSpecified(queryRequest, new ArrayList<>(new HashSet<>(allowedSortProperties))); } return queryRequest; @@ -695,4 +697,10 @@ public DynamoDBQueryCriteria withProjection(Optional projection) this.projection = projection; return this; } + + @Override + public DynamoDBQueryCriteria withLimit(Optional limit) { + this.limit = limit; + return this; + } } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBCountQueryCreator.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBCountQueryCreator.java index 7f95ccca..bcb99057 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBCountQueryCreator.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBCountQueryCreator.java @@ -32,13 +32,13 @@ public class DynamoDBCountQueryCreator extends AbstractDynamoDBQueryCreat public DynamoDBCountQueryCreator(PartTree tree, DynamoDBEntityInformation entityMetadata, DynamoDBOperations dynamoDBOperations, boolean pageQuery) { - super(tree, entityMetadata, Optional.empty(), dynamoDBOperations); + super(tree, entityMetadata, Optional.empty(), Optional.empty(), dynamoDBOperations); this.pageQuery = pageQuery; } public DynamoDBCountQueryCreator(PartTree tree, ParameterAccessor parameterAccessor, DynamoDBEntityInformation entityMetadata, DynamoDBOperations dynamoDBOperations, boolean pageQuery) { - super(tree, parameterAccessor, entityMetadata, Optional.empty(), dynamoDBOperations); + super(tree, parameterAccessor, entityMetadata, Optional.empty(), Optional.empty(), dynamoDBOperations); this.pageQuery = pageQuery; } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashAndRangeKeyCriteria.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashAndRangeKeyCriteria.java index 2cb9bdbc..0b1e3ff4 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashAndRangeKeyCriteria.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashAndRangeKeyCriteria.java @@ -190,7 +190,7 @@ public DynamoDBQueryExpression buildQueryExpression() { queryExpression.setSelect(Select.SPECIFIC_ATTRIBUTES); queryExpression.setProjectionExpression(projection.get()); } - + limit.ifPresent(queryExpression::setLimit); return queryExpression; } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashKeyOnlyCriteria.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashKeyOnlyCriteria.java index 83706f36..e2747652 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashKeyOnlyCriteria.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBEntityWithHashKeyOnlyCriteria.java @@ -116,6 +116,7 @@ public DynamoDBScanExpression buildScanExpression() { scanExpression.setSelect(Select.SPECIFIC_ATTRIBUTES); scanExpression.setProjectionExpression(projection.get()); } + limit.ifPresent(scanExpression::setLimit); return scanExpression; } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCreator.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCreator.java index e485487d..423cfd0f 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCreator.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCreator.java @@ -29,9 +29,9 @@ public class DynamoDBQueryCreator extends AbstractDynamoDBQueryCreator { public DynamoDBQueryCreator(PartTree tree, ParameterAccessor parameterAccessor, - DynamoDBEntityInformation entityMetadata, Optional projection, + DynamoDBEntityInformation entityMetadata, Optional projection, Optional limit, DynamoDBOperations dynamoDBOperations) { - super(tree, parameterAccessor, entityMetadata, projection, dynamoDBOperations); + super(tree, parameterAccessor, entityMetadata, projection, limit, dynamoDBOperations); } @Override @@ -41,7 +41,7 @@ protected Query complete(@Nullable DynamoDBQueryCriteria criteria, Sor } else { criteria.withSort(sort); criteria.withProjection(projection); - + criteria.withLimit(limit); return criteria.buildQuery(dynamoDBOperations); } } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCriteria.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCriteria.java index 537a3b04..b10ae548 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCriteria.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryCriteria.java @@ -43,6 +43,8 @@ DynamoDBQueryCriteria withSingleValueCriteria(String propertyName, Compar DynamoDBQueryCriteria withProjection(Optional projection); + DynamoDBQueryCriteria withLimit(Optional limit); + Query buildQuery(DynamoDBOperations dynamoDBOperations); Query buildCountQuery(DynamoDBOperations dynamoDBOperations, boolean pageQuery); diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryMethod.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryMethod.java index 36304f4c..c2e9afe0 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryMethod.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/DynamoDBQueryMethod.java @@ -28,6 +28,8 @@ import java.lang.reflect.Method; import java.util.Optional; +import static org.socialsignin.spring.data.dynamodb.repository.QueryConstants.QUERY_LIMIT_UNLIMITED; + /** * @author Michael Lavelle * @author Sebastian Just @@ -38,6 +40,7 @@ public class DynamoDBQueryMethod extends QueryMethod { private final boolean scanEnabledForRepository; private final boolean scanCountEnabledForRepository; private final Optional projectionExpression; + private final Optional limitResults; public DynamoDBQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { super(method, metadata, factory); @@ -54,8 +57,15 @@ public DynamoDBQueryMethod(Method method, RepositoryMetadata metadata, Projectio } else { this.projectionExpression = Optional.empty(); } + int limit = query.limit(); + if (limit != QUERY_LIMIT_UNLIMITED) { + this.limitResults = Optional.of(query.limit()); + } else { + this.limitResults = Optional.empty(); + } } else { this.projectionExpression = Optional.empty(); + this.limitResults = Optional.empty(); } } @@ -98,4 +108,7 @@ public Optional getProjectionExpression() { return this.projectionExpression; } + public Optional getLimitResults() { + return this.limitResults; + } } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQuery.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQuery.java index c0f9ded5..bafb592f 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQuery.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQuery.java @@ -39,7 +39,7 @@ public PartTreeDynamoDBQuery(DynamoDBOperations dynamoDBOperations, DynamoDBQuer protected DynamoDBQueryCreator createQueryCreator(ParametersParameterAccessor accessor) { return new DynamoDBQueryCreator<>(tree, accessor, getQueryMethod().getEntityInformation(), - getQueryMethod().getProjectionExpression(), dynamoDBOperations); + getQueryMethod().getProjectionExpression(), getQueryMethod().getLimitResults(), dynamoDBOperations); } protected DynamoDBCountQueryCreator createCountQueryCreator(ParametersParameterAccessor accessor, diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBEntityInformation.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBEntityInformation.java index b117d5ef..c5f664b3 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBEntityInformation.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBEntityInformation.java @@ -50,4 +50,6 @@ default Object getRangeKey(ID id) { } Optional getProjection(); + + Optional getLimit(); } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashAndRangeKeyEntityInformationImpl.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashAndRangeKeyEntityInformationImpl.java index e588e081..7a6ed7e7 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashAndRangeKeyEntityInformationImpl.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashAndRangeKeyEntityInformationImpl.java @@ -41,6 +41,7 @@ public class DynamoDBIdIsHashAndRangeKeyEntityInformationImpl extends Ref private DynamoDBHashAndRangeKeyExtractingEntityMetadata metadata; private HashAndRangeKeyExtractor hashAndRangeKeyExtractor; private Optional projection = Optional.empty(); + private Optional limit = Optional.empty(); public DynamoDBIdIsHashAndRangeKeyEntityInformationImpl(Class domainClass, DynamoDBHashAndRangeKeyExtractingEntityMetadata metadata) { @@ -54,6 +55,11 @@ public Optional getProjection() { return projection; } + @Override + public Optional getLimit() { + return limit; + } + @Override public boolean isRangeKeyAware() { return true; diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashKeyEntityInformationImpl.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashKeyEntityInformationImpl.java index 07055b66..5c6d3c84 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashKeyEntityInformationImpl.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBIdIsHashKeyEntityInformationImpl.java @@ -47,6 +47,7 @@ public class DynamoDBIdIsHashKeyEntityInformationImpl extends FieldAndGet private DynamoDBHashKeyExtractingEntityMetadata metadata; private HashKeyExtractor hashKeyExtractor; private Optional projection = Optional.empty(); + private Optional limit = Optional.empty(); public DynamoDBIdIsHashKeyEntityInformationImpl(Class domainClass, DynamoDBHashKeyExtractingEntityMetadata metadata) { @@ -60,6 +61,11 @@ public Optional getProjection() { return projection; } + @Override + public Optional getLimit() { + return limit; + } + @Override public Object getHashKey(final ID id) { Assert.isAssignable(getIdType(), id.getClass(), diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java index adfd3a41..ffe4f9a4 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java @@ -83,6 +83,7 @@ public void testProjection() { String postCode = "postCode"; String user1 = "projection1" + ThreadLocalRandom.current().nextLong(); String user2 = "projection2" + ThreadLocalRandom.current().nextLong(); + String user3 = "projection2" + ThreadLocalRandom.current().nextLong(); User u1 = new User(); u1.setId("Id1" + ThreadLocalRandom.current().nextLong()); @@ -91,6 +92,13 @@ public void testProjection() { u1.setPostCode(postCode); u1.setNumberOfPlaylists(1); + User u3 = new User(); + u3.setId("Id3" + ThreadLocalRandom.current().nextLong()); + u3.setName(user3); + u3.setLeaveDate(Instant.now()); + u3.setPostCode(postCode); + u3.setNumberOfPlaylists(1); + User u2 = new User(); u2.setId("Id2" + ThreadLocalRandom.current().nextLong()); u2.setName(user2); @@ -100,11 +108,13 @@ public void testProjection() { userRepository.save(u1); userRepository.save(u2); + userRepository.save(u3); List actualList = new ArrayList<>(); userRepository.findAll().forEach(actualList::add); List projectedActuals = userRepository.findByPostCode(postCode); + // 2 matches but should be limited to 1 by @Query assertEquals(1, projectedActuals.size()); User projectedActual = projectedActuals.get(0); assertNull("Attribute not projected", projectedActual.getName()); @@ -113,11 +123,11 @@ public void testProjection() { assertNull("Key not projected", projectedActual.getId()); assertNotNull("LeaveDate is projected", projectedActual.getLeaveDate()); - List fullActuals = userRepository.findByNameIn(Arrays.asList(user1, user2)); - assertEquals(2, fullActuals.size()); + List fullActuals = userRepository.findByNameIn(Arrays.asList(user1, user2, user3)); + assertEquals(3, fullActuals.size()); User fullActual = fullActuals.get(0); - assertThat(Arrays.asList(user1, user2), hasItems(fullActual.getName())); - assertThat(Arrays.asList(user1, user2), hasItems(fullActuals.get(1).getName())); + assertThat(Arrays.asList(user1, user2, user3), hasItems(fullActual.getName())); + assertThat(Arrays.asList(user1, user2, user3), hasItems(fullActuals.get(1).getName())); assertNotNull(fullActual.getPostCode()); assertNotNull(fullActual.getNumberOfPlaylists()); assertNotNull(fullActual.getId()); diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java index bb33a103..69aaf4c6 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java @@ -52,7 +52,7 @@ public interface UserRepository extends CrudRepository { @EnableScan void deleteByIdAndName(String id, String name); - @Query(fields = "leaveDate") + @Query(fields = "leaveDate", limit = 1) List findByPostCode(String postCode); @EnableScan diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQueryUnitTest.java b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQueryUnitTest.java index 3aaa56d6..4afad944 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQueryUnitTest.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/query/PartTreeDynamoDBQueryUnitTest.java @@ -139,18 +139,6 @@ private void setupCommonMocksForThisRepositoryMetho Mockito.when(mockEntityMetadata.isRangeKeyAware()).thenReturn(true); } - try { - Field unwrappedReturnTypeField = mockDynamoDBQueryMethod.getClass() // org.socialsignin.spring.data.dynamodb.repository.query.DynamoDBQueryMethod - .getSuperclass() // org.springframework.data.repository.query.QueryMethod - .getDeclaredField("unwrappedReturnType"); - unwrappedReturnTypeField.setAccessible(true); // It's final therefore unlocking the field - unwrappedReturnTypeField.set(mockDynamoDBQueryMethod, clazz); - } catch (Exception e) { - // There is little we can and want do if it fails - Aborting the whole test is - // fine - throw new RuntimeException(e); - } - Mockito.when(mockDynamoDBQueryMethod.getEntityType()).thenReturn(clazz); Mockito.when(mockDynamoDBQueryMethod.getName()).thenReturn(repositoryMethodName); Mockito.when(mockDynamoDBQueryMethod.getParameters()).thenReturn(mockParameters);