Skip to main content

Operations

Scanamo supports all the DynamoDB operations that interact with individual items in DynamoDB tables:

  • Put for adding a new item, or replacing an existing one
  • Get for retrieving an item by a fully specified key
  • Delete for removing an item
  • Update for updating some portion of the fields of an item, whilst leaving the rest as is
  • Scan for retrieving all elements of a table
  • Query for retrieving all elements with a given hash-key and a range key that matches some criteria

Scanamo also supports batched operations, conditional operations and queries against secondary indexes.

Put and Get

Often when using DynamoDB, the primary use case is simply putting objects into Dynamo and subsequently retrieving them:

import org.scanamo._
import org.scanamo.syntax._
import org.scanamo.generic.auto._
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._

val client = LocalDynamoDB.syncClient()
val scanamo = Scanamo(client)
LocalDynamoDB.createTable(client)("muppets")("name" -> S)

case class Muppet(name: String, species: String)
val muppets = Table[Muppet]("muppets")
// muppets: Table[Muppet] = Table("muppets")
scanamo.exec {
for {
_ <- muppets.put(Muppet("Kermit", "Frog"))
_ <- muppets.put(Muppet("Cookie Monster", "Monster"))
_ <- muppets.put(Muppet("Miss Piggy", "Pig"))
kermit <- muppets.get("name" === "Kermit")
} yield kermit
}
// res1: Option[Either[DynamoReadError, Muppet]] = Some(
// Right(Muppet("Kermit", "Frog"))
// )

Note that when using Table no operations are actually executed against DynamoDB until exec is called.

Delete

To remove an item in its entirety, we can use delete:

import org.scanamo._
import org.scanamo.syntax._
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._

LocalDynamoDB.createTable(client)("villains")("name" -> S)

case class Villain(name: String, catchphrase: String)
val villains = Table[Villain]("villains")
// villains: Table[Villain] = Table("villains")
scanamo.exec {
for {
_ <- villains.put(Villain("Dalek", "EXTERMINATE!"))
_ <- villains.put(Villain("Cyberman", "DELETE"))
_ <- villains.delete("name" === "Cyberman")
survivors <- villains.scan()
} yield survivors
}
// res3: List[Either[DynamoReadError, Villain]] = List(
// Right(Villain("Dalek", "EXTERMINATE!"))
// )

Update

If you want to change some of the fields of an item, that don't form part of its key, without replacing the item entirely, you can use the update operation:

import org.scanamo._
import org.scanamo.syntax._
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._

LocalDynamoDB.createTable(client)("teams")("name" -> S)

case class Team(name: String, goals: Int, scorers: List[String], mascot: Option[String])
val teamTable = Table[Team]("teams")
// teamTable: Table[Team] = Table("teams")
scanamo.exec {
for {
_ <- teamTable.put(Team("Watford", 1, List("Blissett"), Some("Harry the Hornet")))
updated <- teamTable.update("name" === "Watford",
set("goals", 2) and append("scorers", "Barnes") and remove("mascot"))
} yield updated
}
// res5: Either[DynamoReadError, Team] = Right(
// Team("Watford", 2, List("Blissett", "Barnes"), None)
// )

Which fields are updated can be based on incoming data:

import cats.data.NonEmptyList
import org.scanamo.ops.ScanamoOps
import org.scanamo.DynamoReadError
import org.scanamo.update.UpdateExpression

LocalDynamoDB.createTable(client)("favourites")("name" -> S)

case class Favourites(name: String, colour: String, number: Long)
val favouritesTable = Table[Favourites]("favourites")
// favouritesTable: Table[Favourites] = Table("favourites")

scanamo.exec(favouritesTable.put(Favourites("Alice", "Blue", 42L)))

case class FavouriteUpdate(name: String, colour: Option[String], number: Option[Long])

def updateFavourite(fu: FavouriteUpdate): Option[ScanamoOps[Either[DynamoReadError, Favourites]]] = {
val updates: List[UpdateExpression] = List(
fu.colour.map(c => set("colour", c)),
fu.number.map(n => set("number", n))
).flatten
NonEmptyList.fromList(updates).map(ups =>
favouritesTable.update("name" === fu.name, ups.reduce[UpdateExpression](_ and _))
)
}
import cats.implicits._

val updates = List(
FavouriteUpdate("Alice", Some("Aquamarine"), Some(93L)),
FavouriteUpdate("Alice", Some("Red"), None),
FavouriteUpdate("Alice", None, None)
)
// updates: List[FavouriteUpdate] = List(
// FavouriteUpdate("Alice", Some("Aquamarine"), Some(93L)),
// FavouriteUpdate("Alice", Some("Red"), None),
// FavouriteUpdate("Alice", None, None)
// )

scanamo.exec(
for {
_ <- updates.flatMap(updateFavourite).sequence
result <- favouritesTable.get("name" === "Alice")
} yield result
)
// res8: Option[Either[DynamoReadError, Favourites]] = Some(
// Right(Favourites("Alice", "Red", 93L))
// )

Further examples, showcasing different types of update can be found in the scaladoc for the update method on Table.

Scan

If you want to go through all elements of a table, or index, Scanamo supports scanning it:

import org.scanamo._
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._

LocalDynamoDB.createTable(client)("lines")("mode" -> S, "line" -> S)

case class Transport(mode: String, line: String)
val transportTable = Table[Transport]("lines")
// transportTable: Table[Transport] = Table("lines")
val operations = for {
_ <- transportTable.putAll(Set(
Transport("Underground", "Circle"),
Transport("Underground", "Metropolitan"),
Transport("Underground", "Central"),
Transport("Tram", "Croydon Tramlink")
))
allLines <- transportTable.scan()
} yield allLines.toList
// operations: cats.free.Free[ops.ScanamoOpsA, List[Either[DynamoReadError, Transport]]] = FlatMapped(
// FlatMapped(
// Suspend(
// BatchWrite(
// BatchWriteItemRequest(RequestItems={lines=[WriteRequest(PutRequest=PutRequest(Item={line=AttributeValue(S=Circle), mode=AttributeValue(S=Underground)})), WriteRequest(PutRequest=PutRequest(Item={line=AttributeValue(S=Metropolitan), mode=AttributeValue(S=Underground)})), WriteRequest(PutRequest=PutRequest(Item={line=AttributeValue(S=Central), mode=AttributeValue(S=Underground)})), WriteRequest(PutRequest=PutRequest(Item={line=AttributeValue(S=Croydon Tramlink), mode=AttributeValue(S=Tram)}))]})
// )
// ),
// org.scanamo.ScanamoFree$$$Lambda/0x000000c002bdd450@5c7f2d0
// ),
// <function1>
// )

scanamo.exec(operations)
// res10: List[Either[DynamoReadError, Transport]] = List(
// Right(Transport("Tram", "Croydon Tramlink")),
// Right(Transport("Underground", "Central")),
// Right(Transport("Underground", "Circle")),
// Right(Transport("Underground", "Metropolitan"))
// )

Query

Scanamo can be used to perform most queries that can be made against DynamoDB

scanamo.exec {
for {
_ <- transportTable.putAll(Set(
Transport("Underground", "Circle"),
Transport("Underground", "Metropolitan"),
Transport("Underground", "Central")
))
tubesStartingWithC <- transportTable.query("mode" === "Underground" and ("line" beginsWith "C"))
} yield tubesStartingWithC.toList
}
// res11: List[Either[DynamoReadError, Transport]] = List(
// Right(Transport("Underground", "Central")),
// Right(Transport("Underground", "Circle"))
// )