Let’s take a look how neat and nice could be code for persisting in Amazon’s DynamoDB service using Spring data dynamodb project. Speaking of aws dynamodb, you can’t avoid to use local version of dynamodb during development cycle. So, we slightly touch this option in this tutorial.

tip

DynamoDb is a NoSQL highly scalable database from Amazon Web Services.

The full example code of the tutorial could be found on github.

Project setup

Easily start the blank spring project:

1
curl 'https://start.spring.io/starter.zip?&type=gradle-project&language=groovy&bootVersion=2.1.3.RELEASE&baseDir=local-dynamodb&groupId=com.relaximus&artifactId=local-dynamodb&name=local-dynamodb&description=Demo+project+for+Spring+Boot&packageName=com.relaximus.localdynamodb&packaging=jar&javaVersion=1.8' -o local-amazondb.zip

tip

You can do it manually on spring initializer.

Unpack archive, which get after curl executed. Now you are ready to go.

Spring data dynamodb

This is an extension of well-known project Spring data, thus it follows all main abstractions of spring data, like CrudRepository for example. It makes the start is super easy to those fellows, who are familiar with Spring data.

Let’s add spring data dynamodb to our project.

1
2
3
4
dependencies {
    ...
    implementation 'com.github.derjust:spring-data-dynamodb:5.1.0'
}

The next thing we need to do is to add the POJO of the object being persisted in dynamodb and its repository.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAutoGeneratedKey
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable

@DynamoDBTable(tableName = "User")
class User {

    @DynamoDBHashKey
    @DynamoDBAutoGeneratedKey
    String id

    String firstName
    String lastName

    User() {}
}
1
2
3
4
5
import org.socialsignin.spring.data.dynamodb.repository.EnableScan
import org.springframework.data.repository.CrudRepository

@EnableScan
interface UserRepository extends CrudRepository<User, String> {}

Connecting to the cloud

Now we need to configure the spring data dynamodb, to let it know how to connect to the dynamodb. Add a new class to the project - DynamoDBConfigProd:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Profile('prod')
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.relaximus.localdynamodb")
class DynamoDBConfigProd {
    @Value('${amazon.dynamodb.accesskey}')
    private String amazonDynamoDBAccessKey
    @Value('${amazon.dynamodb.secretkey}')
    private String amazonDynamoDBSecretKey

    @Bean
    AWSCredentials amazonAWSCredentials() {
        new BasicAWSCredentials(amazonDynamoDBAccessKey, amazonDynamoDBSecretKey)
    }

    @Bean
    AmazonDynamoDB amazonDynamoDB() {
        AmazonDynamoDBClientBuilder.standard()
                .withCredentials(amazonAWSCredentialsProvider())
                .withRegion(Regions.EU_CENTRAL_1)
                .build()
    }
    @Bean
    DynamoDB dynamoDB() {
        new DynamoDB(amazonDynamoDB())
    }

    AWSCredentialsProvider amazonAWSCredentialsProvider() {
        new AWSStaticCredentialsProvider(amazonAWSCredentials())
    }
}

This is a Spring configuration class. EnableDynamoDBRepositories annotation should point to the repository, which we created. Also this configuration is for using real dynamodb in the aws, so let’s mark it as prod profile.

In this configuration we are creating the bean of DynamoDB, providing aws credentials for connection. The aws key/secret pair should be added to application.yml file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
spring:
    data:
        dynamodb:
            entity2ddl:
                auto: create-only
---
spring:
    profiles: prod
amazon:
    dynamodb:
        accesskey: AKIAJFGUG2EW2GGTZXRQ
        secretkey: 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

Also, you can notice, that we are enabled the schema auto creation option. By this option spring data dynamodb will manage all need preconditions according our model and create the schema objects if needed.

warning

entity2ddl is more for tutorials and playground goals and shouldn’t be used in real production.

Now, we need to run something. Let’s make a simple example - just put some User to the dynamodb and when check how many we have. Add CommandLineRunner to the main Application class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    @Bean
    CommandLineRunner commandLineRunner (UserRepository userRepository) {
        new CommandLineRunner() {
            @Override
            void run(String... args) throws Exception {
                userRepository.save(new User(firstName: 'Alex', lastName: 'Xela'))
                println "There is ${userRepository.count()} users in the table"
            }
        }
    }

Ok, let’s run it:

1
gradle bootRun -Dspring.profiles.active=prod

note

Probably you will need to pass D-property to the bootRun task like this:

1
bootRun { systemProperties = System.properties }

in build.gradle file

Somewhere in the console you should see:

1
There is 1 users in the table

And every time you run the code, the more users will be in the table. Actually, all the magic already happened - application connected to the aws dynamodb, created the User table and added the item in it. But lets proof that there are some items indeed.

1
aws dynamodb describe-table --table-name User

