Lightweight AWS DynamoDB mapper for Kotlin written in pure Kotlin.
kDynamoMapper supports only AWS SDK for Java 2.x.
kDynamoMapper maps a data class
to AttributeValue and vice versa.
kDynamoMapper doesn't wrap DynamoDbClient
. You have to use original DynamoDbClient
from AWS SDK for Java 2.0 to work with DynamoDB.
<dependency>
<groupId>pro.jaitl</groupId>
<artifactId>k-dynamo-mapper</artifactId>
<version>version</version>
</dependency>
implementation 'pro.jaitl:k-dynamo-mapper:<version>'
implementation("pro.jaitl:k-dynamo-mapper:<version>")
kDynamoMapper supports only data classes. When a data class contains another data class as a property it will be mapped as well.
val mapper: KDynamoMapper = Mapper()
data class NestedObject(val dataDouble: Double, val dataInstant: Instant)
data class MyData(val id: String, val dataInt: Int, val nested: NestedObject)
data class MyKey(val id: String)
val data = MyData("1", 1234, NestedObject(333.33, Instant.now()))
val dynamoData = mapper.writeObject(data)
val putRequest = PutItemRequest.builder()
.tableName(table.tableName)
.item(dynamoData)
.build()
dynamoDbClient.putItem(putRequest)
val keyValue = mapper.writeObject(MyKey(data.id))
val getRequest = GetItemRequest.builder()
.key(keyValue)
.tableName(table.tableName)
.build()
val result = dynamoDbClient.getItem(getRequest)
val data = mapper.readObject<MyData>(result.item())
// or val data = mapper.readObject(result.item(), MyData::class)
val itemKey = mapper.writeObject(MyKey("1"))
val updatedValues = mapOf(
"dataInt" to updateAttribute(
attribute = mapper.writeValue(4321),
action = AttributeAction.PUT
)
)
val updateRequest = UpdateItemRequest.builder()
.tableName(table.tableName)
.key(itemKey)
.attributeUpdates(updatedValues)
.build()
dynamoDbClient.updateItem(updateRequest)
val itemKey = mapper.writeObject(MyKey("1"))
val newNested = NestedObject(4321.33, Instant.now().plusSeconds(1000))
val updatedValues = mapOf(
"nested" to updateAttribute(
attribute = mapper.writeValue(newNested),
action = AttributeAction.PUT
)
)
val updateRequest = UpdateItemRequest.builder()
.tableName(table.tableName)
.key(itemKey)
.attributeUpdates(updatedValues)
.build()
dynamoDbClient.updateItem(updateRequest)
You can run and play with the examples in integration tests.
ADT are determined by inheritance from a sealed interface/class.
Each ADT has to contain the adt_class_name
field with the original class.
During write, the adt_class_name
field will be created automatically.
To read an ADT it has to contain the adt_class_name
field otherwise the RequiredFieldNotFoundException
exception will be thrown.
sealed class Adt {
data class AdtOne(val int: Int, val string: String) : Adt()
data class AdtTwo(val long: Long, val instant: Instant, val double: Double) : Adt()
}
data class MyKey(val id: String)
data class MyAdtData(val id: String, val adt: Adt)
val data = MyAdtData("1", Adt.AdtOne(1234, "one one"))
val dynamoData = mapper.writeObject(data)
val putRequest = PutItemRequest.builder()
.tableName(table.tableName)
.item(dynamoData)
.build()
dynamoDbClient.putItem(putRequest)
val itemKey = mapper.writeObject(MyKey("1"))
val updatedAdt = Adt.AdtTwo(4321L, Instant.now(), 4444.0)
val updatedValues = mapOf(
"adt" to updateAttribute(
attribute = mapper.writeValue(updatedAdt),
action = AttributeAction.PUT
)
)
val updateRequest = UpdateItemRequest.builder()
.tableName(table.tableName)
.key(itemKey)
.attributeUpdates(updatedValues)
.build()
dynamoDbClient.updateItem(updateRequest)
val keyValue = mapper.writeObject(MyKey("1"))
val getRequest = GetItemRequest.builder()
.key(keyValue)
.tableName(table.tableName)
.build()
val result = dynamoDbClient.getItem(getRequest)
val updatedItem = mapper.readObject<MyAdtData>(result.item())
// or val updatedItem = mapper.readObject(result.item(), MyAdtData::class)
You can run and play with the examples in integration tests.
class SimpleDataType(val instant: Instant)
class ComplexDataType(val string: String, val int: Int, val simpleDataType: SimpleDataType)
data class MyDataClass(
val id: String,
val simpleDataType: SimpleDataType,
val complexDataType: ComplexDataType
)
data class MyDataKey(val id: String)
This converter returns the custom data type as value.
class SimpleDataTypeConverter : TypeConverter<SimpleDataType> {
override fun read(
reader: KDynamoMapperReader,
attr: AttributeValue,
kType: KType
): SimpleDataType {
return SimpleDataType(
instant = reader.readValue(attr)
)
}
override fun write(writer: KDynamoMapperWriter, value: Any, kType: KType): AttributeValue {
val myData = value as SimpleDataType
return writer.writeValue(myData.instant)
}
override fun type(): KClass<SimpleDataType> = SimpleDataType::class
}
This converter returns the custom data type as map.
class ComplexDataTypeConverter : TypeConverter<ComplexDataType> {
override fun read(
reader: KDynamoMapperReader,
attr: AttributeValue,
kType: KType
): ComplexDataType {
val attrMap = attr.m()
return ComplexDataType(
string = reader.readValue(attrMap["string"]!!),
int = reader.readValue(attrMap["int"]!!),
simpleDataType = reader.readValue(attrMap["simpleDataType"]!!)
)
}
override fun write(writer: KDynamoMapperWriter, value: Any, kType: KType): AttributeValue {
val myData = value as ComplexDataType
return mapAttribute(
mapOf(
"string" to writer.writeValue(myData.string),
"int" to writer.writeValue(myData.int),
"simpleDataType" to writer.writeValue(myData.simpleDataType),
)
)
}
override fun type(): KClass<ComplexDataType> = ComplexDataType::class
}
Configure custom converters then union them with default converters.
val customConvertersMap = listOf(SimpleDataTypeConverter(), ComplexDataTypeConverter())
.associateBy { it.type() }
val registry = ConverterRegistry(DEFAULT_CONVERTERS + customConvertersMap)
val mapper = Mapper(registry)
val data = MyDataClass(
id = "1",
simpleDataType = SimpleDataType(Instant.now()),
complexDataType = ComplexDataType("test", 1234, SimpleDataType(Instant.now().plusSeconds(1000)))
)
val dynamoData = mapper.writeObject(data)
val putRequest = PutItemRequest.builder()
.tableName(table.tableName)
.item(dynamoData)
.build()
dynamoDbClient.putItem(putRequest)
val keyValue = mapper.writeObject(MyDataKey(data.id))
val getRequest = GetItemRequest.builder()
.key(keyValue)
.tableName(table.tableName)
.build()
val result = dynamoDbClient.getItem(getRequest)
val actualData = mapper.readObject<MyDataClass>(result.item())
// or val actualData = mapper.readObject(result.item(), MyDataClass::class)
You can run and play with the examples in integration tests.
Please! If you have written a converter for a common data type that will be useful to other users, contribute it back to the project.
- There are several opened issues. When you want to resolve an opened issue don't forget to write about it in the issue.
- If there isn't a needed converter for you feel free to open a new issue then implement and contribute the converter.