This repository is an example on how to integrate DynamoDB with an Spring application. First thing is to setup a local DynamoDB instance to test things out.
To setup DynamoDB locally, you can create a Docker compose file
with the amazon/dynamodb-local
image.
version: '3.8'
services:
dynamodb-local:
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
image: "amazon/dynamodb-local:latest"
container_name: dynamodb-local
ports:
- "8000:8000"
volumes:
- "./docker/dynamodb:/home/dynamodblocal/data"
working_dir: /home/dynamodblocal
You can then spin it up with the command docker-compose up -d
.
The Spring Boot application can be created with the Spring Initializr https://start.spring.io/
To add DynamoDB support you first need to add some dependencies to the Gradle build file.
implementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.696'
implementation 'com.github.derjust:spring-data-dynamodb:5.1.0'
After you have added and loaded the dependencies, you can create a DynamoDBConfig
configuration in your project.
@Configuration
@EnableDynamoDBRepositories(basePackages = "de.joshuaw.mmatechniques")
public class DynamoDBConfig {
@Value("${amazon.dynamodb.endpoint}")
private String amazonDynamoDBEndpoint;
@Value("${amazon.aws.accesskey}")
private String amazonAWSAccessKey;
@Value("${amazon.aws.secretkey}")
private String amazonAWSSecretKey;
@Bean
public AmazonDynamoDB amazonDynamoDB() {
AmazonDynamoDBClientBuilder builder = AmazonDynamoDBClientBuilder.standard();
builder.setCredentials(new AWSStaticCredentialsProvider(amazonAWSCredentials()));
if (StringUtils.hasText(amazonDynamoDBEndpoint)) {
builder.setEndpointConfiguration(
new EndpointConfiguration(
amazonDynamoDBEndpoint,
Regions.EU_WEST_1.getName()
)
);
}
return builder.build();
}
@Bean
public AWSCredentials amazonAWSCredentials() {
return new BasicAWSCredentials(amazonAWSAccessKey, amazonAWSSecretKey);
}
@Bean(name = "mvcHandlerMappingIntrospectorCustom")
public HandlerMappingIntrospector mvcHandlerMappingIntrospectorCustom() {
return new HandlerMappingIntrospector();
}
}
As you can see, there are some application properties that need to be set for
the connection to the DynamoDB instance. In the src/main/resources/application.properties
you need to set:
amazon.dynamodb.endpoint=http://localhost:8000/
amazon.aws.accesskey=key
amazon.aws.secretkey=key2
For production use, you would need to change this to point to the real DynamoDB endpoint and you would need to change the access to the Credentials from static to load it from environment variables.
With the configuration set, we can then create an entity.
When we want to create an entity, we need to configure it to be compatible with DynamoDB.
We have to set the @DynamoDBTable
annotation and we need to set which attribute is the @DynamoDBHashKey
.
In this repository I have the following example
@NoArgsConstructor
@Setter
@DynamoDBTable(tableName = "Techniques")
public class Technique {
private String title;
private String id;
private String description;
private List<String> videoLinks;
@DynamoDBHashKey
@DynamoDBAutoGeneratedKey
public String getId() {
return id;
}
@DynamoDBAttribute
public String getTitle() {
return title;
}
@DynamoDBAttribute
public String getDescription() {
return description;
}
@DynamoDBAttribute
public List<String> getVideoLinks() {
return videoLinks;
}
public Technique(final String title) {
this.title = title;
}
}
Now that we have the entity, we can setup the repository.
Creating the repository is really simple. See the example from this repository.
package de.joshuaw.mmatechniques;
import java.util.Optional;
import org.socialsignin.spring.data.dynamodb.repository.EnableScan;
import org.springframework.data.repository.CrudRepository;
@EnableScan
public interface TechniqueRepository extends
CrudRepository<Technique, String> {
Optional<Technique> findById(String id);
}
Note that we use the @EnableScan
annotation. That's all for setting it up!
Now that we have set everything up, we want to test if the repository integration works.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MmaTechniquesApplication.class)
@WebAppConfiguration
@ActiveProfiles("local")
@TestPropertySource(properties = {
"amazon.dynamodb.endpoint=http://localhost:8000/",
"amazon.aws.accesskey=test1",
"amazon.aws.secretkey=test231" })
public class TechniqueRepositoryIntegrationTest {
private DynamoDBMapper dynamoDBMapper;
@Autowired
private AmazonDynamoDB amazonDynamoDB;
@Autowired
TechniqueRepository repository;
@Before
public void setup() throws Exception {
dynamoDBMapper = new DynamoDBMapper(amazonDynamoDB);
try {
CreateTableRequest tableRequest = dynamoDBMapper
.generateCreateTableRequest(Technique.class);
tableRequest.setProvisionedThroughput(
new ProvisionedThroughput(1L, 1L));
amazonDynamoDB.createTable(tableRequest);
} catch(final ResourceInUseException e) {
// Do nothing because table exists
}
dynamoDBMapper.batchDelete(
repository.findAll());
}
@Test
public void givenItemWithTitle_whenRunFindAll_thenItemIsFound() {
Technique technique = new Technique("Kimura");
repository.save(technique);
List<Technique> result = (List<Technique>) repository.findAll();
assertThat(result.size(), is(greaterThan(0)));
}
}
I also included a small frontend and a controller in this example, to make
it a "real world" example. You can check out the frontend in the frontend
folder. It is a small React + Vite frontend.
The controller for the application looks like this:
@RestController
@RequiredArgsConstructor
public class TechniqueController {
private final TechniqueRepository repository;
@GetMapping("/techniques")
public ResponseEntity<List<Technique>> getTechniques() {
final List<Technique> list = new ArrayList<>();
repository.findAll().forEach(list::add);
return ResponseEntity.ok(list);
}
@GetMapping("/techniques/{id}")
public ResponseEntity<Technique> getTechnique(@PathVariable String id) {
final Optional<Technique> technique = repository.findById(id);
return technique.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.noContent().build());
}
@PostMapping("/techniques")
public ResponseEntity<String> addTechnique(@RequestBody Technique technique) {
final String id = repository.save(technique).getId();
return ResponseEntity.ok(id);
}
@PatchMapping("/techniques/{id}")
public ResponseEntity<Technique> updateTechnique(@PathVariable String id, @RequestBody Technique techniqueUpdate) {
final Optional<Technique> techniqueOptional = repository.findById(id);
if(techniqueOptional.isPresent()) {
final Technique technique = techniqueOptional.get();
technique.setTitle(techniqueUpdate.getTitle());
technique.setDescription(techniqueUpdate.getDescription());
technique.setVideoLinks(techniqueUpdate.getVideoLinks());
return ResponseEntity.ok(repository.save(technique));
}
return ResponseEntity.noContent().build();
}
@DeleteMapping("/techniques/{id}")
public ResponseEntity<Technique> removeTechnique(@PathVariable String id) {
final Optional<Technique> technique = repository.findById(id);
if(technique.isPresent()) {
repository.delete(technique.get());
return ResponseEntity.ok().build();
}
return ResponseEntity.noContent().build();
}
}
The original article that I used for this example was: https://www.baeldung.com/spring-data-dynamodb
I had to adjust some things to make it up to date and I added the controller and the frontend.