Skip to content

Working with DynamoDB

Dennis Vriend edited this page Dec 25, 2017 · 1 revision

Working with DynamoDB

DynamoDB is AWS fully managed cloud database service. It supports the key/value model where a value can be stored by means of an identifier called the key. When the id is known, a value can be retrieved very quickly. DynamoDB is n't designed to be a very fast key/value store in the cloud, so its use case is, everywhere you need to store a value with a key, or retrieve a value for a key.

DynamoDB does not support aggregations, for this use case, it is better to use a data store that supports aggregations, and publish the results (value) to a key (id) in DynamoDB to be retrieved by clients.

Seed projects

The following seed projects can be used as example on how to use DynamoDB:

Libraries

Components use DynamoDB need to add the following library to build.sbt:

libraryDependencies += "com.amazonaws" % "aws-java-sdk-dynamodb" % "1.11.253"

Setting up DynamoDB

Lambdas automatically have access to DynamoDB when the execution policy allows it and the default client is used:

import com.amazonaws.services.dynamodbv2.{ AmazonDynamoDB, AmazonDynamoDBClientBuilder }
val db: AmazonDynamoDB = AmazonDynamoDBClientBuilder.defaultClient()

Scoped DynamoDB table names

Most resources in an AWS account have a flat namespace, this means that resources like DynamoDB tables must have a unique name inside a single account. To be able to deploy multiple component in an account that use the same DynamoDB table name, the project uses a 'scope'.

A 'scope' works like a 'namespace' or 'package' in programming languages, it allows to reuse a name by prefixing it with a unique prefix.

sbt-sam manages resource names for us. It uses the sbt name setting and samStage setting to define a scope. All resources are prefixed with [name]-[stage]-[resource-name]. For example, a table called 'person' would become name-stage-person.

A scoped table name is useful, in the source code we can refer to the normal name 'person' and the table will be automatically scoped by the settings of the project.

The setting name is obvious, it is the project name, the setting samStage defines a unique stage for the component. The stage can be an arbitrary text, and defines only a scope for the component. At first glance one would use the scope to define 'dev', 'acc', 'prod' environments. The stage is also useful when defining a single component, that can be composed together. For example, when defining a data-lake, consisting of multiple components, a component has the same project name, but the data lake consists of multiple 'stages', for example, 'part1', 'part2', 'part3' etc.

Resolving scoped table names

The com.github.dnvriend.lambda.SamContext, a case class that is always available inside a Lambda, provides methods that can be used to determine the scoped table name from its short name.

def handle(request: HttpRequest, ctx: SamContext): HttpResponse {
val tableName: String = ctx.dynamoDbTableName("person")
}

Accessing DynamoDB

DynamoDB can be access by means of the Java SDK for DynamoDB or any Scala abstraction like Scanamo. For an example on how to use the Java SDK take a look at the com.github.dnvriend.dynamodb.repo.JsonRepository that is part of the sam-lamba library.

JsonRepository

The JsonRepository, part of the sam-lamba library makes it easy to store JSON in a Lambda Key/Value store. It assumes two attributes, id and json. It uses play-json, a type-class based Json library to marshal and unmarshal values of type A:

def handle(request: HttpRequest, ctx: SamContext): HttpResponse {
val tableName: String = ctx.dynamoDbTableName("person")
val repo = new JsonRepository(tableName, ctx)

JsonRepositoryApiGatewayHandler

The com.github.dnvriend.dynamodb.repo.JsonRepositoryApiGatewayHandler is a combination of the ApiGatewayHandler, that defines a Lambda that handles an Api Event, and a JsonRepository:

object Person {
  implicit val format: Format[Person] = Json.format[Person]
}
final case class Person(name: String, id: Option[String] = None)

@HttpHandler(path = "/person", method = "put")
class CreatePerson extends JsonRepositoryApiGatewayHandler[Person]("people") {
  override def handle(person: Option[Person], repo: JsonRepository, request: HttpRequest, ctx: SamContext): HttpResponse = {
    person.fold(HttpResponse.validationError.withBody(Json.toJson("Could not deserialize person"))) { person =>
      val id: String = repo.id
      repo.put(id, person)
      HttpResponse.ok.withBody(Json.toJson(person.copy(id = Option(id))))
    }
  }
}