doobielib
package doobielib
- Alphabetic
- Public
- Protected
Value Members
- object ConnectingToDatabaseSection extends AnyFlatSpec with Matchers with Section
doobie is a monadic API that provides a number of data types that all work the same way but describe computations in different contexts.
Introduction
doobie is a monadic API that provides a number of data types that all work the same way but describe computations in different contexts.
In the doobie high level API the most common types we will deal with have the form ConnectionIO[A], specifying computations that take place in a context where a java.sql.Connection is available, ultimately producing a value of type A.
doobie programs are values. You can compose small programs to build larger programs. Once you have constructed a program you wish to run, you interpret it into an effectful target monad of your choice (Task or IO for example) and drop it into your main application wherever you like.
First programs
Before we can use doobie we need to import some symbols. We will use package imports here as a convenience; this will give us the most commonly-used symbols when working with the high-level API.
import doobie._ import doobie.implicits._
Let’s also bring in Cats.
import cats._ import cats.effect._ import cats.implicits._
In the doobie high level API the most common types we will deal with have the form
ConnectionIO[A]
, specifying computations that take place in a context where ajava.sql.Connection
is available, ultimately producing a value of typeA
.So let’s start with a
ConnectionIO
program that simply returns a constant.val program1 = 42.pure[ConnectionIO] // program1: ConnectionIO[Int] = Pure(42)
This is a perfectly respectable doobie program, but we can’t run it as-is; we need a
Connection
first. There are several ways to do this, but here let’s use aTransactor
.Note: DriverManagerTransactors have the advantage of no connection pooling and configuration, so are perfect for testing. The main disadvantage is that it is slower than pooling connection managers, no provides upper bound for concurrent connections and executes blocking operations in an unbounded pool of threads. The
doobie-hikari
add-on provides aTransactor
implementation backed by a HikariCP connection pool. The connection pool is a lifetime-managed object that must be shut down cleanly, so it is managed as a Resource.import doobie.hikari._ // Resource yielding a transactor configured with a bounded connect EC and an unbounded // transaction EC. Everything will be closed and shut down cleanly after use. val transactor: Resource[IO, HikariTransactor[IO]] = for { ce <- ExecutionContexts.fixedThreadPool[IO] (32) // our connect EC be <- Blocker[IO] // our blocking EC xa <- HikariTransactor.newHikariTransactor[IO] ( "org.h2.Driver", // driver classname "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", // connect URL "sa", // username "", // password ce, // await connection here be // execute JDBC operations here ) } yield xa
A
Transactor
is a data type that knows how to connect to a database, hand out connections, and clean them up; and with this knowledge it can transformConnectionIO ~> IO
, which gives us a program we can run. Specifically it gives us anIO
that, when run, will connect to the database and execute single transaction.We are using
cats.effect.IO
as our final effect type, but you can use any monadM[_]
givencats.effect.Async[M]
. See Using Your Own Target Monad at the end of this chapter for more details. - object DoobieLibrary extends Library
doobie is a pure functional JDBC layer for Scala.
- object DoobieUtils
- object ErrorHandlingSection extends AnyFlatSpec with Matchers with Section
Exceptions are a fact of life when interacting with databases, and they are largely nondeterministic; whether an operation will succeed or not depends on unpredictable factors like network health, the current contents of tables, locking state, and so on.
About Exceptions
Exceptions are a fact of life when interacting with databases, and they are largely nondeterministic; whether an operation will succeed or not depends on unpredictable factors like network health, the current contents of tables, locking state, and so on. So we must decide whether to compute everything in a disjunction like
EitherT[ConnectionIO, Throwable, A]
or allow exceptions to propagate until they are caught explicitly. doobie adopts the second strategy: exceptions are allowed to propagate and escape unless handled explicitly (exactly as IO works). This means when a doobie action (transformed to some target monad) is executed, exceptions can escape.There are three main types of exceptions that are likely to arise:
1. Various types of
IOException
can happen with any kind of I/O, and these exceptions tend to be unrecoverable.2. Database exceptions, typically as a generic
SQLException
with a vendor-specificSQLState
identifying the specific error, are raised for common situations such as key violations. Some vendors (PostgreSQL for instance) publish a table of error codes, and in these cases doobie can provide a matching set of exception-handling combinators. However in most cases the error codes must be passed down as folklore or discovered by experimentation. There exist the XOPEN and SQL:2003 standards, but it seems that no vendor adheres closely to these specifications. Some of these errors are recoverable and others aren’t.3. doobie will raise an
InvariantViolation
in response to invalid type mappings, unknown JDBC constants returned by drivers, observedNULL
values, and other violations of invariants that doobie assumes. These exceptions indicate programmer error or driver non-compliance and are generally unrecoverable.MonadError and Derived Combinators
All doobie monads have associated instances of the
Async
instance, which extendsMonadError[?[_], Throwable]
. This meansConnectionIO
, etc., have the following primitive operations:attempt
convertsM[A]
intoM[Either[Throwable, A]]
fail
constructs anM[A]
that fails with a providedThrowable
So any doobie program can be lifted into a disjunction simply by adding
.attempt
.val p = 42.pure[ConnectionIO] // p: ConnectionIO[Int] = Pure(42) p.attempt // res0: ConnectionIO[Either[Throwable, Int]] = Suspend( // HandleErrorWith( // FlatMapped(Pure(42), cats.Monad$$Lambda$8968/1197202183@4abff98c), // cats.ApplicativeError$$Lambda$9059/715221672@694b43d5 // ) // )
From the
.attempt
andfail
combinators we can derive many other operations, as described in the Cats documentation. In addition doobie provides the following specialized combinators that only pay attention toSQLException
:attemptSql
is likeattempt
but only trapsSQLException
.attemptSomeSql
traps only specifiedSQLException
s.exceptSql
recovers from a SQLException with a new action.onSqlException
executes an action onSQLException
and discards its result.
And finally we have a set of combinators that focus on SQLStates.
attemptSqlState
is likeattemptSql
but yieldsM[Either[SQLState, A]]
.attemptSomeSqlState
traps only specifiedSQLState
s.exceptSqlState
recovers from aSQLState
with a new action.exceptSomeSqlState
recovers from specifiedSQLState
s with a new action.
- object ErrorHandlingSectionHelpers
- object Model
- object MultiColumnQueriesSection extends AnyFlatSpec with Matchers with Section
So far, we have constructed queries that return single-column results.
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])
- object ParameterizedQueriesSection extends AnyFlatSpec with Matchers with Section
Previously we have worked with static SQL queries where the values used to filter data were hard-coded and didn't change.
Previously we have worked with static SQL queries where the values used to filter data were hard-coded and didn't change.
select code, name, population, gnp from country where code = "GBR"
In this section, we'll learn how to construct parameterized queries.
We’re still playing with the country table, shown here for reference.
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])
- object ParameterizedQueryHelpers
- object SelectingDataSection extends AnyFlatSpec with Matchers with Section
We are going to construct some programs that retrieve data from the database and stream it back, mapping to Scala types on the way.
We are going to construct some programs that retrieve data from the database and stream it back, mapping to Scala types on the way.
We will be playing with the country table that has the following structure:
CREATE TABLE country ( code character(3) NOT NULL, name text NOT NULL, population integer NOT NULL, gnp numeric(10,2) )
For the exercises, the country table will contain:
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
How to select data
As we commented in the previous section, the
sql
string interpolator allows us to create a query to select data from the database.For instance,
sql"select name from country".query[String]
defines aQuery0[String]
, which is a one-column query that maps each returned row to a String..to[List]
is a convenience method that accumulates rows into aList
, in this case yielding aConnectionIO[List[String]]
. It works with any collection type that has aCanBuildFrom
. Similar methods are: -.unique
which returns a single value, raising an exception if there is not exactly one row returned. -.option
which returns anOption
, raising an exception if there is more than one row returned. -.nel
which returns aNonEmptyList
, raising an exception if there are no rows returned. See the Scaladoc for Query0 for more information on these and other methods. - object UpdatesSection extends AnyFlatSpec with Matchers with Section
In this section we examine operations that modify data in the database, and ways to retrieve the results of these updates.
In this section we examine operations that modify data in the database, and ways to retrieve the results of these updates.
Data Definition
It is uncommon to define database structures at runtime, but doobie handles it just fine and treats such operations like any other kind of update. And it happens to be useful here!
Let’s create a new table, which we will use for the exercises to follow. This looks a lot like our prior usage of the
sql
interpolator, but this time we’re usingupdate
rather thanquery
. The.run
method gives aConnectionIO[Int]
that yields the total number of rows modified.val drop: Update0 = sql""" DROP TABLE IF EXISTS person """.update val create: Update0 = sql""" CREATE TABLE person ( id SERIAL, name VARCHAR NOT NULL UNIQUE, age SMALLINT ) """.update
We can compose these and run them together.
(drop, create).mapN(_ + _).transact(xa).unsafeRunSync // res0: Int = 0
Inserting
Inserting is straightforward and works just as with selects. Here we define a method that constructs an
Update0
that inserts a row into theperson
table.def insert1(name: String, age: Option[Short]): Update0 = sql"insert into person (name, age) values ($name, $age)".update
- object UpdatesSectionHelpers