Skip to content

Commit

Permalink
Pagination support for DynamoDB v2
Browse files Browse the repository at this point in the history
  • Loading branch information
musketyr committed Jun 17, 2024
1 parent 84e18c9 commit 6c78fc5
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 16 deletions.
5 changes: 4 additions & 1 deletion docs/guide/src/docs/asciidoc/dynamodb.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ Instead you can annotate any method with `@Query` annotation to make it
* batch delete method if its name begins with `delete`
* otherwise an advanced query method

For DynamoDB v2 you can use `@Index`, `@Consistent` `@Descending` and `@Fitler` annotations to further customize the query without the need of creating a custom query class and use it with `@Query` annotation.
For DynamoDB v2 you can use `@Index`, `@Consistent` `@Descending`, `@Filter`, `@Limit`, `@Page` and `@LastEvaluatedKey` annotations to further customize the query without the need of creating a custom query class and use it with `@Query` annotation.

[source,java,indent=0,options="nowrap"]
.Advanced Query Annotations
Expand All @@ -582,6 +582,9 @@ include::{root-dir}/subprojects/micronaut-amazon-awssdk-dynamodb/src/test/groovy
<9> Only`EQ`, `LE`, `LT`, `GE`, `GT`, `BETWEEN` and `BEGINS_WITH` operators are supported
<10> You can also use `@Filter` annotation to specify the sort key name
<11> If you use any customization annotations on delete method, then the method will be used as batch delete method
<12> You can pass the last evaluated key to the query. It must be the same type as the entity type.
<13> You can use `@Page` annotation to give the query the pagination hint
<14> You can use `@Limit` annotation to specify the maximum number of items to return

TIP: The operator `EQ` is used by default if `@Filter` annotation is not present. This makes it special and the service introduction tries to find the appropriate operation based on the actual value. For collections or arrays, `inList` operation is actually used. If the actual value is `null` then `isNull` operation is used. For other types, `eq` operation is used. For sort keys, `eq` operation is always used.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,22 +168,22 @@ private <T> Object doIntercept(MethodInvocationContext<Object, Object> context,
}

if (methodName.startsWith("query") || methodName.startsWith("findAll") || methodName.startsWith("list") || methodName.startsWith("count") || methodName.startsWith("delete")) {
QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata());
QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata(), service.getItemType());
if (methodName.startsWith("count")) {
if (partitionAndSort.isCustomized()) {
return unwrapIfRequired(service.countUsingQuery(partitionAndSort.generateQuery(context)), context.getReturnType().getType());
return unwrapIfRequired(service.countUsingQuery(partitionAndSort.generateQuery(context, conversionService)), context.getReturnType().getType());
}
return unwrapIfRequired(service.count(partitionAndSort.getPartitionValue(context.getParameters()), partitionAndSort.getSortValue(context.getParameters())), context.getReturnType().getType());
}
if (methodName.startsWith("delete")) {
if (partitionAndSort.isCustomized()) {
return unwrapIfRequired(service.deleteAll(service.query(partitionAndSort.generateQuery(context))), context.getReturnType().getType());
return unwrapIfRequired(service.deleteAll(service.query(partitionAndSort.generateQuery(context, conversionService))), context.getReturnType().getType());
}
Optional<ItemArgument> maybeItemArgument = ItemArgument.findItemArgument(service.getItemType(), context);
return unwrapIfRequired(handleDelete(service, context, maybeItemArgument), context.getReturnType().getType());
}
if (partitionAndSort.isCustomized()) {
return unwrapIfRequired(service.query(partitionAndSort.generateQuery(context)), context.getReturnType().getType());
return unwrapIfRequired(service.query(partitionAndSort.generateQuery(context, conversionService)), context.getReturnType().getType());
}
return unwrapIfRequired(
service.findAll(partitionAndSort.getPartitionValue(context.getParameters()), partitionAndSort.getSortValue(context.getParameters())),
Expand Down Expand Up @@ -268,7 +268,7 @@ private <T> Publisher<?> handleDelete(AsyncDynamoDbService<T> service, MethodInv
throw new UnsupportedOperationException("Method expects at most 2 parameters - partition key and sort key, an item or items");
}

QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata());
QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata(), service.getItemType());
return service.delete(partitionAndSort.getPartitionValue(params), partitionAndSort.getSortValue(params));
}

Expand All @@ -280,7 +280,7 @@ private <T> Publisher<T> handleGet(AsyncDynamoDbService<T> service, MethodInvoca
throw new UnsupportedOperationException("Method expects at most 2 parameters - partition key and sort key or sort keys");
}

QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata());
QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata(), service.getItemType());
Object partitionValue = partitionAndSort.getPartitionValue(params);

if (!partitionAndSort.hasSortKey()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,22 @@ private <T> Object doIntercept(MethodInvocationContext<Object, Object> context,
}

if (methodName.startsWith("query") || methodName.startsWith("findAll") || methodName.startsWith("list") || methodName.startsWith("count") || methodName.startsWith("delete")) {
QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata());
QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata(), service.getItemType());
if (methodName.startsWith("count")) {
if (partitionAndSort.isCustomized()) {
return service.countUsingQuery(partitionAndSort.generateQuery(context));
return service.countUsingQuery(partitionAndSort.generateQuery(context, conversionService));
}
return service.count(partitionAndSort.getPartitionValue(context.getParameters()), partitionAndSort.getSortValue(context.getParameters()));
}
if (methodName.startsWith("delete")) {
if (partitionAndSort.isCustomized()) {
return service.deleteAll(service.query(partitionAndSort.generateQuery(context)));
return service.deleteAll(service.query(partitionAndSort.generateQuery(context, conversionService)));
}
Optional<ItemArgument> maybeItemArgument = ItemArgument.findItemArgument(service.getItemType(), context);
return handleDelete(service, context, maybeItemArgument);
}
if (partitionAndSort.isCustomized()) {
return publisherOrIterable(service.query(partitionAndSort.generateQuery(context)), context.getReturnType().getType());
return publisherOrIterable(service.query(partitionAndSort.generateQuery(context, conversionService)), context.getReturnType().getType());
}
return publisherOrIterable(
service.findAll(partitionAndSort.getPartitionValue(context.getParameters()), partitionAndSort.getSortValue(context.getParameters())),
Expand Down Expand Up @@ -215,7 +215,7 @@ private <T> Object handleDelete(DynamoDbService<T> service, MethodInvocationCont
throw new UnsupportedOperationException("Method expects at most 2 parameters - partition key and sort key, an item or items");
}

QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata());
QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata(), service.getItemType());
service.delete(partitionAndSort.getPartitionValue(params), partitionAndSort.getSortValue(params));
return 1;
}
Expand All @@ -228,7 +228,7 @@ private <T> Object handleGet(DynamoDbService<T> service, MethodInvocationContext
throw new UnsupportedOperationException("Method expects at most 2 parameters - partition key and sort key or sort keys");
}

QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata());
QueryArguments partitionAndSort = QueryArguments.create(context, service.getTable().tableSchema().tableMetadata(), service.getItemType());
Object partitionValue = partitionAndSort.getPartitionValue(params);

