Tranzactio
ZIO wrapper around Doobie and Anorm.
Install / Use
/learn @gaelrenoux/TranzactioREADME
TranzactIO
TranzactIO is a ZIO wrapper for some Scala database access libraries (Doobie and Anorm, for now).
If the library comes with an IO monad (like Doobie's ConnectionIO), it lifts it into a ZIO[Connection, E, A].
If the library doesn't have an IO monad to start with (like Anorm), it provides a ZIO[Connection, E, A] for the role.
Note that Connection is not Java's java.sql.Connection, it's a TranzactIO type.
When you're done chaining ZIO instances (containing either queries or whatever code you need), use TranzactIO's Database module to provide the Connection and execute the transaction.
Database can also provide a Connection in auto-commit mode, without a transaction.
TranzactIO comes with a very small amount of dependencies: only ZIO and ZIO-interop-Cats are required.
Any constructive criticism, bug report or offer to help is welcome. Just open an issue or a PR.
Why ?
On my applications, I regularly have quite a bunch of business logic around my queries.
If I want to run that logic within a transaction, I have to wrap it with Doobie's ConnectionIO.
But I'm already using ZIO as my effect monad! I don't want another one...
In addition, IO monads in DB libraries (like Doobie's ConnectionIO) miss quite a bit of the operations that ZIO has.
That's where TranzactIO comes from. I wanted a way to use ZIO everywhere, and run the transaction whenever I decided to.
Getting started
Sbt setup
TranzactIO is available on Maven Central (see the badge on top of this README to get the latest version). In your build.sbt:
// Add this if you use doobie:
libraryDependencies += "io.github.gaelrenoux" %% "tranzactio-doobie" % TranzactIOVersion
// Or this if you use anorm:
libraryDependencies += "io.github.gaelrenoux" %% "tranzactio-anorm" % TranzactIOVersion
Imports
Most of the time, you will need to import two packages.
The first is io.github.gaelrenoux.tranzactio._ and contains Tranzactio's generic classes, like DbException.
The second one is specific to the DB-library you are using.
The names of most entities are the same for each DB-library: for instance, you'll always have the tzio function, or the Connection and Database classes.
The package is always named after the DB-library it is used with, e.g.:
io.github.gaelrenoux.tranzactio.doobie._io.github.gaelrenoux.tranzactio.anorm._
Wrapping a query
Just use tzio to wrap your usual query type!
Doobie
import zio._
import doobie.implicits._
import io.github.gaelrenoux.tranzactio._
import io.github.gaelrenoux.tranzactio.doobie._
val list: ZIO[Connection, DbException, List[String]] = tzio {
sql"SELECT name FROM users".query[String].to[List]
}
Anorm
Since Anorm doesn't provide an IO monad (or even a specific query type), tzio will provide the JDBC connection you need to run a query. The operation will be wrapped in a ZIO (as a blocking effect).
import zio._
import anorm._
import io.github.gaelrenoux.tranzactio._
import io.github.gaelrenoux.tranzactio.anorm._
val list: ZIO[Connection, DbException, List[String]] = tzio { implicit c =>
SQL"SELECT name FROM users".as(SqlParser.str(1).*)
}
Running the transaction (or using auto-commit)
The Database module (from the same package as tzio) contains the methods needed to provide the Connection and run the transactions.
Here are some examples with Doobie.
The code for Anorm is identical, except it has a different import: io.github.gaelrenoux.tranzactio.anorm._ instead of io.github.gaelrenoux.tranzactio.doobie._.
import io.github.gaelrenoux.tranzactio._
import io.github.gaelrenoux.tranzactio.doobie._
import query._
import query.console.Console
// Let's start with a very simple one. Connection exceptions are transformed into defects.
val query: ZIO[Connection, String, Long] = ???
val tran: ZIO[Database, String, Long] = Database.transactionOrDie(query)
// If you have an additional environment, it would end up on the resulting effect as well.
val queryWithEnv: ZIO[Connection with Console, String, Long] = ???
val tranWithEnv: ZIO[Database with Console, String, Long] = Database.transactionOrDie(queryWithEnv)
// Do you want to handle connection errors yourself? They will appear on the Left side of the Either.
val tranWithSeparateErrors: ZIO[Database, Either[DbException, String], Long] = Database.transaction(query)
// Are you only expecting errors coming from the DB ? Let's handle all of them at the same time.
val queryWithDbEx: ZIO[Connection, DbException, Long] = ???
val tranWithDbEx: ZIO[Database, DbException, Long] = Database.transactionOrWiden(queryWithDbEx)
// Or maybe you're just grouping all errors together as exceptions.
val queryWithEx: ZIO[Connection, java.io.IOException, Long] = ???
val tranWithEx: ZIO[Database, Exception, Long] = Database.transactionOrWiden(queryWithEx)
// You can also commit even on a failure (only rollbacking on a defect). Useful if you're using the failure channel for short-circuiting!
val tranCommitOnFailure: ZIO[Database, String, Long] = Database.transactionOrDie(query, tranCommitOnFailure = true)
// And if you're actually not interested in a transaction, you can just auto-commit all queries.
val autoCommit: ZIO[Database, String, Long] = Database.autoCommitOrDie(query)
Providing the Database
The Database methods return a ZIO instance which requires a Database as an environment.
This module is provided as usual through a ZLayer.
The most common way to construct a Database is using a javax.sql.DataSource, which your connection pool implementation (like HikariCP) should provide.
Alternatively (e.g. in a test environment), you can create a DataSource manually.
The layer to build a Database from a javax.sql.DataSource is on the Database object.
Here's an example for Doobie.
Again, the code for Anorm is identical, except it has a different import: io.github.gaelrenoux.tranzactio.anorm._ instead of io.github.gaelrenoux.tranzactio.doobie._.
import io.github.gaelrenoux.tranzactio.doobie._
import javax.sql.DataSource
import zio._
val dbLayer: ZLayer[DataSource, Nothing, Database] = Database.fromDatasource
More code samples
Find more in src/main/samples, or look below for some details.
Detailed documentation
Version compatibility
The table below indicates for each version of TranzactIO, the versions of ZIO or libraries it's been built with. Check the backward compatibility information on those libraries to check which versions TranzactIO can support.
| TranzactIO | Scala | ZIO | Doobie | Anorm | |------------|-------------|--------------|------------|--------| | 0.1.0 | 2.13 | 1.0.0-RC17 | 0.8.6 | - | | 0.2.0 | 2.13 | 1.0.0-RC18-2 | 0.8.6 | - | | 0.3.0 | 2.13 | 1.0.0-RC18-2 | 0.8.6 | 2.6.5 | | 0.4.0 | 2.13 | 1.0.0-RC19-2 | 0.9.0 | 2.6.5 | | 0.5.0 | 2.13 | 1.0.0-RC20 | 0.9.0 | 2.6.5 | | 0.6.0 | 2.13 | 1.0.0-RC21-1 | 0.9.0 | 2.6.5 | | 1.0.0 | 2.13 | 1.0.0 | 0.9.0 | 2.6.7 | | 1.0.1 | 2.13 | 1.0.0 | 0.9.0 | 2.6.7 | | 1.1.0 | 2.13 | 1.0.3 | 0.9.2 | 2.6.7 | | 1.2.0 | 2.13 | 1.0.3 | 0.9.2 | 2.6.7 | | 1.3.0 | 2.13 | 1.0.5 | 0.9.4 | 2.6.10 | | 2.0.0 | 2.13 | 1.0.5 | 0.12.1 | 2.6.10 | | 2.1.0 | 2.12 2.13 | 1.0.9 | 0.13.4 | 2.6.10 | | 3.0.0 | 2.12 2.13 | 1.0.11 | 1.0.0-RC2 | 2.6.10 | | 4.0.0 | 2.12 2.13 | 2.0.0 | 1.0.0-RC2 | 2.6.10 | | 4.1.0 | 2.12 2.13 3 | 2.0.2 | 1.0.0-RC2 | 2.7.0 | | 4.2.0 | 2.12 2.13 3 | 2.0.13 | 1.0.0-RC2 | 2.7.0 | | 5.0.1 | 2.12 2.13 3 | 2.0.15 | 1.0.0-RC4 | 2.7.0 | | 5.1.0 | 2.12 2.13 3 | 2.0.21 | 1.0.0-RC5 | 2.7.0 | | 5.2.0 | 2.12 2.13 3 | 2.0.21 | 1.0.0-RC5 | 2.7.0 | | 5.3.0 | 2.12 2.13 3 | 2.0.21 | 1.0.0-RC8 | 2.7.0 | | 5.4.0 | 2.12 2.13 3 | 2.0.21 | 1.0.0-RC9 | 2.7.0 | | 5.5.0 | 2.12 2.13 3 | 2.0.22 | 1.0.0-RC10 | 2.7.0 | | 5.5.2 | 2.12 2.13 3 | 2.0.22 | 1.0.0-RC10 | 2.7.0 | | 5.6.0 | 2.12 2.13 3 | 2.1.21 | 1.0.0-RC10 | 2.7.0 | | 5.7.0 | 2.12 2.13 3 | 2.1.24 | 1.0.0-RC11 | 2.7.0 | | master | 2.12 2.13 3 | 2.1.24 | 1.0.0-RC11 | 2.7.0 |
Some definitions
Database operations
You will find reference through the documentation to Database operations. Those are the specific operations handled by Tranzactio, that are necessary to interact with a database:
- openConnection
- setAutoCommit
- commitConnection
- rollbackConnection
- closeConnection
They correspond to specific methods in the ConnectionSource service.
You would not usually address that service directly, going through Database instead.
Error kinds
In TranzactIO, we recognize two kinds of errors relating to the DB: query errors, and connection errors:
Query errors happen when you run a specific query.
They can be timeouts, SQL syntax errors, constraint errors, etc.
When you have a ZIO[Connection, E, A], E is the type for query errors.
Connection errors happen when you manage connections or transactions: opening connections, creating, commiting or rollbacking transactions, etc. They are not linked
