Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:30 +08:00
commit 171acedaa4
220 changed files with 85967 additions and 0 deletions

View File

@@ -0,0 +1,392 @@
---
name: aws-sdk-java-v2-dynamodb
description: Amazon DynamoDB patterns using AWS SDK for Java 2.x. Use when creating, querying, scanning, or performing CRUD operations on DynamoDB tables, working with indexes, batch operations, transactions, or integrating with Spring Boot applications.
category: aws
tags: [aws, dynamodb, java, sdk, nosql, database]
version: 1.1.0
allowed-tools: Read, Write, Bash
---
# AWS SDK for Java 2.x - Amazon DynamoDB
## When to Use
Use this skill when:
- Creating, updating, or deleting DynamoDB tables
- Performing CRUD operations on DynamoDB items
- Querying or scanning tables
- Working with Global Secondary Indexes (GSI) or Local Secondary Indexes (LSI)
- Implementing batch operations for efficiency
- Using DynamoDB transactions
- Integrating DynamoDB with Spring Boot applications
- Working with DynamoDB Enhanced Client for type-safe operations
## Dependencies
Add to `pom.xml`:
```xml
<!-- Low-level DynamoDB client -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb</artifactId>
</dependency>
<!-- Enhanced client (recommended) -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>dynamodb-enhanced</artifactId>
</dependency>
```
## Client Setup
### Low-Level Client
```java
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
DynamoDbClient dynamoDb = DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build();
```
### Enhanced Client (Recommended)
```java
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient;
DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder()
.dynamoDbClient(dynamoDb)
.build();
```
## Entity Mapping
To define DynamoDB entities, use `@DynamoDbBean` annotation:
```java
@DynamoDbBean
public class Customer {
@DynamoDbPartitionKey
private String customerId;
@DynamoDbAttribute("customer_name")
private String name;
private String email;
@DynamoDbSortKey
private String orderId;
// Getters and setters
}
```
For complex entity mapping with GSIs and custom converters, see [Entity Mapping Reference](references/entity-mapping.md).
## CRUD Operations
### Basic Operations
```java
// Create or update item
DynamoDbTable<Customer> table = enhancedClient.table("Customers", TableSchema.fromBean(Customer.class));
table.putItem(customer);
// Get item
Customer result = table.getItem(Key.builder().partitionValue(customerId).build());
// Update item
return table.updateItem(customer);
// Delete item
table.deleteItem(Key.builder().partitionValue(customerId).build());
```
### Composite Key Operations
```java
// Get item with composite key
Order order = table.getItem(Key.builder()
.partitionValue(customerId)
.sortValue(orderId)
.build());
```
## Query Operations
### Basic Query
```java
import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
QueryConditional queryConditional = QueryConditional
.keyEqualTo(Key.builder()
.partitionValue(customerId)
.build());
List<Order> orders = table.query(queryConditional).items().stream()
.collect(Collectors.toList());
```
### Advanced Query with Filters
```java
import software.amazon.awssdk.enhanced.dynamodb.Expression;
Expression filter = Expression.builder()
.expression("status = :pending")
.putExpressionValue(":pending", AttributeValue.builder().s("PENDING").build())
.build();
List<Order> pendingOrders = table.query(r -> r
.queryConditional(queryConditional)
.filterExpression(filter))
.items().stream()
.collect(Collectors.toList());
```
For detailed query patterns, see [Advanced Operations Reference](references/advanced-operations.md).
## Scan Operations
```java
// Scan all items
List<Customer> allCustomers = table.scan().items().stream()
.collect(Collectors.toList());
// Scan with filter
Expression filter = Expression.builder()
.expression("points >= :minPoints")
.putExpressionValue(":minPoints", AttributeValue.builder().n("1000").build())
.build();
List<Customer> vipCustomers = table.scan(r -> r.filterExpression(filter))
.items().stream()
.collect(Collectors.toList());
```
## Batch Operations
### Batch Get
```java
import software.amazon.awssdk.enhanced.dynamodb.model.*;
List<Key> keys = customerIds.stream()
.map(id -> Key.builder().partitionValue(id).build())
.collect(Collectors.toList());
ReadBatch.Builder<Customer> batchBuilder = ReadBatch.builder(Customer.class)
.mappedTableResource(table);
keys.forEach(batchBuilder::addGetItem);
BatchGetResultPageIterable result = enhancedClient.batchGetItem(r ->
r.addReadBatch(batchBuilder.build()));
List<Customer> customers = result.resultsForTable(table).stream()
.collect(Collectors.toList());
```
### Batch Write
```java
WriteBatch.Builder<Customer> batchBuilder = WriteBatch.builder(Customer.class)
.mappedTableResource(table);
customers.forEach(batchBuilder::addPutItem);
enhancedClient.batchWriteItem(r -> r.addWriteBatch(batchBuilder.build()));
```
## Transactions
### Transactional Write
```java
enhancedClient.transactWriteItems(r -> r
.addPutItem(customerTable, customer)
.addPutItem(orderTable, order));
```
### Transactional Read
```java
TransactGetItemsEnhancedRequest request = TransactGetItemsEnhancedRequest.builder()
.addGetItem(customerTable, customerKey)
.addGetItem(orderTable, orderKey)
.build();
List<Document> results = enhancedClient.transactGetItems(request);
```
## Spring Boot Integration
### Configuration
```java
@Configuration
public class DynamoDbConfiguration {
@Bean
public DynamoDbClient dynamoDbClient() {
return DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build();
}
@Bean
public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) {
return DynamoDbEnhancedClient.builder()
.dynamoDbClient(dynamoDbClient)
.build();
}
}
```
### Repository Pattern
```java
@Repository
public class CustomerRepository {
private final DynamoDbTable<Customer> customerTable;
public CustomerRepository(DynamoDbEnhancedClient enhancedClient) {
this.customerTable = enhancedClient.table("Customers", TableSchema.fromBean(Customer.class));
}
public void save(Customer customer) {
customerTable.putItem(customer);
}
public Optional<Customer> findById(String customerId) {
Key key = Key.builder().partitionValue(customerId).build();
return Optional.ofNullable(customerTable.getItem(key));
}
}
```
For comprehensive Spring Boot integration patterns, see [Spring Boot Integration Reference](references/spring-boot-integration.md).
## Testing
### Unit Testing with Mocks
```java
@ExtendWith(MockitoExtension.class)
class CustomerServiceTest {
@Mock
private DynamoDbClient dynamoDbClient;
@Mock
private DynamoDbEnhancedClient enhancedClient;
@Mock
private DynamoDbTable<Customer> customerTable;
@InjectMocks
private CustomerService customerService;
@Test
void saveCustomer_ShouldReturnSavedCustomer() {
// Arrange
when(enhancedClient.table(anyString(), any(TableSchema.class)))
.thenReturn(customerTable);
Customer customer = new Customer("123", "John Doe", "john@example.com");
// Act
Customer result = customerService.saveCustomer(customer);
// Assert
assertNotNull(result);
verify(customerTable).putItem(customer);
}
}
```
### Integration Testing with LocalStack
```java
@Testcontainers
@SpringBootTest
class DynamoDbIntegrationTest {
@Container
static LocalStackContainer localstack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:3.0"))
.withServices(LocalStackContainer.Service.DYNAMODB);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("aws.endpoint",
() -> localstack.getEndpointOverride(LocalStackContainer.Service.DYNAMODB).toString());
}
@Autowired
private DynamoDbEnhancedClient enhancedClient;
@Test
void testCustomerCRUDOperations() {
// Test implementation
}
}
```
For detailed testing strategies, see [Testing Strategies](references/testing-strategies.md).
## Best Practices
1. **Use Enhanced Client**: Provides type-safe operations with less boilerplate
2. **Design partition keys carefully**: Distribute data evenly across partitions
3. **Use composite keys**: Leverage sort keys for efficient queries
4. **Create GSIs strategically**: Support different access patterns
5. **Use batch operations**: Reduce API calls for multiple items
6. **Implement pagination**: For large result sets use pagination
7. **Use transactions**: For operations that must be atomic
8. **Avoid scans**: Prefer queries with proper indexes
9. **Handle conditional writes**: Prevent race conditions
10. **Use proper error handling**: Handle exceptions like `ProvisionedThroughputExceeded`
## Common Patterns
### Conditional Operations
```java
PutItemEnhancedRequest request = PutItemEnhancedRequest.builder(table)
.item(customer)
.conditionExpression("attribute_not_exists(customerId)")
.build();
table.putItemWithRequestBuilder(request);
```
### Pagination
```java
ScanEnhancedRequest request = ScanEnhancedRequest.builder()
.limit(100)
.build();
PaginatedScanIterable<Customer> results = table.scan(request);
results.stream().forEach(page -> {
// Process each page
});
```
## Performance Considerations
- Monitor read/write capacity units
- Implement exponential backoff for retries
- Use proper pagination for large datasets
- Consider eventual consistency for reads
- Use `ReturnConsumedCapacity` to monitor capacity usage
## Related Skills
- `aws-sdk-java-v2-core `- Core AWS SDK patterns
- `spring-data-jpa` - Alternative data access patterns
- `unit-test-service-layer` - Service testing patterns
- `unit-test-wiremock-rest-api` - Testing external APIs
## References
- [AWS DynamoDB Documentation](https://docs.aws.amazon.com/dynamodb/)
- [AWS SDK for Java Documentation](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/)
- [DynamoDB Examples](https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javav2/example_code/dynamodb)
- [LocalStack for Testing](https://docs.localstack.cloud/user-guide/aws/)
For detailed implementations, see the references folder:
- [Entity Mapping Reference](references/entity-mapping.md)
- [Advanced Operations Reference](references/advanced-operations.md)
- [Spring Boot Integration Reference](references/spring-boot-integration.md)
- [Testing Strategies](references/testing-strategies.md)

View File

@@ -0,0 +1,195 @@
# Advanced Operations Reference
This document covers advanced DynamoDB operations and patterns.
## Query Operations
### Key Conditions
#### Key.equalTo()
```java
QueryConditional equalTo = QueryConditional
.keyEqualTo(Key.builder()
.partitionValue("customer123")
.build());
```
#### Key.between()
```java
QueryConditional between = QueryConditional
.sortBetween(
Key.builder().partitionValue("customer123").sortValue("2023-01-01").build(),
Key.builder().partitionValue("customer123").sortValue("2023-12-31").build());
```
#### Key.beginsWith()
```java
QueryConditional beginsWith = QueryConditional
.sortKeyBeginsWith(Key.builder()
.partitionValue("customer123")
.sortValue("2023-")
.build());
```
### Filter Expressions
```java
Expression filter = Expression.builder()
.expression("points >= :minPoints AND status = :status")
.putExpressionName("#p", "points")
.putExpressionName("#s", "status")
.putExpressionValue(":minPoints", AttributeValue.builder().n("1000").build())
.putExpressionValue(":status", AttributeValue.builder().s("ACTIVE").build())
.build();
```
### Projection Expressions
```java
Expression projection = Expression.builder()
.expression("customerId, name, email")
.putExpressionName("#c", "customerId")
.putExpressionName("#n", "name")
.putExpressionName("#e", "email")
.build();
```
## Scan Operations
### Pagination
```java
ScanEnhancedRequest request = ScanEnhancedRequest.builder()
.limit(100)
.build();
PaginatedScanIterable<Customer> results = table.scan(request);
results.stream().forEach(page -> {
// Process each page of results
});
```
### Conditional Scan
```java
Expression filter = Expression.builder()
.expression("active = :active")
.putExpressionValue(":active", AttributeValue.builder().bool(true).build())
.build();
return table.scan(r -> r
.filterExpression(filter)
.limit(50))
.items().stream()
.collect(Collectors.toList());
```
## Batch Operations
### Batch Get with Unprocessed Keys
```java
List<Key> keys = customerIds.stream()
.map(id -> Key.builder().partitionValue(id).build())
.collect(Collectors.toList());
ReadBatch.Builder<Customer> batchBuilder = ReadBatch.builder(Customer.class)
.mappedTableResource(table);
keys.forEach(batchBuilder::addGetItem);
BatchGetResultPageIterable result = enhancedClient.batchGetItem(r ->
r.addReadBatch(batchBuilder.build()));
// Handle unprocessed keys
result.stream()
.flatMap(page -> page.unprocessedKeys().entrySet().stream())
.forEach(entry -> {
// Retry logic for unprocessed keys
});
```
### Batch Write with Different Operations
```java
WriteBatch.Builder<Customer> batchBuilder = WriteBatch.builder(Customer.class)
.mappedTableResource(table);
batchBuilder.addPutItem(customer1);
batchBuilder.addDeleteItem(customer2);
batchBuilder.addPutItem(customer3);
enhancedClient.batchWriteItem(r -> r.addWriteBatch(batchBuilder.build()));
```
## Transactions
### Conditional Writes
```java
PutItemEnhancedRequest putRequest = PutItemEnhancedRequest.builder(table)
.item(customer)
.conditionExpression("attribute_not_exists(customerId)")
.build();
table.putItemWithRequestBuilder(putRequest);
```
### Multiple Table Operations
```java
TransactWriteItemsEnhancedRequest request = TransactWriteItemsEnhancedRequest.builder()
.addPutItem(customerTable, customer)
.addPutItem(orderTable, order)
.addUpdateItem(productTable, product)
.addDeleteItem(cartTable, cartKey)
.build();
enhancedClient.transactWriteItems(request);
```
## Conditional Operations
### Condition Expressions
```java
// Check if attribute exists
.setAttribute("conditionExpression", "attribute_not_exists(customerId)")
// Check attribute values
.setAttribute("conditionExpression", "points > :currentPoints")
.setAttribute("expressionAttributeValues", Map.of(
":currentPoints", AttributeValue.builder().n("500").build()))
// Multiple conditions
.setAttribute("conditionExpression", "points > :min AND active = :active")
.setAttribute("expressionAttributeValues", Map.of(
":min", AttributeValue.builder().n("100").build(),
":active", AttributeValue.builder().bool(true).build()))
```
## Error Handling
### Provisioned Throughput Exceeded
```java
try {
table.putItem(customer);
} catch (TransactionCanceledException e) {
// Handle transaction cancellation
} catch (ConditionalCheckFailedException e) {
// Handle conditional check failure
} catch (ResourceNotFoundException e) {
// Handle table not found
} catch (DynamoDbException e) {
// Handle other DynamoDB exceptions
}
```
### Exponential Backoff for Retry
```java
int maxRetries = 3;
long baseDelay = 1000; // 1 second
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
operation();
break;
} catch (ProvisionedThroughputExceededException e) {
long delay = baseDelay * (1 << attempt);
Thread.sleep(delay);
}
}
```

View File

@@ -0,0 +1,120 @@
# Entity Mapping Reference
This document provides detailed information about entity mapping in DynamoDB Enhanced Client.
## @DynamoDbBean Annotation
The `@DynamoDbBean` annotation marks a class as a DynamoDB entity:
```java
@DynamoDbBean
public class Customer {
// Class implementation
}
```
## Field Annotations
### @DynamoDbPartitionKey
Marks a field as the partition key:
```java
@DynamoDbPartitionKey
public String getCustomerId() {
return customerId;
}
```
### @DynamoDbSortKey
Marks a field as the sort key (used with composite keys):
```java
@DynamoDbSortKey
@DynamoDbAttribute("order_id")
public String getOrderId() {
return orderId;
}
```
### @DynamoDbAttribute
Maps a field to a DynamoDB attribute with custom name:
```java
@DynamoDbAttribute("customer_name")
public String getName() {
return name;
}
```
### @DynamoDbSecondaryPartitionKey
Marks a field as a partition key for a Global Secondary Index:
```java
@DynamoDbSecondaryPartitionKey(indexNames = "category-index")
public String getCategory() {
return category;
}
```
### @DynamoDbSecondarySortKey
Marks a field as a sort key for a Global Secondary Index:
```java
@DynamoDbSecondarySortKey(indexNames = "category-index")
public BigDecimal getPrice() {
return price;
}
```
### @DynamoDbConvertedBy
Custom attribute conversion:
```java
@DynamoDbConvertedBy(LocalDateTimeConverter.class)
public LocalDateTime getCreatedAt() {
return createdAt;
}
```
## Supported Data Types
The enhanced client automatically handles the following data types:
- String → S (String)
- Integer, Long → N (Number)
- BigDecimal → N (Number)
- Boolean → BOOL
- LocalDateTime → S (ISO-8601 format)
- LocalDate → S (ISO-8601 format)
- UUID → S (String)
- Enum → S (String representation)
- Custom types with converters
## Custom Converters
Create custom converters for complex data types:
```java
public class LocalDateTimeConverter extends AttributeConverter<LocalDateTime, String> {
@Override
public String transformFrom(LocalDateTime input) {
return input.toString();
}
@Override
public LocalDateTime transformTo(String input) {
return LocalDateTime.parse(input);
}
@Override
public AttributeValue transformToAttributeValue(String input) {
return AttributeValue.builder().s(input).build();
}
@Override
public String transformFromAttributeValue(AttributeValue attributeValue) {
return attributeValue.s();
}
}
```

View File

@@ -0,0 +1,377 @@
# Spring Boot Integration Reference
This document provides detailed information about integrating DynamoDB with Spring Boot applications.
## Configuration
### Basic Configuration
```java
@Configuration
public class DynamoDbConfiguration {
@Bean
@Profile("local")
public DynamoDbClient dynamoDbClient() {
return DynamoDbClient.builder()
.region(Region.US_EAST_1)
.build();
}
@Bean
@Profile("prod")
public DynamoDbClient dynamoDbClientProd(
@Value("${aws.region}") String region,
@Value("${aws.accessKeyId}") String accessKeyId,
@Value("${aws.secretAccessKey}") String secretAccessKey) {
return DynamoDbClient.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretAccessKey)))
.build();
}
@Bean
public DynamoDbEnhancedClient dynamoDbEnhancedClient(DynamoDbClient dynamoDbClient) {
return DynamoDbEnhancedClient.builder()
.dynamoDbClient(dynamoDbClient)
.build();
}
}
```
### Properties Configuration
`application-local.properties`:
```properties
aws.region=us-east-1
```
`application-prod.properties`:
```properties
aws.region=us-east-1
aws.accessKeyId=${AWS_ACCESS_KEY_ID}
aws.secretAccessKey=${AWS_SECRET_ACCESS_KEY}
```
## Repository Pattern Implementation
### Base Repository Interface
```java
public interface DynamoDbRepository<T> {
void save(T entity);
Optional<T> findById(Object partitionKey);
Optional<T> findById(Object partitionKey, Object sortKey);
void delete(Object partitionKey);
void delete(Object partitionKey, Object sortKey);
List<T> findAll();
List<T> findAll(int limit);
boolean existsById(Object partitionKey);
boolean existsById(Object partitionKey, Object sortKey);
}
public interface CustomerRepository extends DynamoDbRepository<Customer> {
List<Customer> findByEmail(String email);
List<Customer> findByPointsGreaterThan(Integer minPoints);
}
```
### Generic Repository Implementation
```java
@Repository
public class GenericDynamoDbRepository<T> implements DynamoDbRepository<T> {
private final DynamoDbTable<T> table;
@SuppressWarnings("unchecked")
public GenericDynamoDbRepository(DynamoDbEnhancedClient enhancedClient,
Class<T> entityClass,
String tableName) {
this.table = enhancedClient.table(tableName, TableSchema.fromBean(entityClass));
}
@Override
public void save(T entity) {
table.putItem(entity);
}
@Override
public Optional<T> findById(Object partitionKey) {
Key key = Key.builder().partitionValue(partitionKey).build();
return Optional.ofNullable(table.getItem(key));
}
@Override
public Optional<T> findById(Object partitionKey, Object sortKey) {
Key key = Key.builder()
.partitionValue(partitionKey)
.sortValue(sortKey)
.build();
return Optional.ofNullable(table.getItem(key));
}
@Override
public void delete(Object partitionKey) {
Key key = Key.builder().partitionValue(partitionKey).build();
table.deleteItem(key);
}
@Override
public List<T> findAll() {
return table.scan().items().stream()
.collect(Collectors.toList());
}
@Override
public List<T> findAll(int limit) {
return table.scan(ScanEnhancedRequest.builder().limit(limit).build())
.items().stream()
.collect(Collectors.toList());
}
}
```
### Specific Repository Implementation
```java
@Repository
public class CustomerRepositoryImpl implements CustomerRepository {
private final DynamoDbTable<Customer> customerTable;
public CustomerRepositoryImpl(DynamoDbEnhancedClient enhancedClient) {
this.customerTable = enhancedClient.table(
"Customers",
TableSchema.fromBean(Customer.class));
}
@Override
public List<Customer> findByEmail(String email) {
Expression filter = Expression.builder()
.expression("email = :email")
.putExpressionValue(":email", AttributeValue.builder().s(email).build())
.build();
return customerTable.scan(r -> r.filterExpression(filter))
.items().stream()
.collect(Collectors.toList());
}
@Override
public List<Customer> findByPointsGreaterThan(Integer minPoints) {
Expression filter = Expression.builder()
.expression("points >= :minPoints")
.putExpressionValue(":minPoints", AttributeValue.builder().n(minPoints.toString()).build())
.build();
return customerTable.scan(r -> r.filterExpression(filter))
.items().stream()
.collect(Collectors.toList());
}
}
```
## Service Layer Implementation
### Service with Transaction Management
```java
@Service
@Transactional
public class CustomerService {
private final CustomerRepository customerRepository;
private final OrderRepository orderRepository;
private final DynamoDbEnhancedClient enhancedClient;
public CustomerService(CustomerRepository customerRepository,
OrderRepository orderRepository,
DynamoDbEnhancedClient enhancedClient) {
this.customerRepository = customerRepository;
this.orderRepository = orderRepository;
this.enhancedClient = enhancedClient;
}
public void createCustomerWithOrder(Customer customer, Order order) {
// Use transaction for atomic operation
enhancedClient.transactWriteItems(r -> r
.addPutItem(getCustomerTable(), customer)
.addPutItem(getOrderTable(), order));
}
private DynamoDbTable<Customer> getCustomerTable() {
return enhancedClient.table("Customers", TableSchema.fromBean(Customer.class));
}
private DynamoDbTable<Order> getOrderTable() {
return enhancedClient.table("Orders", TableSchema.fromBean(Order.class));
}
}
```
### Async Operations
```java
@Service
public class AsyncCustomerService {
private final DynamoDbEnhancedClient enhancedClient;
public CompletableFuture<Void> saveCustomerAsync(Customer customer) {
return CompletableFuture.runAsync(() -> {
DynamoDbTable<Customer> table = enhancedClient.table(
"Customers",
TableSchema.fromBean(Customer.class));
table.putItem(customer);
});
}
public CompletableFuture<List<Customer>> findCustomersByPointsAsync(Integer minPoints) {
return CompletableFuture.supplyAsync(() -> {
Expression filter = Expression.builder()
.expression("points >= :minPoints")
.putExpressionValue(":minPoints", AttributeValue.builder().n(minPoints.toString()).build())
.build();
DynamoDbTable<Customer> table = enhancedClient.table(
"Customers",
TableSchema.fromBean(Customer.class));
return table.scan(r -> r.filterExpression(filter))
.items().stream()
.collect(Collectors.toList());
});
}
}
```
## Testing with LocalStack
### Test Configuration
```java
@TestConfiguration
@ContextConfiguration(classes = {LocalStackDynamoDbConfig.class})
public class DynamoDbTestConfig {
@Bean
public DynamoDbClient dynamoDbClient() {
return LocalStackDynamoDbConfig.dynamoDbClient();
}
@Bean
public DynamoDbEnhancedClient dynamoDbEnhancedClient() {
return DynamoDbEnhancedClient.builder()
.dynamoDbClient(dynamoDbClient())
.build();
}
}
@SpringBootTest(classes = {DynamoDbTestConfig.class})
@Import(DynamoDbTestConfig.class)
public class CustomerRepositoryIntegrationTest {
@Autowired
private DynamoDbEnhancedClient enhancedClient;
@BeforeEach
void setUp() {
// Clean up test data
clearTestData();
}
@Test
void testCustomerOperations() {
// Test implementation
}
}
```
### LocalStack Container Setup
```java
public class LocalStackDynamoDbConfig {
@Container
static LocalStackContainer localstack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:3.0"))
.withServices(LocalStackContainer.Service.DYNAMODB);
@Bean
@DynamicPropertySource
public static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("aws.region", () -> Region.US_EAST_1.toString());
registry.add("aws.accessKeyId", () -> localstack.getAccessKey());
registry.add("aws.secretAccessKey", () -> localstack.getSecretKey());
registry.add("aws.endpoint",
() -> localstack.getEndpointOverride(LocalStackContainer.Service.DYNAMODB).toString());
}
@Bean
public DynamoDbClient dynamoDbClient(
@Value("${aws.region}") String region,
@Value("${aws.accessKeyId}") String accessKeyId,
@Value("${aws.secretAccessKey}") String secretAccessKey,
@Value("${aws.endpoint}") String endpoint) {
return DynamoDbClient.builder()
.region(Region.of(region))
.endpointOverride(URI.create(endpoint))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKeyId, secretAccessKey)))
.build();
}
}
```
## Health Check Integration
### Custom Health Indicator
```java
@Component
public class DynamoDbHealthIndicator implements HealthIndicator {
private final DynamoDbClient dynamoDbClient;
public DynamoDbHealthIndicator(DynamoDbClient dynamoDbClient) {
this.dynamoDbClient = dynamoDbClient;
}
@Override
public Health health() {
try {
dynamoDbClient.listTables();
return Health.up()
.withDetail("region", dynamoDbClient.serviceClientConfiguration().region())
.build();
} catch (Exception e) {
return Health.down()
.withException(e)
.build();
}
}
}
```
## Metrics Collection
### Micrometer Integration
```java
@Component
public class DynamoDbMetricsCollector {
private final DynamoDbClient dynamoDbClient;
private final MeterRegistry meterRegistry;
@EventListener
public void handleDynamoDbOperation(DynamoDbOperationEvent event) {
Timer.Sample sample = Timer.start();
sample.stop(Timer.builder("dynamodb.operation")
.tag("operation", event.getOperation())
.tag("table", event.getTable())
.register(meterRegistry));
}
}
public class DynamoDbOperationEvent {
private String operation;
private String table;
private long duration;
// Getters and setters
}
```

View File

@@ -0,0 +1,407 @@
# Testing Strategies for DynamoDB
This document provides comprehensive testing strategies for DynamoDB applications using the AWS SDK for Java 2.x.
## Unit Testing with Mocks
### Mocking DynamoDbClient
```java
@ExtendWith(MockitoExtension.class)
class CustomerServiceTest {
@Mock
private DynamoDbClient dynamoDbClient;
@Mock
private DynamoDbEnhancedClient enhancedClient;
@Mock
private DynamoDbTable<Customer> customerTable;
@InjectMocks
private CustomerService customerService;
@Test
void saveCustomer_ShouldReturnSavedCustomer() {
// Arrange
Customer customer = new Customer("123", "John Doe", "john@example.com");
when(enhancedClient.table(anyString(), any(TableSchema.class)))
.thenReturn(customerTable);
when(customerTable.putItem(customer))
.thenReturn(null);
// Act
Customer result = customerService.saveCustomer(customer);
// Assert
assertNotNull(result);
assertEquals("123", result.getCustomerId());
verify(customerTable).putItem(customer);
}
@Test
void getCustomer_NotFound_ShouldReturnEmpty() {
// Arrange
when(enhancedClient.table(anyString(), any(TableSchema.class)))
.thenReturn(customerTable);
when(customerTable.getItem(any(Key.class)))
.thenReturn(null);
// Act
Optional<Customer> result = customerService.getCustomer("123");
// Assert
assertFalse(result.isPresent());
verify(customerTable).getItem(any(Key.class));
}
}
```
### Testing Query Operations
```java
@Test
void queryCustomersByStatus_ShouldReturnMatchingCustomers() {
// Arrange
List<Customer> mockCustomers = List.of(
new Customer("1", "Alice", "alice@example.com"),
new Customer("2", "Bob", "bob@example.com")
);
DynamoDbTable<Customer> mockTable = mock(DynamoDbTable.class);
DynamoDbIndex<Customer> mockIndex = mock(DynamoDbIndex.class);
QueryEnhancedRequest queryRequest = QueryEnhancedRequest.builder()
.queryConditional(QueryConditional.keyEqualTo(Key.builder()
.partitionValue("ACTIVE")
.build()))
.build();
when(enhancedClient.table("Customers", TableSchema.fromBean(Customer.class)))
.thenReturn(mockTable);
when(mockTable.index("status-index"))
.thenReturn(mockIndex);
when(mockIndex.query(queryRequest))
.thenReturn(PaginatedQueryIterable.from(mock(Customer.class), mock(QueryResponseEnhanced.class)));
QueryResponseEnhanced mockResponse = mock(QueryResponseEnhanced.class);
when(mockResponse.items())
.thenReturn(mockCustomers.stream());
when(mockIndex.query(any(QueryEnhancedRequest.class)))
.thenReturn(PaginatedQueryIterable.from(mock(Customer.class), mockResponse));
// Act
List<Customer> result = customerService.findByStatus("ACTIVE");
// Assert
assertEquals(2, result.size());
verify(mockIndex).query(any(QueryEnhancedRequest.class));
}
```
## Integration Testing with Testcontainers
### LocalStack Setup
```java
@Testcontainers
@SpringBootTest
@AutoConfigureMockMvc
class DynamoDbIntegrationTest {
@Container
static LocalStackContainer localstack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:3.0"))
.withServices(LocalStackContainer.Service.DYNAMODB);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("aws.region", () -> Region.US_EAST_1.toString());
registry.add("aws.accessKeyId", () -> localstack.getAccessKey());
registry.add("aws.secretAccessKey", () -> localstack.getSecretKey());
registry.add("aws.endpoint",
() -> localstack.getEndpointOverride(LocalStackContainer.Service.DYNAMODB).toString());
}
@Autowired
private DynamoDbEnhancedClient enhancedClient;
@BeforeEach
void setup() {
createTestTable();
}
@Test
void testCustomerCRUDOperations() {
// Test create
Customer customer = new Customer("test-123", "Test User", "test@example.com");
enhancedClient.table("Customers", TableSchema.fromBean(Customer.class))
.putItem(customer);
// Test read
Customer retrieved = enhancedClient.table("Customers", TableSchema.fromBean(Customer.class))
.getItem(Key.builder().partitionValue("test-123").build());
assertNotNull(retrieved);
assertEquals("Test User", retrieved.getName());
// Test update
customer.setPoints(1000);
enhancedClient.table("Customers", TableSchema.fromBean(Customer.class))
.putItem(customer);
// Test delete
enhancedClient.table("Customers", TableSchema.fromBean(Customer.class))
.deleteItem(Key.builder().partitionValue("test-123").build());
}
private void createTestTable() {
DynamoDbClient client = DynamoDbClient.builder()
.region(Region.US_EAST_1)
.endpointOverride(localstack.getEndpointOverride(LocalStackContainer.Service.DYNAMODB))
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())))
.build();
CreateTableRequest request = CreateTableRequest.builder()
.tableName("Customers")
.keySchema(KeySchemaElement.builder()
.attributeName("customerId")
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName("customerId")
.attributeType(ScalarAttributeType.S)
.build())
.provisionedThroughput(ProvisionedThroughput.builder()
.readCapacityUnits(5L)
.writeCapacityUnits(5L)
.build())
.build();
client.createTable(request);
waiterForTableActive(client, "Customers");
}
private void waiterForTableActive(DynamoDbClient client, String tableName) {
Waiter waiter = client.waiter();
CreateTableResponse response = client.createTable(request);
waiter.waitUntilTableExists(r -> r
.tableName(tableName)
.maxWait(Duration.ofSeconds(30)));
try {
waiter.waitUntilTableExists(r -> r.tableName(tableName));
} catch (WaiterTimeoutException e) {
throw new RuntimeException("Table creation timed out", e);
}
}
}
```
### Testcontainers with H2 Migration
```java
@SpringBootTest
@Testcontainers
@AutoConfigureDataJpa
class CustomerRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void postgresProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private CustomerRepository customerRepository;
@Autowired
private DynamoDbEnhancedClient dynamoDbClient;
@Test
void testRepositoryWithRealDatabase() {
// Test with real database
Customer customer = new Customer("123", "Test User", "test@example.com");
customerRepository.save(customer);
Customer retrieved = customerRepository.findById("123").orElse(null);
assertNotNull(retrieved);
assertEquals("Test User", retrieved.getName());
}
}
```
## Performance Testing
### Load Testing with Gatling
```java
class CustomerSimulation extends Simulation {
HttpProtocolBuilder httpProtocolBuilder = http
.baseUrl("http://localhost:8080")
.acceptHeader("application/json");
ScenarioBuilder scn = scenario("Customer Operations")
.exec(http("create_customer")
.post("/api/customers")
.body(StringBody(
"""{
"customerId": "test-123",
"name": "Test User",
"email": "test@example.com"
}"""))
.asJson()
.check(status().is(201)))
.exec(http("get_customer")
.get("/api/customers/test-123")
.check(status().is(200)));
{
setUp(
scn.injectOpen(
rampUsersPerSec(10).to(100).during(60),
constantUsersPerSec(100).during(120)
)
).protocols(httpProtocolBuilder);
}
}
```
### Microbenchmark Testing
```java
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class DynamoDbPerformanceBenchmark {
private DynamoDbEnhancedClient enhancedClient;
private DynamoDbTable<Customer> customerTable;
private Customer testCustomer;
@Setup
public void setup() {
enhancedClient = DynamoDbEnhancedClient.builder()
.dynamoDbClient(DynamoDbClient.builder().build())
.build();
customerTable = enhancedClient.table("Customers", TableSchema.fromBean(Customer.class));
testCustomer = new Customer("benchmark-123", "Benchmark User", "benchmark@example.com");
}
@Benchmark
public void testPutItem() {
customerTable.putItem(testCustomer);
}
@Benchmark
public void testGetItem() {
customerTable.getItem(Key.builder().partitionValue("benchmark-123").build());
}
@Benchmark
public void testQuery() {
customerTable.scan().items().stream().collect(Collectors.toList());
}
}
```
## Property-Based Testing
### Using jqwik
```java
@Property
@Report(Reporting.GENERATED)
void customerSerializationShouldBeConsistent(
@ForAll("customers") Customer customer
) {
// When
String serialized = serializeCustomer(customer);
Customer deserialized = deserializeCustomer(serialized);
// Then
assertEquals(customer.getCustomerId(), deserialized.getCustomerId());
assertEquals(customer.getName(), deserialized.getName());
assertEquals(customer.getEmail(), deserialized.getEmail());
}
@Provide
Arbitrary<Customer> customers() {
return Arbitraries.one(
Arbitraries.of("customer-", "user-", "client-").string()
).map(id -> new Customer(
id + Arbitraries.integers().between(1000, 9999).sample(),
Arbitraries.strings().ofLength(10).sample(),
Arbitraries.strings().email().sample()
));
}
```
## Test Data Management
### Test Data Factory
```java
@Component
public class TestDataFactory {
private final DynamoDbEnhancedClient enhancedClient;
@Autowired
public TestDataFactory(DynamoDbEnhancedClient enhancedClient) {
this.enhancedClient = enhancedClient;
}
public Customer createTestCustomer(String id) {
Customer customer = new Customer(
id != null ? id : UUID.randomUUID().toString(),
"Test User",
"test@example.com"
);
customer.setPoints(1000);
customer.setCreatedAt(LocalDateTime.now());
enhancedClient.table("Customers", TableSchema.fromBean(Customer.class))
.putItem(customer);
return customer;
}
public void cleanupTestData() {
// Implementation to clean up test data
}
}
```
### Test Database Configuration
```java
@TestConfiguration
public class TestDataConfig {
@Bean
public TestDataCleaner testDataCleaner() {
return new TestDataCleaner();
}
}
@Component
public class TestDataCleaner {
private final DynamoDbClient dynamoDbClient;
@EventListener(ApplicationReadyEvent.class)
public void cleanup() {
// Clean up test data before each test run
}
}
```