the response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
    "Table": {
        "AttributeDefinitions": [
            {
                "AttributeName": "id",
                "AttributeType": "S"
            }
        ],
        "TableName": "User",
        "KeySchema": [
            {
                "AttributeName": "id",
                "KeyType": "HASH"
            }
        ],
        "TableStatus": "ACTIVE",
        "CreationDateTime": 1554141367.107,
        "ProvisionedThroughput": {
            "NumberOfDecreasesToday": 0,
            "ReadCapacityUnits": 10,
            "WriteCapacityUnits": 1
        },
        "TableSizeBytes": 126,
        "ItemCount": 2,
        "TableArn": "arn:aws:dynamodb:eu-central-1:...:table/User",
        "TableId": "c024f...aac7"
    }
}

And what items do we have inside?

1
aws dynamodb scan --table-name User

the response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
    "Items": [
        {
            "id": {
                "S": "96a5d6f6-d7b4-40bc-b291-d543787dcd07"
            },
            "firstName": {
                "S": "Alex"
            },
            "lastName": {
                "S": "Xela"
            }
        },
        {
            "id": {
                "S": "981196c2-2509-41dd-b65b-b3e6b38f0bde"
            },
            "firstName": {
                "S": "Alex"
            },
            "lastName": {
                "S": "Xela"
            }
        },
        {
            "id": {
                "S": "74144ee7-9002-4a00-ad49-8e3fc3d0c276"
            },
            "firstName": {
                "S": "Alex"
            },
            "lastName": {
                "S": "Xela"
            }
        }
    ],
    "Count": 3,
    "ScannedCount": 3,
    "ConsumedCapacity": null
}

Pretty much easy, yes? That is how approximately we can use the dynamodb in the project. But during development, it is more convenient to have local instance of dynamodb, so let’s take a look how we can arrange this also.

Local Amazon dynaomdb configuration

First, we would need the separate configuration for that, so let’s add this to the root package of the project:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Profile("!prod")
@Configuration
@EnableDynamoDBRepositories(basePackages = "com.relaximus.localdynamodb")
class DynamoDbConfigDev {
    @Value('${amazon.dynamodb.endpoint}')
    private String amazonDynamoDBEndpoint
    @Value('${amazon.dynamodb.region}')
    private String amazonDynamoDBRegion

    @Bean(destroyMethod = "shutdown")
    AmazonDynamoDB amazonDynamoDB(Environment env) {
        System.setProperty("sqlite4java.library.path", "build/libs")

        final String[] localArgs = ["-inMemory"].toArray()
        DynamoDBProxyServer server = ServerRunner.createServerFromCommandLineArgs(localArgs)
        server.start()

        def auth = new BasicAWSCredentials("fakeKey", "fakeSecret")
        AmazonDynamoDBClientBuilder
                .standard().withCredentials(new AWSStaticCredentialsProvider(auth))
                .withEndpointConfiguration(
                new AwsClientBuilder
                        .EndpointConfiguration(amazonDynamoDBEndpoint, amazonDynamoDBRegion))
                .build()
    }

    @Bean
    DynamoDB dynamoDB() {
        new DynamoDB(amazonDynamoDB())
    }
}

This configuration available for all profiles, except the prod. The main part here is the starting server by means of ServerRunner.createServerFromCommandLineArgs(). Looks simple, but there are a few gotcha’s, which you need to solve before start using the local dynamodb.

First the fakekey/fakeSecret is not a garbage here, they are realy needed to local dynamodb worked. For some reason it requires AWS credentials, even though they are not needed. As you understand, there is no real fakekey and fakeSecret.

The second thing, which should be taken into account is the native libraries, on which the local dynamodb depends. This is part of internal implementation of dynamodb: it runs the sql-lite inside, but with the DynamoDB API exposed to the user. So it should be clear, that this approach can’t be used for running production servers, it is only for development lifecycle locally.

Ok, so, why we are talking so much about this internal stuff? Because sometimes it turns so, that the user should take care about the native libs - the simple way to do that is to put them in one place in project build folder and “tell” to the dynamodb where they are. So that’s how we do that:

add this task to build.gradle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
task copyNativeDeps(type: Copy) {
    from(configurations.compileClasspath) {
        include '*.dll'
        include '*.dylib'
        include '*.so'
    }
    into 'build/libs'
}

bootRun.dependsOn('copyNativeDeps')

and there is already set system property in our config file above:

1
System.setProperty("sqlite4java.library.path", "build/libs")

tip

Of course, you can set the system property in the way, which is more suitable to you project settings.

Oh, almost forget, we added a few properties, which should be added to the application.yml:

1
2
3
4
amazon:
    dynamodb:
        endpoint: http://localhost:8000
        region: eu-central-1

Ok, now we are ready to go further: run

1
gradle bootRun

and see

1
There is 1 users in the table

and this time you will get 1 users each time, because the dynamodb will be created from scratch every time in memory.

Something as conclusion for the tutorial

This was a short tutorial of the way of using the dynamodb in the development. We saw a tool for making a requests to dynamodb in the aws cloud and the way to start Amazon dynamodb locally. Actually, this is a huge topic, the dynamodb tables optimization and designing of the application for the eventual consistency, for example, have been left behind the scene. But this is a good start for quick setting the project as playground with dynamodb, avoiding spending time on solving simple problems with the setup.