Trabalhar com resultados paginados: verificações e consultas - AWS SDK for Java 2.x

Trabalhar com resultados paginados: verificações e consultas

Os métodos scan, query e batch da API do Cliente Aprimorado do DynamoDB retornam respostas com uma ou mais páginas. Uma página contém um ou mais itens. Seu código pode processar a resposta por página ou pode processar itens individuais.

Uma resposta paginada retornada pelo cliente DynamoDbEnhancedClient síncrono retorna um objeto PageIterable, enquanto uma resposta retornada pelo cliente DynamoDbEnhancedAsyncClient assíncrono retorna um objeto PagePublisher.

Esta seção analisa o processamento de resultados paginados e fornece exemplos que usam as APIs de verificação e consulta.

Verificar uma tabela

O método do SDK scan corresponde à operação do DynamoDB com o mesmo nome. A API do Cliente Aprimorado do DynamoDB oferece as mesmas opções, mas usa um modelo de objeto familiar e gerencia a paginação para você.

Primeiro, exploramos a interface PageIterable examinando o método scan da classe de mapeamento síncrono, DynamoDbTable.

Usar a API síncrona

O exemplo a seguir mostra o método scan que usa uma expressão para filtrar os itens retornados. O ProductCatalog é o objeto de modelo que foi mostrado anteriormente.

A expressão de filtragem mostrada após a linha de comentário 2 limita os itens ProductCatalog que são retornados àqueles com um valor de preço de 8,00 a 80,00.

Este exemplo também exclui os valores isbn usando o método attributesToProject mostrado após a linha de comentário 1.

Depois da linha de comentário 3, o objeto PageIterable, pagedResults, é retornado pelo método scan. O método stream de PageIterable retorna um objeto java.util.Stream, que você pode usar para processar as páginas. Neste exemplo, o número de páginas é contado e registrado.

Começando com a linha de comentários 4, o exemplo mostra duas variações de acesso aos itens ProductCatalog. A versão após a linha de comentário 4a percorre cada página e classifica e registra os itens em cada página. A versão após a linha de comentário 4b ignora a iteração da página e acessa os itens diretamente.

A interface PageIterable oferece várias maneiras de processar resultados por causa de suas duas interfaces principais: java.lang.Iterable e SdkIterable. Iterable traz os métodos forEach, iterator e spliterator, e SdkIterable traz o método stream.

