Anorm Primer

HKT HKT
Views

Typically, you would read the results of an Anorm query, and map to a already defined type - case class, something like this:

val persons: List[Person] =
  SQL("select id from ....")
    .on(...)
    .getRows()
    .map(toPerson)

final case class Person(id: Int, name: String, weight: Float)

def toPerson(row: Row): Person =
  Person(
    row[Int]("id"),
    row[String]("name"),
    row[Float]("weight"),
  )

There are times when you would want to read a single column. Or perhaps a set of columns for which you don’t have a pre-defined type (like Person above). In such cases, you will toil as follows:

val ids: List[Int] =
SQL("select id from ....")
    .on(...)
    .getRows()
    .map(row => row[Int]("id"))

// or

val ps: List[(Int, String)] =
  SQL("select id from ....")
    .on(...)
    .getRows()
    .map { row =>
      (
        row => row[Int]("id"),
        row => row[String]("name")
      )
    }

Note that the following is different from what we are discussing here.

// NOTE: We are not talking about this !!!
val count: Long = SQL("select count(*) from users").as(scalar[Long].single)

Using core Anorm functions

import anorm.{SQL, SqlParser}, SqlParser.{int, str, to}

val result: List[Int] =
  SQL("select id from persons where ...")
    .on(...)
    .as(int("id"))

val result: List[Int ~ String] =    // Anorm's type `~` for chanining types (much like shapeless HList)
  SQL("select id, name from ....")
    .on(...)
    .as(int("id") ~ str("name"))

Anorm’s parser combinators 101

Extracting a reusable parser for your query

val resultSetParser: ResultSetParser[List[Int ~ String]] = int("id") ~ str("name")
val parser: ResultSetParser[List[(Int, String)]] =
  resultSetParser.map { case n ~ p => (n, p) }

// Shorthand of the above using built-in `flatten` function
val parser: ResultSetParser[List[(Int, String)]] =
  resultSetParser.map(flatten).*

// The finishing touch!
val result: Map[Int, String] =
  SQL(query)
    .on(...)
    .as(parser)  // List[(Int, String)]
    .toMap

A Bit More of Anorms’ Parser Combinators

Anorm provides a bunch of other parser combinators. Check Anorm’s documentation for more details but here is a teaser:

Explicit control of how much to read from the ResultSet

val parser: ResultSetParser[List[(String, Int)]] =
  for {
    name <- str("name")
    age  <- int("age")
  } yield name -> age


val exactlyOne: (String, Int)         = SELECT("select ...").as(parser.single)
val zeroOneOrMore: List[(String, Int) = SQL("select ...").as(parser.*)
val oneOrMore: List[(String, Int)     = SQL("select ...").as(parser.+)

The to combinator to the rescue

import anorm.SqlParser.{ int, str, to }

def show(name: String, age: Int): String =  s"...."

val parser: ResultSetParser[List[(String, Int)]] =
  str("name") ~ int("age").map(to(show _))

Parser using Pattern matching

val parser: ResultSetParser[List[(String, Int, Boolean)]] =
  str("name") ~ int("age") ~ str("millionaire").map {
    case n ~ l ~ "T" => (n, l, true)
    case n ~ l ~ "F" => (n, l, false)
  }.*