if (!partitionAndSort.hasSortKey()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2018-2024 Agorapulse.
*
* 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
*
* https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation;


import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* This annotation on a parameter of a method will mark the parameter as the last evaluated key. The parameter must be of the same type as the entity.
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
public @interface LastEvaluatedKey {


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2018-2024 Agorapulse.
*
* 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
*
* https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation;


import java.lang.annotation.*;

/**
* This annotation on a parameter of a method will mark the parameter as the absolute maximum number of items to be returned by the annotated method. The parameter can be any numeric type.
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
public @interface Limit {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2018-2024 Agorapulse.
*
* 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
*
* https://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 com.agorapulse.micronaut.amazon.awssdk.dynamodb.annotation;


import java.lang.annotation.*;

/**
* This annotation on a parameter of a method will mark the parameter as the page hint for retrieving the items by the annotated method. The parameter can be any numeric type.
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER})
public @interface Page {

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public class QueryArguments {
private static final String SORT = "sort";
private static final String HASH = "hash";
private static final String RANGE = "range";
private static final String LAST_EVALUATED_KEY = "lastEvaluatedKey";
private static final String LIMIT = "limit";
private static final String PAGE = "page";

private final Map<String, FilterArgument> filters = new LinkedHashMap<>();

Expand All @@ -47,8 +50,11 @@ public class QueryArguments {
private String index;
private boolean consistent;
private boolean descending;
private Argument<?> lastEvaluatedKey;
private Argument<?> limit;
private Argument<?> page;

public static QueryArguments create(MethodInvocationContext<Object, Object> context, TableMetadata tableMetadata) {
public static <T> QueryArguments create(MethodInvocationContext<Object, Object> context, TableMetadata tableMetadata, Class<T> itemType) {
QueryArguments queryArguments = new QueryArguments();

queryArguments.index = context.getTargetMethod().isAnnotationPresent(Index.class) ? context.getTargetMethod().getAnnotation(Index.class).value() : null;
Expand Down Expand Up @@ -77,6 +83,24 @@ public static QueryArguments create(MethodInvocationContext<Object, Object> cont
|| argument.getName().equals(tableMetadata.primaryPartitionKey())
) {
queryArguments.partitionKey = argument;
} else if (
argument.isAnnotationPresent(LastEvaluatedKey.class)
|| argument.getName().toLowerCase().contains(LAST_EVALUATED_KEY)
) {
if (!argument.getType().equals(itemType)) {
throw new UnsupportedOperationException("Last evaluated key must be of the same type as the entity");
}
queryArguments.lastEvaluatedKey = argument;
} else if (
argument.isAnnotationPresent(Limit.class)
|| argument.getName().toLowerCase().contains(LIMIT)
) {
queryArguments.limit = argument;
} else if (
argument.isAnnotationPresent(Page.class)
|| argument.getName().toLowerCase().contains(PAGE)
) {
queryArguments.page = argument;
} else {
String name = FilterArgument.getArgumentName(argument);
queryArguments.filters.computeIfAbsent(name, argName -> new FilterArgument()).fill(argument);
Expand Down Expand Up @@ -134,7 +158,7 @@ public Publisher<?> getSortAttributeValues(ConversionService conversionService,
}


public <T> Consumer<QueryBuilder<T>> generateQuery(MethodInvocationContext<Object, Object> context) {
public <T> Consumer<QueryBuilder<T>> generateQuery(MethodInvocationContext<Object, Object> context, ConversionService conversionService) {
return q -> {
if (index != null) {
q.index(index);
Expand Down Expand Up @@ -174,6 +198,18 @@ public <T> Consumer<QueryBuilder<T>> generateQuery(MethodInvocationContext<Objec
);
});
}

if (lastEvaluatedKey != null) {
q.lastEvaluatedKey(context.getParameters().get(lastEvaluatedKey.getName()).getValue());
}

if (limit != null) {
q.limit(conversionService.convertRequired(context.getParameters().get(limit.getName()).getValue(), Integer.class));
}

if (page != null) {
q.page(conversionService.convertRequired(context.getParameters().get(page.getName()).getValue(), Integer.class));
}
};
}

Expand All @@ -182,6 +218,6 @@ public boolean isSortKeyPublisherOrIterable() {
}

public boolean isCustomized() {
return index != null || consistent || descending || !filters.isEmpty() || sortKey != null && sortKey.getOperator() != Filter.Operator.EQ;
return index != null || consistent || descending || !filters.isEmpty() || sortKey != null && sortKey.getOperator() != Filter.Operator.EQ || lastEvaluatedKey != null || limit != null || page != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,23 @@ public void testJavaService() {
assertEquals(4, s.findAllByNumberInExplicit("1", List.of(1, 2)).size());
assertEquals(4, s.findAllByNumberBetween("1", 1, 2).size());
assertEquals(4, s.findAllByRangeBeginsWith("1", "f").size());

List<DynamoDBEntity> allByNumberNotPaginated = s.findAllByNumberNot("1", 3, createLastEvaluatedKey("1", "2"), 1, 2);
assertEquals(2, allByNumberNotPaginated.size());
assertEquals("3", allByNumberNotPaginated.get(0).getId());
assertEquals("4", allByNumberNotPaginated.get(1).getId());

assertEquals(4, s.deleteAllByRangeBeginsWith("1", "f"));
assertEquals(0, s.findAllByRangeBeginsWith("1", "f").size());
}

private DynamoDBEntity createLastEvaluatedKey(String parentId, String id) {
DynamoDBEntity entity = new DynamoDBEntity();
entity.setParentId(parentId);
entity.setId(id);
return entity;
}


private DynamoDBEntity createEntity(String parentId, String id, String rangeIndex, Integer number, Date date) {
DynamoDBEntity entity = new DynamoDBEntity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ int deleteAllByRangeBeginsWith(
)
String rangeIndexPrefix
);

List<DynamoDBEntity> findAllByNumberNot(
@PartitionKey String parentId,
@Filter(Filter.Operator.NE) Integer number,
@LastEvaluatedKey DynamoDBEntity lastEvaluatedKey, // <12>
@Page int page, // <13>
@Limit int limit // <14>
);
// end::advanced-query-methods[]
// CHECKSTYLE:ON

Expand Down

0 comments on commit 6c78fc5

Please sign in to comment.