-
Notifications
You must be signed in to change notification settings - Fork 4
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.
The following seed projects can be used as example on how to use DynamoDB:
Components use DynamoDB need to add the following library to build.sbt
:
libraryDependencies += "com.amazonaws" % "aws-java-sdk-dynamodb" % "1.11.253"
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()
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.
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")
}
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.
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)
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))))
}
}
}