Ich hatte erhebliche Schwierigkeiten bei der Integration von DynamoDB in eines unserer Projekte. Die Haupt Herausforderung war, geeignete Dokumentationen zu finden, da viele Quellen die Integration mit der AWS SDK Version 1.x beschreiben.
Und wie kann man DynamoDB mit der AWS SDK 2.x in ein Spring Boot Projekt integrieren?
Zu Beginn wird der folgende Docker Container gestartet, um eine lokale DynamoDB für Entwicklungszwecke zu haben. Hier ist ein Auszug aus unserer docker-compose.yml
:
1dynamodb:
2 image: amazon/dynamodb-local
3 container_name: sis-dynamodb
4 ports:
5 - "8000:8000"
6 command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ."
7 environment:
8 AWS_ACCESS_KEY_ID: 'DUMMYIDEXAMPLE'
9 AWS_SECRET_ACCESS_KEY: 'DUMMYEXAMPLEKEY'
10 AWS_REGION: 'eu-central-1'
Für die Integration nutzen wir die AWS SDK. Folgender Gradle Import wird hinzugefügt:
1implementation 'software.amazon.awssdk:dynamodb' //general integration
2implementation 'software.amazon.awssdk:dynamodb-enhanced' //enhanced methods to work with the dynamo in a more ORM manner
Die folgende Spring Konfiguration ist notwendig, um lokalen und AWS DynamoDB zu verbinden:
1@Configuration
2@EnableConfigurationProperties(AwsDynamoDBConfig.Config.class)
3public class AwsDynamoDBConfig {
4 private static final String DEV_ACCESS_KEY_ID = "DUMMYIDEXAMPLE";
5 private static final String DEV_SECRET_ACCESS_KEY = "DUMMYEXAMPLEKEY";
6
7 @Bean
8 @Profile("dev")
9 public DynamoDbClient amazonDynamoDbDev(Config config) {
10 return DynamoDbClient.builder()
11 .credentialsProvider(
12 StaticCredentialsProvider.create(AwsBasicCredentials.create(DEV_ACCESS_KEY_ID, DEV_SECRET_ACCESS_KEY)))
13 .endpointOverride(config.url())
14 .region(Region.EU_CENTRAL_1)
15 .build();
16 }
17
18 @Bean
19 @Profile("!dev")
20 public DynamoDbClient amazonDynamoDbProd() {
21 return DynamoDbClient.create();
22 }
23
24 @Bean
25 public DynamoDbEnhancedClient createDynamoDbEnhancedClient(DynamoDbClient client) {
26 return DynamoDbEnhancedClient.builder().dynamoDbClient(client).build();
27 }
28
29 @ConfigurationProperties(prefix = "dynamodb")
30 public record Config(URI url) {}
31}
Da das DynamoDB Schema "schema-less" ist, ist es nicht notwendig, das gesamte Schema vorzugeben. Allerdings muss man eine Tabelle mit Schlüsseln und möglicherweise sekundären Indizes erstellen.
Für die lokale Entwicklung:
1@Service
2@Slf4j
3@RequiredArgsConstructor
4@Profile("dev")
5public class DynamoDBInitService {
6
7 private final DynamoDbClient amazonDynamoDB;
8
9 @PostConstruct
10 public void initialiseTables() {
11 log.info("Initialising DynamoDB tables");
12 String tableName = SatelliteImage.TABLE_NAME;
13 try {
14
15 DescribeTableRequest describeTableRequest =
16 DescribeTableRequest.builder().tableName(tableName).build();
17
18 DescribeTableResponse response = amazonDynamoDB.describeTable(describeTableRequest);
19
20 if (TableStatus.ACTIVE.equals(response.table().tableStatus())) {
21 log.info("Table {} is active", tableName);
22 }
23 } catch (ResourceNotFoundException e) {
24 log.info("Table {} does not exist", tableName);
25 log.info("Creating table {}", tableName);
26
27 CreateTableRequest createTableRequest = CreateTableRequest.builder()
28 .tableName(tableName)
29 .keySchema(KeySchemaElement.builder()
30 .keyType(KeyType.HASH)
31 .attributeName("id")
32 .build())
33 .attributeDefinitions(
34 AttributeDefinition.builder()
35 .attributeName("id")
36 .attributeType(ScalarAttributeType.S)
37 .build(),
38 AttributeDefinition.builder()
39 .attributeName("field_id")
40 .attributeType(ScalarAttributeType.S)
41 .build(),
42 AttributeDefinition.builder()
43 .attributeName("image_from")
44 .attributeType(ScalarAttributeType.S)
45 .build())
46 .globalSecondaryIndexes(GlobalSecondaryIndex.builder()
47 .indexName("FieldIDImageFromDateIndex")
48 .keySchema(
49 KeySchemaElement.builder()
50 .keyType(KeyType.HASH)
51 .attributeName("field_id")
52 .build(),
53 KeySchemaElement.builder()
54 .keyType(KeyType.RANGE)
55 .attributeName("image_from")
56 .build())
57 .projection(Projection.builder()
58 .projectionType(ProjectionType.ALL)
59 .build())
60 .provisionedThroughput(ProvisionedThroughput.builder()
61 .readCapacityUnits(10L)
62 .writeCapacityUnits(10L)
63 .build())
64 .build())
65 .provisionedThroughput(ProvisionedThroughput.builder()
66 .readCapacityUnits(10L)
67 .writeCapacityUnits(10L)
68 .build())
69 .build();
70
71 try {
72 amazonDynamoDB.createTable(createTableRequest);
73 log.info("Table {} created", tableName);
74 } catch (DynamoDbException ex) {
75 log.error("Error creating table {}", tableName, ex);
76 }
77 }
78 }
79}
Auf der AWS Seite wird diese Struktur mittels IaaC beschrieben. In unserem Fall verwenden wir CloudFormation. Hier ein Auszug aus dem CloudFormation Template:
1 SatelliteImageDynamoDB:
2 Type: 'AWS::DynamoDB::Table'
3 Properties:
4 TableName: 'satellite_image'
5 AttributeDefinitions:
6 - AttributeName: "id"
7 AttributeType: "S"
8 - AttributeName: "field_id"
9 AttributeType: "S"
10 - AttributeName: "image_from"
11 AttributeType: "S"
12 KeySchema:
13 - AttributeName: "id"
14 KeyType: "HASH"
15 ProvisionedThroughput:
16 ReadCapacityUnits: 10
17 WriteCapacityUnits: 10
18 GlobalSecondaryIndexes:
19 - IndexName: "FieldIDImageFromDateIndex"
20 KeySchema:
21 - AttributeName: "field_id"
22 KeyType: "HASH"
23 - AttributeName: "image_from"
24 KeyType: "RANGE"
25 Projection:
26 ProjectionType: "ALL"
27 ProvisionedThroughput:
28 ReadCapacityUnits: 10
29 WriteCapacityUnits: 10
Für eine einfachere Handhabung haben wir eine Art "Entity" erstellt, die in DynamoDB gespeichert werden kann:
1@DynamoDbBean
2@Getter
3@Setter
4@Builder
5@AllArgsConstructor
6@NoArgsConstructor
7public class SatelliteImage {
8
9 public static final String TABLE_NAME = "satellite_image";
10
11 private UUID id;
12 private UUID fieldId;
13 private LocalDate imageFrom;
14 private String s3Path;
15 private double cloudCoverage;
16 private Instant creationDate;
17
18 @DynamoDbPartitionKey
19 public UUID getId() {
20 return id;
21 }
22
23 @DynamoDbAttribute("field_id")
24 public UUID getFieldId() {
25 return fieldId;
26 }
27
28 @DynamoDbAttribute("image_from")
29 public LocalDate getImageFrom() {
30 return imageFrom;
31 }
32
33 @DynamoDbAttribute("s3_path")
34 public String getS3Path() {
35 return s3Path;
36 }
37
38 @DynamoDbAttribute("cloud_coverage")
39 public double getCloudCoverage() {
40 return cloudCoverage;
41 }
42
43 @DynamoDbAttribute("creation_date")
44 public Instant getCreationDate() {
45 return creationDate;
46 }
47}
Um solch ein Objekt zu speichern, muss man Folgendes tun:
1 private final DynamoDbEnhancedClient dynamoDbEnhancedClient;
2
3 private void store(SatelliteImage satelliteImage) {
4 DynamoDbTable<SatelliteImage> table = dynamoDbEnhancedClient.table(
5 SatelliteImage.TABLE_NAME, TableSchema.fromClass(SatelliteImage.class));
6
7 PutItemEnhancedRequest<SatelliteImage> putItemRequest = PutItemEnhancedRequest.builder(SatelliteImage.class)
8 .item(satelliteImage)
9 .