Skip to content

Commit

Permalink
Introduces DbContext. Migrates to Doobie-RC4 (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaelrenoux committed Aug 5, 2023
1 parent 985dcb8 commit 112d9ec
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 57 deletions.
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ Again, the code for Anorm is identical, except it has a different import: `io.gi
import io.github.gaelrenoux.tranzactio.doobie._
import javax.sql.DataSource
import zio._
import zio.clock.Clock

val dbLayer: ZLayer[Has[DataSource], Nothing, Database] = Database.fromDatasource
```
Expand Down Expand Up @@ -289,7 +288,7 @@ Database.transaction(???) // es is passed implicitly to the method

```scala
val es: ErrorStrategies = ErrorStrategies.retryForeverFixed(10.seconds)
val dbLayerFromDatasource: ZLayer[Has[DataSource] with Clock, Nothing, Database] = Database.fromDatasource(es)
val dbLayerFromDatasource: ZLayer[Has[DataSource], Nothing, Database] = Database.fromDatasource(es)
```

#### Defining an ErrorStrategies instance
Expand Down Expand Up @@ -353,20 +352,36 @@ import zio._
import doobie.implicits._
import io.github.gaelrenoux.tranzactio.DbException
import io.github.gaelrenoux.tranzactio.doobie._
import zio.clock.Clock

val liveQuery: ZIO[Connection, DbException, List[String]] = tzio { sql"SELECT name FROM users".query[String].to[List] }
val testQuery: ZIO[Connection, DbException, List[String]] = ZIO.succeed(List("Buffy Summers"))

val liveEffect: ZIO[Database, DbException, List[String]] = Database.transactionOrWiden(liveQuery)
val testEffect: ZIO[Database, DbException, List[String]] = Database.transactionOrWiden(testQuery)

val willFail: ZIO[Clock, Any, List[String]] = liveEffect.provideLayer(Database.none) // THIS WILL FAIL
val testing: ZIO[Clock, Any, List[String]] = testEffect.provideLayer(Database.none) // This will work
val willFail: ZIO[Any, DbException, List[String]] = liveEffect.provideLayer(Database.none) // THIS WILL FAIL
val testing: ZIO[Any, DbException, List[String]] = testEffect.provideLayer(Database.none) // This will work
```



### Doobie-specific stuff

#### Log handler

From Doobie RC3 and onward, the `LogHandler` is no longer passed when building the query, but when running the transaction instead.
In TranzactIO, you can define a `LogHandler` through the `DbContext`. This context is passed implicitly when creating the Database layer, so you just need to declare your own.

```scala
import doobie.util.log.LogHandler
import io.github.gaelrenoux.tranzactio.doobie._
import javax.sql.DataSource
import zio._
import zio.interop.catz._

implicit val doobieContext: DbContext = DbContext(logHandler = LogHandler.jdkLogHandler[Task])
val dbLayer: ZLayer[Has[DataSource], Nothing, Database] = Database.fromDatasource
```


## FAQ
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.gaelrenoux.tranzactio

import io.github.gaelrenoux.tranzactio.test.DatabaseModuleTestOps
import io.github.gaelrenoux.tranzactio.test.{DatabaseModuleTestOps, NoopJdbcConnection}
import zio.ZIO.attemptBlocking
import zio.{Tag, ZIO, ZLayer, Trace}
import zio.{Tag, Trace, ZIO, ZLayer}

import java.sql.{Connection => JdbcConnection}

Expand All @@ -11,6 +11,7 @@ import java.sql.{Connection => JdbcConnection}
package object anorm extends Wrapper {
override final type Connection = JdbcConnection
override final type Database = Database.Service
override final type DbContext = EmptyDbContext.type
override final type Query[A] = JdbcConnection => A
override final type TranzactIO[A] = ZIO[Connection, DbException, A]

Expand All @@ -23,25 +24,21 @@ package object anorm extends Wrapper {

/** Database for the Anorm wrapper */
object Database
extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection]]
with DatabaseModuleTestOps[Connection] {
extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection], DbContext]
with DatabaseModuleTestOps[Connection, DbContext] {
self =>

private[tranzactio] override implicit val connectionTag: Tag[Connection] = anorm.connectionTag

/** How to provide a Connection for the module, given a JDBC connection and some environment. */
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] = {
ZIO.succeed(connection)
}
override def noConnection(implicit trace: Trace): ZIO[Any, Nothing, Connection] = ZIO.succeed(NoopJdbcConnection)

/** Creates a Database Layer which requires an existing ConnectionSource. */
override final def fromConnectionSource(implicit trace: Trace): ZLayer[ConnectionSource, Nothing, Database] =
ZLayer.fromFunction { (cs: ConnectionSource) =>
new DatabaseServiceBase[Connection](cs) {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] =
self.connectionFromJdbc(connection)
}
}
override final def fromConnectionSource(implicit dbContext: DbContext, trace: Trace): ZLayer[ConnectionSource, Nothing, Database] =
ZLayer.fromFunction { (cs: ConnectionSource) => new DatabaseService(cs) }
}

private class DatabaseService(cs: ConnectionSource) extends DatabaseServiceBase[Connection](cs) {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] =
ZIO.succeed(connection)
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package io.github.gaelrenoux.tranzactio


import zio.{Tag, ZIO, ZLayer, Trace}
import zio.{Tag, Trace, ZIO, ZLayer}

import java.sql.{Connection => JdbcConnection}
import javax.sql.DataSource

/** Template implementing the commodity methods for a Db module. */
abstract class DatabaseModuleBase[Connection, Database <: DatabaseOps.ServiceOps[Connection] : Tag]
abstract class DatabaseModuleBase[Connection, Database <: DatabaseOps.ServiceOps[Connection] : Tag, DbContext]
extends DatabaseOps.ModuleOps[Connection, Database] {

type Service = DatabaseOps.ServiceOps[Connection]
Expand All @@ -24,32 +23,29 @@ abstract class DatabaseModuleBase[Connection, Database <: DatabaseOps.ServiceOps
override def autoCommit[R, E, A](
zio: => ZIO[Connection with R, E, A]
)(implicit errorStrategies: ErrorStrategiesRef = ErrorStrategies.Parent, trace: Trace): ZIO[Database with R, Either[DbException, E], A] = {
ZIO.serviceWithZIO[Database]{ db=>
ZIO.serviceWithZIO[Database] { db =>
db.autoCommit[R, E, A](zio)
}
}

/** Creates a Database Layer which requires an existing ConnectionSource. */
def fromConnectionSource(implicit trace: Trace): ZLayer[ConnectionSource, Nothing, Database]

/** Creates a Tranzactio Connection, given a JDBC connection and a Blocking. Useful for some utilities. */
def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection]
def fromConnectionSource(implicit dbContext: DbContext, trace: Trace): ZLayer[ConnectionSource, Nothing, Database]

/** Commodity method: creates a Database Layer which includes its own ConnectionSource based on a DataSource. Most
* connection pool implementations should be able to provide you a DataSource.
*
* When no implicit ErrorStrategies is available, the default ErrorStrategies will be used.
*/
final def fromDatasource(implicit trace: Trace): ZLayer[DataSource, Nothing, Database] =
final def fromDatasource(implicit dbContext: DbContext, trace: Trace): ZLayer[DataSource, Nothing, Database] =
ConnectionSource.fromDatasource >>> fromConnectionSource

/** As `fromDatasource`, but provides a default ErrorStrategiesRef. When a method is called with no available implicit
* ErrorStrategiesRef, the ErrorStrategiesRef in argument will be used. */
final def fromDatasource(errorStrategies: ErrorStrategiesRef)(implicit trace: Trace): ZLayer[DataSource, Nothing, Database] =
final def fromDatasource(errorStrategies: ErrorStrategiesRef)(implicit dbContext: DbContext, trace: Trace): ZLayer[DataSource, Nothing, Database] =
ConnectionSource.fromDatasource(errorStrategies) >>> fromConnectionSource

/** As `fromDatasource(ErrorStrategiesRef)`, but an `ErrorStrategies` is provided through a layer instead of as a parameter. */
final def fromDatasourceAndErrorStrategies(implicit trace: Trace): ZLayer[DataSource with ErrorStrategies, Nothing, Database] =
final def fromDatasourceAndErrorStrategies(implicit dbContext: DbContext, trace: Trace): ZLayer[DataSource with ErrorStrategies, Nothing, Database] =
ConnectionSource.fromDatasourceAndErrorStrategies >>> fromConnectionSource

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.gaelrenoux.tranzactio

/** To be used by libraries where a DbContext is not necessary */
object EmptyDbContext {
implicit val Default: EmptyDbContext.type = EmptyDbContext
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ trait Wrapper {

val Database: DatabaseOps.ModuleOps[Connection, _ <: DatabaseOps.ServiceOps[Connection]] // scalastyle:ignore field.name

/** Contextual information for the DB, its content depending on the underlying library. */
type DbContext

/** The specific type used in the wrapped library to represent an SQL query. */
type Query[A]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import io.github.gaelrenoux.tranzactio._
import zio.{Tag, ZEnvironment, ZIO, ZLayer, Trace}

/** Testing utilities on the Database module. */
trait DatabaseModuleTestOps[Connection] extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection]] {
trait DatabaseModuleTestOps[Connection, DbContext] extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection], DbContext] {

type AnyDatabase = DatabaseOps.ServiceOps[Connection]

private[tranzactio] implicit val connectionTag: Tag[Connection]

/** A Connection which is incapable of running anything, to use when unit testing (and the queries are actually stubbed,
* so they do not need a Database). Trying to run actual queries against it will fail. */
def noConnection(implicit trace: Trace): ZIO[Any, Nothing, Connection] = connectionFromJdbc(NoopJdbcConnection)
def noConnection(implicit trace: Trace): ZIO[Any, Nothing, Connection]

/** A Database which is incapable of running anything, to use when unit testing (and the queries are actually stubbed,
* so they do not need a Database). Trying to run actual queries against it will fail. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package io.github.gaelrenoux.tranzactio

import _root_.doobie.free.KleisliInterpreter
import _root_.doobie.LogHandler
import _root_.doobie.util.transactor.{Strategy, Transactor}
import cats.effect.Resource
import io.github.gaelrenoux.tranzactio.test.DatabaseModuleTestOps
import io.github.gaelrenoux.tranzactio.test.NoopJdbcConnection
import zio.interop.catz._
import zio.stream.ZStream
import zio.stream.interop.fs2z._
import zio.{Tag, Task, ZIO, ZLayer, Trace}
import zio.{Tag, Task, Trace, ZIO, ZLayer}

import java.sql.{Connection => JdbcConnection}

Expand All @@ -22,7 +24,17 @@ package object doobie extends Wrapper {
private[tranzactio] val connectionTag = implicitly[Tag[Connection]]

/** Default queue size when converting from FS2 streams. Same default value as in FS2RIOStreamSyntax.toZStream. */
final val DefaultStreamQueueSize = 16
private final val DefaultStreamQueueSize = 16

/** How to provide a Connection for the module, given a JDBC connection and some environment. */
private final def transactorFromJdbcConnection(connection: => JdbcConnection, dbContext: DbContext)(implicit trace: Trace): ZIO[Any, Nothing, Connection] = {
ZIO.succeed {
val connect = (c: JdbcConnection) => Resource.pure[Task, JdbcConnection](c)
val interp = KleisliInterpreter[Task](dbContext.logHandler).ConnectionInterpreter
val tran = Transactor(connection, connect, interp, Strategy.void)
tran
}
}

override final def tzio[A](q: => Query[A])(implicit trace: Trace): TranzactIO[A] =
ZIO.serviceWithZIO[Connection] { c =>
Expand All @@ -37,30 +49,31 @@ package object doobie extends Wrapper {

/** Database for the Doobie wrapper */
object Database
extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection]]
with DatabaseModuleTestOps[Connection] {
extends DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection], DbContext]
with DatabaseModuleTestOps[Connection, DbContext] {
self =>

private[tranzactio] override implicit val connectionTag: Tag[Connection] = doobie.connectionTag

/** How to provide a Connection for the module, given a JDBC connection and some environment. */
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] = {
ZIO.succeed {
val connect = (c: JdbcConnection) => Resource.pure[Task, JdbcConnection](c)
val interp = KleisliInterpreter[Task].ConnectionInterpreter
val tran = Transactor(connection, connect, interp, Strategy.void)
tran
}
}
override def noConnection(implicit trace: Trace): ZIO[Any, Nothing, Transactor[Task]] =
transactorFromJdbcConnection(NoopJdbcConnection, DbContext.Default)

/** Creates a Database Layer which requires an existing ConnectionSource. */
override final def fromConnectionSource(implicit trace: Trace): ZLayer[ConnectionSource, Nothing, Database] =
ZLayer.fromFunction { (cs: ConnectionSource) =>
new DatabaseServiceBase[Connection](cs) {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] =
self.connectionFromJdbc(connection)
}
}
override final def fromConnectionSource(implicit dbContext: DbContext, trace: Trace): ZLayer[ConnectionSource, Nothing, Database] =
ZLayer.fromFunction { (cs: ConnectionSource) => new DatabaseService(cs, dbContext) }
}

private class DatabaseService(cs: ConnectionSource, val dbContext: DbContext) extends DatabaseServiceBase[Connection](cs) {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Transactor[Task]] =
transactorFromJdbcConnection(connection, dbContext)
}

case class DbContext(
logHandler: LogHandler[Task]
)

object DbContext {
implicit val Default: DbContext = DbContext(LogHandler.noop[Task])
}

}
1 change: 0 additions & 1 deletion examples/src/test/scala/samples/anorm/LayeredApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import io.github.gaelrenoux.tranzactio.anorm._
import io.github.gaelrenoux.tranzactio.{DbException, ErrorStrategiesRef}
import samples.{Conf, ConnectionPool, Person}
import zio._
import zio.Console

/** A sample app where all modules are linked through ZLayer. Should run as is (make sure you have com.h2database:h2 in
* your dependencies). */
Expand Down
5 changes: 4 additions & 1 deletion examples/src/test/scala/samples/doobie/LayeredApp.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package samples.doobie

import doobie.util.log.LogHandler
import io.github.gaelrenoux.tranzactio.doobie._
import io.github.gaelrenoux.tranzactio.{DbException, ErrorStrategiesRef}
import samples.{Conf, ConnectionPool, Person}
import zio._
import zio.Console
import zio.interop.catz._

/** A sample app where all modules are linked through ZLayer. Should run as is (make sure you have com.h2database:h2 in
* your dependencies). */
Expand All @@ -13,6 +14,8 @@ object LayeredApp extends zio.ZIOAppDefault {
private val conf = Conf.live("samble-doobie-app")
private val dbRecoveryConf = conf >>> ZLayer.fromFunction((_: Conf).dbRecovery)
private val datasource = conf >>> ConnectionPool.live
// The DbContext is optional, default is to have the noop LogHandler
implicit val dbContext: DbContext = DbContext(logHandler = LogHandler.jdkLogHandler[Task])
private val database = (datasource ++ dbRecoveryConf) >>> Database.fromDatasourceAndErrorStrategies
private val personQueries = PersonQueries.live

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import io.github.gaelrenoux.tranzactio.{DbException, ErrorStrategiesRef}
import samples.{Conf, ConnectionPool, Person}
import zio._
import zio.stream._
import zio.Console

/** Same as LayeredApp, but using Doobie's stream (converted into ZIO strem). */
// scalastyle:off magic.number
Expand Down
2 changes: 1 addition & 1 deletion project/BuildHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ object BuildHelper {
val zio = "2.0.15"
val zioCats = "23.0.0.8"
val cats = "2.9.0"
val doobie = "1.0.0-RC2"
val doobie = "1.0.0-RC4"
val anorm = "2.7.0"
val h2 = "2.2.220"
val scala212 = "2.12.18"
Expand Down

0 comments on commit 112d9ec

Please sign in to comment.