Initial commit
This commit is contained in:
392
skills/aws-sdk-java-v2-dynamodb/SKILL.md
Normal file
392
skills/aws-sdk-java-v2-dynamodb/SKILL.md
Normal 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)
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
```
|
||||
120
skills/aws-sdk-java-v2-dynamodb/references/entity-mapping.md
Normal file
120
skills/aws-sdk-java-v2-dynamodb/references/entity-mapping.md
Normal 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
407
skills/aws-sdk-java-v2-dynamodb/references/testing-strategies.md
Normal file
407
skills/aws-sdk-java-v2-dynamodb/references/testing-strategies.md
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user