# Table definition
Let's imagine that we want to implement game leaderboard. The leaderboard would contain the user's id and score. Here is how the possible interface for a leaderboard persistence could look like:
trait Ladder[F[_, _]] {
def submitScore(userId: UserId, score: Score): F[QueryFailure, Unit]
def getScores: F[QueryFailure, List[UserWithScore]]
}
final case class UserId(value: UUID) extends AnyVal
final case class Score(value: Long) extends AnyVal
final case class UserWithScore(userId: UserId, score: Score)
final case class QueryFailure(queryName: String, cause: Throwable)
extends RuntimeException(
s"""Query "$queryName" failed with ${cause.getMessage}""",
cause,
)
To define a table you must extend TableDef
trait which has table
and ddl
values. In this particular case, we have a SUPER-simple table without indexes
and with hash key
only.
final class LadderTable(implicit meta: DynamoMeta) extends TableDef {
private[this] val mainKey = DynamoKey(hashKey = DynamoField[UUID]("userId"))
override val table: TableReference = TableReference("d4s-ladder-table", mainKey)
override val ddl: TableDDL = TableDDL(table)
def mainFullKey(userId: UserId): Map[String, AttributeValue] = {
mainKey.bind(userId.value)
}
}
DynamoFiled
is used to describe the type of the key. DynamoKey
type has several constructors. We use the one with hashKey
only, but you could also specify
rangeKey
value. TableReference
is used to describe a table. In the example above we pass the name of the table and key, but you could also specify optional TTL field and prefix like that:
val table = TableReference("name", key, Some("expiredAt"), Some(NamedPrefix("tag", "prefix")))
Obviously, ddl value contains the table's ddl, and we use TableDDL
type to represent this in Scala code. Using TableDDL
you can
describe global and local indexes, provide additional attributes and even set up provisioning throughput. In our example, we just pass a TableReference
to TableDDL
.
More information about a table's ddl and indexes you could find here.
You could notice that we make the key private value and define a helper function to access key indirectly, which is not a necessary thing to do.
One thing we forgot to cover is implicit DynamoMeta parameter. It is required by TableDDL and provides AWS namespace and provisioning config.
We mustn't forget to define codecs that convert data types we wanna store in DB to AWS format. Hopefully, D4S has capabilities to automatically derive codes from user's defined types. This could be made with
D4SCodec.derived
macros that successfully derives an encoder and decoder instances for the user's defined data type.
object LadderTable {
final case class UserIdWithScoreStored(userId: UUID, score: Long){
def toAPI: UserWithScore = UserWithScore(UserId(userId), Score(score))
}
object UserIdWithScoreStored {
implicit val codec: D4SCodec[UserIdWithScoreStored] = D4SCodec.derived[UserIdWithScoreStored]
}
}
By default, D4S relies on Magnolia (opens new window) to derive typeclasses,
but you could also use Circe (opens new window) to do the same. In case you wanna use circe
just
include d4s-circe
module as a dependency for your project.
Now, we are ready to make some queries!!!
← Setup Basic queries →