diff --git a/src/main/scala/doobie/DoobieLibrary.scala b/src/main/scala/doobie/DoobieLibrary.scala index 1a2b41c..184199b 100644 --- a/src/main/scala/doobie/DoobieLibrary.scala +++ b/src/main/scala/doobie/DoobieLibrary.scala @@ -15,6 +15,7 @@ object DoobieLibrary extends Library { override def sections: List[Section] = List( ConnectingToDatabaseSection, - SelectingDataSection + SelectingDataSection, + MultiColumnQueriesSection ) } diff --git a/src/main/scala/doobie/DoobieUtils.scala b/src/main/scala/doobie/DoobieUtils.scala index dab5726..6073e1e 100644 --- a/src/main/scala/doobie/DoobieUtils.scala +++ b/src/main/scala/doobie/DoobieUtils.scala @@ -1,7 +1,5 @@ package doobie -import java.util.UUID - import doobie.Model._ import doobie.imports._ import doobie.free.{ drivermanager => FD } diff --git a/src/main/scala/doobie/Model.scala b/src/main/scala/doobie/Model.scala index 2c4ada1..21cdf3d 100644 --- a/src/main/scala/doobie/Model.scala +++ b/src/main/scala/doobie/Model.scala @@ -4,9 +4,13 @@ object Model { case class Country(code: String, name: String, population: Long, gnp: Option[Double]) + case class Code(code: String) + + case class CountryInfo(name: String, pop: Int, gnp: Option[Double]) + val countries = List( Country("DEU", "Germany", 82164700, Option(2133367.00)), - Country("ESP", "Spain", 39441700, Option(553223.00)), + Country("ESP", "Spain", 39441700, None), Country("FRA", "France", 59225700, Option(1424285.00)), Country("GBR", "United Kingdom", 59623400, Option(1378330.00)), Country("USA", "United States of America", 278357000, Option(8510700.00)) diff --git a/src/main/scala/doobie/MultiColumnQueriesSection.scala b/src/main/scala/doobie/MultiColumnQueriesSection.scala new file mode 100644 index 0000000..d267efc --- /dev/null +++ b/src/main/scala/doobie/MultiColumnQueriesSection.scala @@ -0,0 +1,144 @@ +package doobie + +import doobie.DoobieUtils._ +import doobie.Model._ +import doobie.imports._ +import org.scalaexercises.definitions.Section +import org.scalatest.{FlatSpec, Matchers} +import shapeless.record._ +import shapeless.{::, HNil} + +/** + * 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 + * }}} + * + * @param name multi_column_queries + */ +object MultiColumnQueriesSection extends FlatSpec with Matchers with Section { + + /** + * 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]`. + */ + def selectMultipleColumnsUsingTuple(res0: Option[Double]) = { + + val (name, population, gnp) = + sql"select name, population, gnp from country where code = 'ESP'" + .query[(String, Int, Option[Double])] + .unique + .transact(xa) + .run + + 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` + */ + def selectMultipleColumnsUsingHList(res0: String) = { + + type CountryHListType = String :: Int :: Option[Double] :: HNil + + val hlist: CountryHListType = + sql"select name, population, gnp from country where code = 'FRA'" + .query[CountryHListType] + .unique + .transact(xa) + .run + + hlist.head should be(res0) + } + + /** + * And with a shapeless record: + */ + def selectMultipleColumnsUsingRecord(res0: Int) = { + + type Rec = Record.`'name -> String, 'pop -> Int, 'gnp -> Option[Double]`.T + + val record: Rec = + sql"select name, population, gnp from country where code = 'USA'" + .query[Rec] + .unique + .transact(xa) + .run + + record('pop) should be(res0) + } + + /** + * And again, mapping rows to a case class. + * + * {{{ + * case class Country(code: String, name: String, population: Long, gnp: Option[Double]) + * }}} + */ + def selectMultipleColumnsUsingCaseClass(res0: String) = { + + val country = + sql"select code, name, population, gnp from country where name = 'United Kingdom'" + .query[Country] + .unique + .transact(xa) + .run + + 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]) + * }}} + */ + def selectMultipleColumnsUsingNestedCaseClass(res0: String) = { + + val (code, country) = + sql"select code, name, population, gnp from country where code = 'ESP'" + .query[(Code, CountryInfo)] + .unique + .transact(xa) + .run + + 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. + */ + def selectMultipleColumnsUsingMap(res0: String, res1: Option[CountryInfo]) = { + + val notFoundCountry = CountryInfo("Not Found", 0, None) + + val countriesMap: Map[Code, CountryInfo] = + sql"select code, name, population, gnp from country" + .query[(Code, CountryInfo)] + .list + .transact(xa) + .run + .toMap + + countriesMap.getOrElse(Code("DEU"), notFoundCountry).name should be(res0) + countriesMap.get(Code("ITA")) should be(res1) + } +} diff --git a/src/main/scala/doobie/SelectingDataSection.scala b/src/main/scala/doobie/SelectingDataSection.scala index e694374..51389d8 100644 --- a/src/main/scala/doobie/SelectingDataSection.scala +++ b/src/main/scala/doobie/SelectingDataSection.scala @@ -23,7 +23,7 @@ import org.scalatest.{FlatSpec, Matchers} * {{{ * code name population gnp * "DEU" "Germany" 82164700 2133367.00 - * "ESP" "Spain" 39441700 553223.00 + * "ESP" "Spain" 39441700 null * "FRA" "France", 59225700 1424285.00 * "GBR" "United Kingdom" 59623400 1378330.00 * "USA" "United States of America" 278357000 8510700.00 diff --git a/src/test/scala/doobie/MultiColumnQueriesSectionSpec.scala b/src/test/scala/doobie/MultiColumnQueriesSectionSpec.scala new file mode 100644 index 0000000..cd0e484 --- /dev/null +++ b/src/test/scala/doobie/MultiColumnQueriesSectionSpec.scala @@ -0,0 +1,67 @@ +package doobie + +import doobie.Model.CountryInfo +import org.scalacheck.Shapeless._ +import org.scalaexercises.Test +import org.scalatest.Spec +import org.scalatest.prop.Checkers +import shapeless.HNil + +class MultiColumnQueriesSectionSpec extends Spec with Checkers { + + def `select multiple columns using tuple` = { + val validResult: Option[Double] = None + check( + Test.testSuccess( + MultiColumnQueriesSection.selectMultipleColumnsUsingTuple _, + validResult :: HNil + ) + ) + } + + def `select multiple columns using HList` = { + check( + Test.testSuccess( + MultiColumnQueriesSection.selectMultipleColumnsUsingHList _, + "France" :: HNil + ) + ) + } + + def `select multiple columns using Record` = { + check( + Test.testSuccess( + MultiColumnQueriesSection.selectMultipleColumnsUsingRecord _, + 278357000 :: HNil + ) + ) + } + + def `select multiple columns using case class` = { + check( + Test.testSuccess( + MultiColumnQueriesSection.selectMultipleColumnsUsingCaseClass _, + "GBR" :: HNil + ) + ) + } + + def `select multiple columns using nested case class` = { + check( + Test.testSuccess( + MultiColumnQueriesSection.selectMultipleColumnsUsingNestedCaseClass _, + "Spain" :: HNil + ) + ) + } + + def `select multiple columns using map` = { + val validResult: Option[CountryInfo] = None + check( + Test.testSuccess( + MultiColumnQueriesSection.selectMultipleColumnsUsingMap _, + "Germany" :: validResult :: HNil + ) + ) + } +}