public static void scanSync(DynamoDbTable<ProductCatalog> productCatalog) { Map<String, AttributeValue> expressionValues = Map.of( ":min_value", numberValue(8.00), ":max_value", numberValue(80.00)); ScanEnhancedRequest request = ScanEnhancedRequest.builder() .consistentRead(true) // 1. the 'attributesToProject()' method allows you to specify which values you want returned. .attributesToProject("id", "title", "authors", "price") // 2. Filter expression limits the items returned that match the provided criteria. .filterExpression(Expression.builder() .expression("price >= :min_value AND price <= :max_value") .expressionValues(expressionValues) .build()) .build(); // 3. A PageIterable object is returned by the scan method. PageIterable<ProductCatalog> pagedResults = productCatalog.scan(request); logger.info("page count: {}", pagedResults.stream().count()); // 4. Log the returned ProductCatalog items using two variations. // 4a. This version sorts and logs the items of each page. pagedResults.stream().forEach(p -> p.items().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach( item -> logger.info(item.toString()) )); // 4b. This version sorts and logs all items for all pages. pagedResults.items().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach( item -> logger.info(item.toString()) ); }

Usar a API assíncrona

O método scan assíncrono retorna os resultados como um objeto PagePublisher. A interface PagePublisher tem dois métodos subscribe que você pode usar para processar páginas de resposta. Um método subscribe vem da interface principal org.reactivestreams.Publisher. Para processar páginas usando essa primeira opção, transmita uma instância Subscriber para o método subscribe. O primeiro exemplo a seguir mostra o uso do método subscribe.

O segundo método subscribe vem da interface SdkPublisher. Esta versão de subscribe aceita um Consumer em vez de um Subscriber. Essa variação do método subscribe é mostrada no segundo exemplo.

O exemplo a seguir mostra a versão assíncrona do método scan que usa a mesma expressão de filtro mostrada no exemplo anterior.

Após a linha de comentário 3, DynamoDbAsyncTable.scan retorna um objeto PagePublisher. Na próxima linha, o código cria uma instância da interface org.reactivestreams.Subscriber, ProductCatalogSubscriber, que assina a PagePublisher após o comentário na linha 4.

O objeto Subscriber coleta os itens ProductCatalog de cada página no método onNext após a linha de comentário 8 no exemplo da classe ProductCatalogSubscriber. Os itens são armazenados na varável List privada e acessados no código de chamada com o método ProductCatalogSubscriber.getSubscribedItems(). A chamado é feita após a linha de comentários 5.

Depois que a lista é recuperada, o código classifica todos os itens ProductCatalog por preço e registra cada item.

O CountDownLatch na classe ProductCatalogSubscriber bloqueia o thread de chamada até que todos os itens tenham sido adicionados à lista antes de continuar após a linha de comentário 5.

public static void scanAsync(DynamoDbAsyncTable productCatalog) { ScanEnhancedRequest request = ScanEnhancedRequest.builder() .consistentRead(true) .attributesToProject("id", "title", "authors", "price") .filterExpression(Expression.builder() // 1. :min_value and :max_value are placeholders for the values provided by the map .expression("price >= :min_value AND price <= :max_value") // 2. Two values are needed for the expression and each is supplied as a map entry. .expressionValues( Map.of( ":min_value", numberValue(8.00), ":max_value", numberValue(400_000.00))) .build()) .build(); // 3. A PagePublisher object is returned by the scan method. PagePublisher<ProductCatalog> pagePublisher = productCatalog.scan(request); ProductCatalogSubscriber subscriber = new ProductCatalogSubscriber(); // 4. Subscribe the ProductCatalogSubscriber to the PagePublisher. pagePublisher.subscribe(subscriber); // 5. Retrieve all collected ProductCatalog items accumulated by the subscriber. subscriber.getSubscribedItems().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach(item -> logger.info(item.toString())); // 6. Use a Consumer to work through each page. pagePublisher.subscribe(page -> page .items().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach(item -> logger.info(item.toString()))) .join(); // If needed, blocks the subscribe() method thread until it is finished processing. // 7. Use a Consumer to work through each ProductCatalog item. pagePublisher.items() .subscribe(product -> logger.info(product.toString())) .exceptionally(failure -> { logger.error("ERROR - ", failure); return null; }) .join(); // If needed, blocks the subscribe() method thread until it is finished processing. }
private static class ProductCatalogSubscriber implements Subscriber<Page<ProductCatalog>> { private CountDownLatch latch = new CountDownLatch(1); private Subscription subscription; private List<ProductCatalog> itemsFromAllPages = new ArrayList<>(); @Override public void onSubscribe(Subscription sub) { subscription = sub; subscription.request(1L); try { latch.await(); // Called by main thread blocking it until latch is released. } catch (InterruptedException e) { throw new RuntimeException(e); } } @Override public void onNext(Page<ProductCatalog> productCatalogPage) { // 8. Collect all the ProductCatalog instances in the page, then ask the publisher for one more page. itemsFromAllPages.addAll(productCatalogPage.items()); subscription.request(1L); } @Override public void onError(Throwable throwable) { } @Override public void onComplete() { latch.countDown(); // Call by subscription thread; latch releases. } List<ProductCatalog> getSubscribedItems() { return this.itemsFromAllPages; } }

O exemplo de trecho a seguir usa a versão do método PagePublisher.subscribe que aceita um Consumer após a linha de comentário 6. O parâmetro lambda em Java consome páginas, que processam ainda mais cada item. Neste exemplo, cada página é processada e os itens em cada página são classificados e, em seguida, registrados.

// 6. Use a Consumer to work through each page. pagePublisher.subscribe(page -> page .items().stream() .sorted(Comparator.comparing(ProductCatalog::price)) .forEach(item -> logger.info(item.toString()))) .join(); // If needed, blocks the subscribe() method thread until it is finished processing.

O método items do PagePublisher desempacota as instâncias do modelo para que seu código possa processar os itens diretamente. Essa abordagem é mostrada no trecho a seguir.

// 7. Use a Consumer to work through each ProductCatalog item. pagePublisher.items() .subscribe(product -> logger.info(product.toString())) .exceptionally(failure -> { logger.error("ERROR - ", failure); return null; }) .join(); // If needed, blocks the subscribe() method thread until it is finished processing.

Consultar uma tabela

É possível usar o Cliente Aprimorado do DynamoDB para consultar sua tabela e recuperar vários itens que correspondam a critérios específicos. O método query() encontra itens com base nos valores da chave primária usando as anotações @DynamoDbPartitionKey e, opcionalmente, @DynamoDbSortKey definidas em sua classe de dados.

O método query() requer um valor de chave de partição e, opcionalmente, aceita condições de chave de classificação para refinar ainda mais os resultados. Assim como a API scan, as consultas retornam um PageIterable para chamadas síncronas e um PagePublisher para chamadas assíncronas.

Exemplos de métodos Query

O exemplo de código do método query() a seguir usa a classe MovieActor. A classe de dados define uma chave primária composta constituída pelo atributo movie como a chave de partição e pelo atributo actor como a chave de classificação.

package org.example.tests.model; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; import java.util.Objects; @DynamoDbBean public class MovieActor implements Comparable<MovieActor> { private String movieName; private String actorName; private String actingAward; private Integer actingYear; private String actingSchoolName; @DynamoDbPartitionKey @DynamoDbAttribute("movie") public String getMovieName() { return movieName; } public void setMovieName(String movieName) { this.movieName = movieName; } @DynamoDbSortKey @DynamoDbAttribute("actor") public String getActorName() { return actorName; } public void setActorName(String actorName) { this.actorName = actorName; } @DynamoDbSecondaryPartitionKey(indexNames = "acting_award_year") @DynamoDbAttribute("actingaward") public String getActingAward() { return actingAward; } public void setActingAward(String actingAward) { this.actingAward = actingAward; } @DynamoDbSecondarySortKey(indexNames = {"acting_award_year", "movie_year"}) @DynamoDbAttribute("actingyear") public Integer getActingYear() { return actingYear; } public void setActingYear(Integer actingYear) { this.actingYear = actingYear; } @DynamoDbAttribute("actingschoolname") public String getActingSchoolName() { return actingSchoolName; } public void setActingSchoolName(String actingSchoolName) { this.actingSchoolName = actingSchoolName; } @Override public String toString() { final StringBuffer sb = new StringBuffer("MovieActor{"); sb.append("movieName='").append(movieName).append('\''); sb.append(", actorName='").append(actorName).append('\''); sb.append(", actingAward='").append(actingAward).append('\''); sb.append(", actingYear=").append(actingYear); sb.append(", actingSchoolName='").append(actingSchoolName).append('\''); sb.append('}'); return sb.toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MovieActor that = (MovieActor) o; return Objects.equals(movieName, that.movieName) && Objects.equals(actorName, that.actorName) && Objects.equals(actingAward, that.actingAward) && Objects.equals(actingYear, that.actingYear) && Objects.equals(actingSchoolName, that.actingSchoolName); } @Override public int hashCode() { return Objects.hash(movieName, actorName, actingAward, actingYear, actingSchoolName); } @Override public int compareTo(MovieActor o) { if (this.movieName.compareTo(o.movieName) != 0){ return this.movieName.compareTo(o.movieName); } else { return this.actorName.compareTo(o.actorName); } } }

Os exemplos de código a seguir são consultados com base nos itens a seguir.

MovieActor{movieName='movie01', actorName='actor0', actingAward='actingaward0', actingYear=2001, actingSchoolName='null'} MovieActor{movieName='movie01', actorName='actor1', actingAward='actingaward1', actingYear=2001, actingSchoolName='actingschool1'} MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'} MovieActor{movieName='movie01', actorName='actor3', actingAward='actingaward3', actingYear=2001, actingSchoolName='null'} MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'} MovieActor{movieName='movie02', actorName='actor0', actingAward='actingaward0', actingYear=2002, actingSchoolName='null'} MovieActor{movieName='movie02', actorName='actor1', actingAward='actingaward1', actingYear=2002, actingSchoolName='actingschool1'} MovieActor{movieName='movie02', actorName='actor2', actingAward='actingaward2', actingYear=2002, actingSchoolName='actingschool2'} MovieActor{movieName='movie02', actorName='actor3', actingAward='actingaward3', actingYear=2002, actingSchoolName='null'} MovieActor{movieName='movie02', actorName='actor4', actingAward='actingaward4', actingYear=2002, actingSchoolName='actingschool4'} MovieActor{movieName='movie03', actorName='actor0', actingAward='actingaward0', actingYear=2003, actingSchoolName='null'} MovieActor{movieName='movie03', actorName='actor1', actingAward='actingaward1', actingYear=2003, actingSchoolName='actingschool1'} MovieActor{movieName='movie03', actorName='actor2', actingAward='actingaward2', actingYear=2003, actingSchoolName='actingschool2'} MovieActor{movieName='movie03', actorName='actor3', actingAward='actingaward3', actingYear=2003, actingSchoolName='null'} MovieActor{movieName='movie03', actorName='actor4', actingAward='actingaward4', actingYear=2003, actingSchoolName='actingschool4'}

O código a seguir define duas instâncias QueryConditional: keyEqual (após a linha de comentário 1) e sortGreaterThanOrEqualTo (após a linha de comentário 1a).

Consultar itens por chave de partição

A instância keyEqual combina itens com um valor de chave de partição de movie01.

Esse exemplo também define uma expressão de filtro após a linha de comentário 2 que filtra qualquer item que não tenha um valor actingschoolname.

O QueryEnhancedRequest combina a condição da chave e a expressão do filtro para a consulta.

public static void query(DynamoDbTable movieActorTable) { // 1. Define a QueryConditional instance to return items matching a partition value. QueryConditional keyEqual = QueryConditional.keyEqualTo(b -> b.partitionValue("movie01")); // 1a. Define a QueryConditional that adds a sort key criteria to the partition value criteria. QueryConditional sortGreaterThanOrEqualTo = QueryConditional.sortGreaterThanOrEqualTo(b -> b.partitionValue("movie01").sortValue("actor2")); // 2. Define a filter expression that filters out items whose attribute value is null. final Expression filterOutNoActingschoolname = Expression.builder().expression("attribute_exists(actingschoolname)").build(); // 3. Build the query request. QueryEnhancedRequest tableQuery = QueryEnhancedRequest.builder() .queryConditional(keyEqual) .filterExpression(filterOutNoActingschoolname) .build(); // 4. Perform the query using the "keyEqual" conditional and filter expression. PageIterable<MovieActor> pagedResults = movieActorTable.query(tableQuery); logger.info("page count: {}", pagedResults.stream().count()); // Log number of pages. pagedResults.items().stream() .sorted() .forEach( item -> logger.info(item.toString()) // Log the sorted list of items. );
exemplo – Saída usando a consulta condicional keyEqual

Esta é a saída gerada pela execução do método. A saída exibe itens com um valor movieName de movie01 e não exibe nenhum item com actingSchoolName igual a null.

2023-03-05 13:11:05 [main] INFO org.example.tests.QueryDemo:46 - page count: 1 2023-03-05 13:11:05 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor1', actingAward='actingaward1', actingYear=2001, actingSchoolName='actingschool1'} 2023-03-05 13:11:05 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'} 2023-03-05 13:11:05 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}

Consultar itens por chave de partição e chave de classificação

O sortGreaterThanOrEqualTo QueryConditional refina uma correspondência de chave de partição (movie01) adicionando uma condição de chave de classificação para valores maiores ou iguais a actor2.

Os métodos QueryConditional que começam com sort exigem que o valor da chave de partição corresponda e refinam ainda mais a consulta por meio de uma comparação com base no valor da chave de classificação. Sort no nome do método não significa que os resultados estão classificados, mas que um valor de chave de classificação será usado para comparação.

No trecho a seguir, alteramos a solicitação de consulta mostrada anteriormente após a linha de comentário 3. Esse trecho substitui a condicional de consulta “keyEqual” pela condicional de consulta “sortGreaterThanOrEqualTo” que foi definida após a linha de comentário 1a. O código a seguir também remove a expressão do filtro.

QueryEnhancedRequest tableQuery = QueryEnhancedRequest.builder() .queryConditional(sortGreaterThanOrEqualTo).build();
exemplo – Saída usando a consulta condicional sortGreaterThanOrEqualTo

A saída a seguir exibe os resultados da consulta. A consulta retorna itens que têm um valor movieName igual a movie01 e somente itens que têm um valor actorName maior ou igual a actor2. Como removemos o filtro, a consulta retorna itens que não têm valor para o atributo actingSchoolName.

2023-03-05 13:15:00 [main] INFO org.example.tests.QueryDemo:46 - page count: 1 2023-03-05 13:15:00 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor2', actingAward='actingaward2', actingYear=2001, actingSchoolName='actingschool2'} 2023-03-05 13:15:00 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor3', actingAward='actingaward3', actingYear=2001, actingSchoolName='null'} 2023-03-05 13:15:00 [main] INFO org.example.tests.QueryDemo:51 - MovieActor{movieName='movie01', actorName='actor4', actingAward='actingaward4', actingYear=2001, actingSchoolName='actingschool4'}