So far, we have constructed queries that return single-column results. These results were mapped to Scala types. But how can we deal with multi-column queries?
In this section, we'll see what alternatives doobie offers us to work with multi-column queries.
As in previous sections, we'll keep working with the 'country' table:
code name population gnp
"DEU" "Germany" 82164700 2133367.00
"ESP" "Spain" 39441700 null
"FRA" "France", 59225700 1424285.00
"GBR" "United Kingdom" 59623400 1378330.00
"USA" "United States of America" 278357000 8510700.00
To make simpler the code we built a method which prepares the database, makes the query and transacts it all:
def transactorBlock[A](f: => ConnectionIO[A]): IO[A] =
transactor.use((createCountryTable *> insertCountries(countries) *> f).transact[IO])
We can select multiple columns and map them to a tuple. The gnp
column in our table is
nullable so we’ll select that one into an Option[Double]
.
val (name, population, gnp) =
transactorBlock {
sql"select name, population, gnp from country where code = 'ESP'"
.query[(String, Int, Option[Double])]
.unique
}.unsafeRunSync()
gnp should be(res0)
doobie automatically supports row mappings for atomic column types, as well as options,
tuples, HList
s, shapeless records, and case classes thereof. So let’s try the same query
with an HList
type CountryHListType = String :: Int :: Option[Double] :: HNil
val hlist: CountryHListType =
transactorBlock {
sql"select name, population, gnp from country where code = 'FRA'"
.query[CountryHListType]
.unique
}.unsafeRunSync()
hlist.head should be(res0)
And with a shapeless record:
type Rec = Record.`'name -> String, 'pop -> Int, 'gnp -> Option[Double]`.T
val record: Rec =
transactorBlock {
sql"select name, population, gnp from country where code = 'USA'"
.query[Rec]
.unique
}.unsafeRunSync()
record(Symbol("pop")) should be(res0)
And again, mapping rows to a case class.
case class Country(code: String, name: String, population: Long, gnp: Option[Double])
val country =
transactorBlock {
sql"select code, name, population, gnp from country where name = 'United Kingdom'"
.query[Country]
.unique
}.unsafeRunSync()
country.code should be(res0)
You can also nest case classes, HList
s, shapeless records, and/or tuples arbitrarily as long
as the eventual members are of supported columns types. For instance, here we map the same set
of columns to a tuple of two case classes:
case class Code(code: String)
case class CountryInfo(name: String, pop: Int, gnp: Option[Double])
val (code, country) =
transactorBlock {
sql"select code, name, population, gnp from country where code = 'ESP'"
.query[(Code, CountryInfo)]
.unique
}.unsafeRunSync()
country.name should be(res0)
And just for fun, since the Code
values are constructed from the primary key, let’s turn the
results into a Map
. Trivial but useful.
val notFoundCountry = CountryInfo("Not Found", 0, None)
val countriesMap: Map[Code, CountryInfo] =
transactorBlock {
sql"select code, name, population, gnp from country"
.query[(Code, CountryInfo)]
.to[List]
}.unsafeRunSync().toMap
countriesMap.getOrElse(Code("DEU"), notFoundCountry).name should be(res0)
countriesMap.get(Code("ITA")) should be(res1)