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"))
// )