Initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user