DynamoDB Query en Java (AWS SDK v2)

queryPaginator dans AWS SDK for Java 2.x ressemble à une collection et n'en est pas une. C'est un itérable paresseux réutilisable, et la différence se voit sur ta facture la deuxième fois que tu boucles dessus. Pour savoir si Query est la bonne lecture tout court, voir Query vs. Scan.

Code

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;

public class QueryExample {
    public static void main(String[] args) {
        try (DynamoDbClient ddb = DynamoDbClient.builder()
                .region(Region.US_EAST_1)
                .build()) {

            Map<String, String> names = new HashMap<>();
            names.put("#hashKey", "Artist");
            names.put("#rangeKey", "SongTitle");

            Map<String, AttributeValue> values = new HashMap<>();
            values.put(":hashKeyValue", AttributeValue.builder().s("Arturo Sandoval").build());
            values.put(":rangeKeyValue", AttributeValue.builder().s("C").build());

            QueryRequest request = QueryRequest.builder()
                    .tableName("Music")
                    .keyConditionExpression(
                            "#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)")
                    .expressionAttributeNames(names)
                    .expressionAttributeValues(values)
                    .build();

            List<Map<String, AttributeValue>> items = new ArrayList<>();
            for (QueryResponse page : ddb.queryPaginator(request)) {
                items.addAll(page.items());
            }
            System.out.println("Found " + items.size() + " items");
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

QueryIterable relance la requête à chaque itération

Sur un jeu d'essai de 600 morceaux, chacun d'environ 3,9 Ko et tous sous Artist = "Arturo Sandoval", ddb.queryPaginator(request) a rendu la main en 2,6 ms sans rien envoyer. Puis le même objet QueryIterable a été parcouru deux fois :

queryPaginator(request) returned in 2.60 ms   (class QueryIterable, 0 requests)
iteration 1: pages=3  items=600  capacity=284.5
iteration 2: pages=3  items=600  capacity=284.5   <- same object

Deux boucles for sur une seule variable, 569 unités de lecture. Les pages n'ont jamais été mises en cache ; chaque itération reparcourt LastEvaluatedKey depuis le début. Si tu as besoin des éléments deux fois, vide l'itérable une fois dans une List, comme le fait l'exemple.

L'autre face de cette même paresse, c'est l'endroit où les erreurs remontent. Construis une requête dont la condition de clé omet la clé de partition et queryPaginator l'accepte sans broncher, parce qu'aucun appel n'a encore eu lieu :

queryPaginator(bad) constructed without throwing
threw on iteration: DynamoDbException / ValidationException /
Query condition missed key schema element / http 400

Un try-catch enroulé autour du builder n'attrape rien. Il doit entourer la boucle, et c'est pour ça que l'exemple met tout le bloc dans un seul try.

Mesuré le 2026-07-28 contre DynamoDB Local (amazon/dynamodb-local) avec software.amazon.awssdk:dynamodb 2.49.4 sur OpenJDK 26.0.1.

Explication

  • ddb.queryPaginator(request).items() aplatit les pages en un Iterable<Map<String, AttributeValue>> et pagine derrière toi, si bien que la double boucle de l'exemple se réduit à une seule quand tu ne veux que les éléments. C'est aussi un SdkIterable, donc .stream() fonctionne.
  • Les nombres sont des String dans le builder. AttributeValue.builder().n("1994") n'est pas une faute de frappe pour .n(1994) — le setter n() prend un java.lang.String, parce que DynamoDB transporte les nombres en texte décimal pour éviter les arrondis des flottants binaires. Passer un int Java ne compilera pas.
  • keyConditionExpression prend une égalité sur la clé de partition plus au plus une condition sur la clé de tri (=, <, <=, >, >=, BETWEEN, begins_with) ; .scanIndexForward(false) inverse l'ordre et .indexName("...") recible un index secondaire.
  • L'Enhanced Client est l'autre ergonomie. software.amazon.awssdk.enhanced.dynamodb mappe des beans annotés au lieu de Map<String, AttributeValue>, et sa méthode query renvoie un PageIterable<T> avec la même sémantique de réitération paresseuse mesurée plus haut.

Le faire visuellement

#hashKey et #rangeKey sont ici des alias que rien ne t'impose : ni Artist ni SongTitle ne figurent sur la liste AWS des 573 mots réservés. Le vérificateur de mots réservés te dit lesquels de tes noms d'attribut ont vraiment besoin du traitement #, pour que la map d'alias cesse d'être un rituel.

Pour essayer une condition de clé sur une vraie table avant de construire le QueryRequest, télécharge DynoTable et parcours les résultats dans une grille.

Exemples liés

Références

Construis cette requête visuellement

Compose cette opération dans le Générateur de requêtes DynamoDB gratuit — condition de clé, filtre, index, Limit, ordre de tri et boucle de pagination — et copie-la en retour comme programme exécutable SDK v3, CLI ou boto3.

Ouvrir le Générateur de requêtes DynamoDB

Travaille avec DynamoDB sans la Console

Un client de bureau rapide pour DynamoDB qui exécute le vrai SQL que DynamoDB ne peut pas — JOINs, GROUP BY, agrégations — avec édition visuelle et un agent IA sur tes propres clés Bedrock.

Essai gratuit de 30 jours, sans carte bancaire — ensuite la formule Gratuit, sans limite de durée.