Il y a 7 ans -
Temps de lecture 6 minutes
Amazon DynamoDB guidé par les tests
Amazon DynamoDB est un service de base de données NoSQL rapide complètement géré par Amazon.
Afin de réduire le feedback loop de nos changements de code, nous souhaitons mettre en place des tests unitaires. Mais comment peut-on écrire des tests automatisés pour DynamoDB, étant donné qu’il s’agit d’un service cloud ?
Dans cette article nous allons découvrir comment utiliser DynamoDB via le SDK, étape par étape, guidé par les tests.
Démarrage
Notre but principal est d’apprendre à exécuter des opérations CRUD avec DynamoDB.
Amazon propose une version locale de DynamoDB qui implémente la même API, portée par une base de données SQLite.
Commençons par l’accès à DynamoDB. Pour se faire, Amazon met à disposition des utilisateurs un SDK. Dans cet article nous utilisons la version Java. Nous pouvons récupérer le SDK Java ainsi que Junit, Assertj et DynamoDBLocal depuis le repository d’Amazon :
[java]repositories {
mavenCentral()
maven { url ‘http://dynamodb-local.s3-website-us-west-2.amazonaws.com/release’ }
}
dependencies {
compile(‘com.amazonaws:aws-java-sdk:1.10.28’)
testCompile group: ‘junit’, name: ‘junit’, version: ‘4.12’
testCompile(‘org.assertj:assertj-core:3.3.0’)
testCompile(‘com.amazonaws:DynamoDBLocal:1.10.5.1’)
}[/java]
Le DynamoDBLocal peut être démarré comme suit :
[java]@Test
public void startAnInMemoryServer() throws Exception {
final String[] localArgs = {"-inMemory", "-port", "30000"};
final DynamoDBProxyServer server = ServerRunner.createServerFromCommandLineArgs(localArgs);
server.start();
}[/java]
Exécutons le test et vérifions qu’il se termine avec succès :
[java]Initializing DynamoDB Local with the following configuration:
Port: 30000
InMemory: true
DbPath: null
SharedDb: false
shouldDelayTransientStatuses: false
CorsParams: *[/java]
Il nous faut ensuite une table dans laquelle stocker les objets, tels que des User
:
[java]@Test
public void createTable() throws Exception {
DynamoDBMapperConfig config = new DynamoDBMapperConfig(DynamoDBMapperConfig.DEFAULT, new DynamoDBMapperConfig(ConversionSchemas.V2));
AmazonDynamoDBClient dynamoDbClient = new AmazonDynamoDBClient();
DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(dynamoDbClient, config);
CreateTableRequest req = dynamoDBMapper.generateCreateTableRequest(User.class);
DynamoDB dynamoDB = new DynamoDB(dynamoDbClient);
Table table = dynamoDB.createTable(req);
table.waitForActive();
}
public class User {
}[/java]
Plusieurs classes utilitaires sont initialisées pour au final créer la table. Notre premier exemple est basé sur une classe User
.
En exécutant le test nous sommes confrontés à une première erreur :
[java]com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException: Class class fr.xebia.dynamodb.CreateTableTest$User must be annotated with interface com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable[/java]
Le message d’erreur est plutôt clair, il nous faut ajouter l’annotation DynamoDBTable
sur la class User :
[java]@DynamoDBTable(tableName = "User")
public class User {
}[/java]
Relançons le test :
[java]com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException: Public, zero-parameter hash key property must be annotated with interface com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey[/java]
Encore une annotation manquante. Il s’agit le DynamoDBHashKey
, l’équivalent de la clé primaire dans DynamoDB – nous continuons d’apprendre :
[java]@DynamoDBTable(tableName = "User")
public class User {
@DynamoDBHashKey
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}[/java]
Et maintenant ?
[java]com.amazonaws.AmazonServiceException: 1 validation error detected: Value null at ‘provisionedThroughput’ failed to satisfy constraint: Member must not be null (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException; Request ID: 266TV8IP5LCTO05RTJJ700MK0RVV4KQNSO5AEMVJF66Q9ASUAAJG)[/java]
On découvre que chaque table DynamoDB a besoin d’avoir un provisioned throughput configuré, c’est-à-dire la capacité d’écriture et de lecture par seconde garantie par le service :
[java]…
CreateTableRequest req = dynamoDBMapper.generateCreateTableRequest(User.class);
req.setProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(1L).withWriteCapacityUnits(1L));
…[/java]
Mais quelque chose ne fonctionne toujours pas : cette fois-ci le test attend un bon moment avant d’échouer, en présentant cette erreur cryptique :
[java]The request processing has failed because of an unknown error, exception or failure. (Service: AmazonDynamoDBv2; Status Code: 500; Error Code: InternalFailure; Request ID: 49f3e6cf-93aa-47c6-84bc-399864503368)
com.amazonaws.AmazonServiceException: The request processing has failed because of an unknown error, exception or failure. (Service: AmazonDynamoDBv2; Status Code: 500; Error Code: InternalFailure; Request ID: 49f3e6cf-93aa-47c6-84bc-399864503368)[/java]
En faisant un peu d’investigation, on apprend que DynamoDBLocal n’a pas vraiment démarré, à cause d’un problème de dépendance runtime sur SQLite. Pour satisfaire DynamoDBLocal, il faut paramétrer le java.library.path
, à l’intérieur de notre configuration de build :
[groovy]buildscript {
ext {
runtimeTestLibraries = "$buildDir/runtimeTestLibs"
}
}
// sqlite4java dependencies
tasks.withType(Test) {
systemProperty "java.library.path", runtimeTestLibraries
}
task copyRuntimeLibs(type: Copy) {
into runtimeTestLibraries
from configurations.testRuntime
}
test.dependsOn copyRuntimeLibs[/groovy]
Et enfin un succès !
Create
Le DynamoDBMapper que nous avons vu passer dans la création de la table User
est aussi capable de persister des objets grâce à l’annotation sur les classes :
[java]@Test
public void saveUser() throws Exception {
AmazonDynamoDBClient dynamoDbClient = new AmazonDynamoDBClient();
dynamoDbClient.setEndpoint("http://localhost:30000");
DynamoDBMapperConfig config = new DynamoDBMapperConfig(DynamoDBMapperConfig.DEFAULT, new DynamoDBMapperConfig(ConversionSchemas.V2));
final DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(dynamoDbClient, config);
User user = new User();
user.setId(UUID.randomUUID().toString());
dynamoDBMapper.save(user);
}[/java]
Nous créons un client qui pointe vers notre instance DynamoDBLocal, et ce même client est utilisé par le DynamoDBMapper pour persister notre instance User
.
Read
DynamoDBMapper peut aussi charger le user que nous venons de créer :
[java]@Test
public void loadUser() throws Exception {
User user = dynamoDBMapper.load(User.class, ID);
assertThat(user.getId()).isEqualTo(ID);
}[/java]
On peut ajouter un champ dans notre classe User
:
[java]@DynamoDBTable(tableName = "User")
public class User {
@DynamoDBHashKey
private String id;
private String email;
public User() {}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
}[/java]
Update
Et l’objet peut toujours être persisté sans d’autres modifications :
[java]User user = new User();
user.setId(ID);
user.setEmail(EMAIL_ADDRESS);
dynamoDBMapper.save(user);[/java]
Avec le champ email en place, nous pouvons lancer une requête, mais il faut d’abord créer un index sur ce champ.
[java]@DynamoDBIndexHashKey(globalSecondaryIndexName = "email_index")
private String email;[/java]
Encore une annotation : les globalSecondaryIndex
peuvent être rajoutés pendant ou même après la création d’un table, chose qui n’est pas possible avec les local secondary indexes.
Nous rajoutons aussi ce code à la création de la table :
[java]req.getGlobalSecondaryIndexes().forEach(gsi -> {
gsi.setProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(1L).withWriteCapacityUnits(1L));
gsi.setProjection(new Projection().withProjectionType(ProjectionType.ALL));
});[/java]
On doit fournir à Amazon un ProvisionedThroughput
et de plus il faut spécifier quels champs sont projetés / incluses dans la résultat.
Read again !
Nous pouvons donc passer à une requête un peu plus complexe :
[java]@Test
public void queryByEmail() throws Exception {
String emailIndex = "email_index";
DynamoDBQueryExpression<User> queryExpression = new DynamoDBQueryExpression<User>()
.withIndexName(emailIndex)
.withConsistentRead(false)
.withKeyConditionExpression("email" + " = :email")
.withExpressionAttributeValues(new HashMap<String, AttributeValue>() {
{
put(":email", new AttributeValue(EMAIL_ADDRESS));
}
});
PaginatedQueryList<User> result = dynamoDBMapper.query(User.class, queryExpression);
assertThat(result).isNotEmpty();
}[/java]
Dans la query expression nous spécifions quel index utiliser, que nous acceptons de la consistance à terme, et enfin la valeur de l’attribut email
.
Delete
Je pense que vous pouvez imaginer comment supprimer un User
avec DynamoDB… :)
Conclusion
Nous avons découvert comment utiliser DynamoDB via le SDK, grâce aux tests automatisés et DynamoDBLocal.
Les tests vont continuer à vérifier que les objets peuvent être persistés et chargés face aux évolutions du code, tout cela sans accumuler des coûts d’utilisation de services AWS.
Vous pouvez retrouver le code complet de cet article sur github.
Commentaire