Sharing my working answer for posterity. As of Oct 2020, there is a way to query multiple hash keys using a single query using aws-java-sdk-dynamodb-1.11.813.jar
. I had the same requirement where I had to select items based on multiple hash keys(partition keys), and you can relate the requirement with the RDMS scenario, similar to the query select * from photo where id in ('id1','id2','id3')
, here id is the primary key of the photo
table.
Code Snippet
package com.test.demo.dynamodb.entity;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@NoArgsConstructor
@AllArgsConstructor
@lombok.Data
@DynamoDBTable(tableName = "test_photos")
@Builder
public class Photo implements Serializable {
@DynamoDBHashKey
private String id;
private String title;
private String url;
private String thumbnailUrl;
}
- DynamoDB Repository Class
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.KeyPair;
import com.test.demo.dynamodb.entity.Photo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Repository
public class PhotoRepository {
@Autowired
private DynamoDBMapper dynamoDBMapper = null;
public List<Photo> findByIds(Collection<String> photoIds) {
//Constructing `KeyPair` instance and setting the HashKey,
// in this example I have only hash key,
// if you have RangeKey(Sort) you can set that also here using KeyPair#withRangeKey
List<KeyPair> keyPairs = photoIds.stream()
.map(id -> new KeyPair().withHashKey(id))
.collect(Collectors.toList());
//Creating Map where Key as Class<?> and value as a list of created keyPairs
//you can also directly use batchLoad(List<Photo> itemsToGet), the only constraint
//is if you didn't specify the Type as key and simply using the
//DynamoDBMapper#batchLoad(Iterable<? extends Object> itemsToGet)
//then the Type of Iterable should have annotated with @DynamoDBTable
Map<Class<?>, List<KeyPair>> keyPairForTable = new HashMap<>();
keyPairForTable.put(Photo.class, keyPairs);
Map<String, List<Object>> listMap = dynamoDBMapper.batchLoad(keyPairForTable);
//result map contains key as dynamoDBtable name of Photo.class
//entity(test_photo) and values as matching results of given ids
String tableName = dynamoDBMapper.generateCreateTableRequest(Photo.class)
.getTableName();
return listMap.get(tableName).stream()
.map(e -> (Photo) e)
.collect(Collectors.toList());
}
}
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.TableCollection;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.ListTablesRequest;
import com.amazonaws.services.dynamodbv2.model.ListTablesResult;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
import com.test.demo.dynamodb.Application;
import com.test.demo.dynamodb.entity.Photo;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ActiveProfiles("test")
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DynamoDBFindByIdsITest {
@Autowired
private DynamoDBMapper dynamoDBMapper = null;
@Autowired
private DynamoDB dynamoDB = null;
@Autowired
private PhotoRepository photoRepository = null;
@Test
void findByIdsTest() throws InterruptedException {
//Creating dynamodb table if not already exists
createDataTableIfNotExists("test", Photo.class);
int size = 5;
//creating dummy entries for test and persisting and collecting it to
//validate with results
List<Photo> photos = IntStream.range(0, size)
.mapToObj(e -> UUID.randomUUID().toString())
.map(id ->
Photo.builder()
.id(id)
.title("Dummy title")
.url("http://photos.info/" + id)
.thumbnailUrl("http://photos.info/thumbnails/" + id)
.build()
).peek(dynamoDBMapper::save)
.collect(Collectors.toList());
//calling findByIds with the Collection of HashKey ids (Partition Key Ids)
Set<String> photoIds = photos.stream()
.map(Photo::getId)
.collect(Collectors.toSet());
List<Photo> photosResultSet = photoRepository.findByIds(photoIds);
Assertions.assertEquals(size, photosResultSet.size());
//validating returned photoIds with the created Ids
Set<String> resultedPhotoIds = photosResultSet.stream()
.map(Photo::getId)
.collect(Collectors.toSet());
Assertions.assertTrue(photoIds.containsAll(resultedPhotoIds));
}
public <T> void createDataTableIfNotExists(String tablePrefix, Class<T> clazz)
throws InterruptedException {
ListTablesRequest listTablesRequest = new ListTablesRequest();
listTablesRequest.setExclusiveStartTableName(tablePrefix);
TableCollection<ListTablesResult> tables = dynamoDB.listTables();
List<String> tablesList = new ArrayList<>();
tables.forEach((tableResult) -> {
tablesList.add(tableResult.getTableName());
});
String tableName = dynamoDBMapper.generateCreateTableRequest(clazz).getTableName();
if (!tablesList.contains(tableName)) {
CreateTableRequest tableRequest = dynamoDBMapper.generateCreateTableRequest(clazz);
tableRequest.withProvisionedThroughput(new ProvisionedThroughput(5L, 5L));
Table table = dynamoDB.createTable(tableRequest);
table.waitForActive();
}
}
}