Skip to content

Commit

Permalink
Support multiple databases (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaelrenoux committed Mar 24, 2024
1 parent 765da6c commit cfc719f
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 10 deletions.
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ import io.github.gaelrenoux.tranzactio.doobie._
import javax.sql.DataSource
import zio._

val dbLayer: ZLayer[Has[DataSource], Nothing, Database] = Database.fromDatasource
val dbLayer: ZLayer[DataSource, Nothing, Database] = Database.fromDatasource
```


Expand Down Expand Up @@ -290,7 +290,7 @@ Database.transaction(???) // es is passed implicitly to the method

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

#### Defining an ErrorStrategies instance
Expand Down Expand Up @@ -329,6 +329,36 @@ val es: ErrorStrategies =



### Multiple Databases

Some applications use multiple databases.
In that case, it is necessary to have them be different types in the ZIO environment.

Tranzactio offers a typed database class, called `DatabaseT[_]`.
You only need to provide a different marker type for each database you use.

```scala
import io.github.gaelrenoux.tranzactio.doobie._
import javax.sql.DataSource
import zio._

trait Db1
trait Db2

val db1Layer: ZLayer[Any, Nothing, DatabaseT[Db1]] = datasource1Layer >>> Database[Db1].fromDatasource
val db2Layer: ZLayer[Any, Nothing, DatabaseT[Db2]] = datasource2Layer >>> Database[Db2].fromDatasource

val queries1: ZIO[Connection, DbException, List[String]] = ???
val zio1: ZIO[DatabaseT[Db1], DbException, List[Person]] = DatabaseT[Db1].transactionOrWiden(queries1)
val zio2: ZIO[DatabaseT[Db2], DbException, List[Person]] = DatabaseT[Db2].transactionOrWiden(queries1)
```

When creating the layers for the datasources, don't forget to use `.fresh` when you have sub-layers of the same type on both sides!

You can see a full example in the `examples` submodule (in `LayeredAppMultipleDatabases`).



### Single-connection Database

In some cases, you might want to have a `Database` module representing a single connection.
Expand Down Expand Up @@ -382,7 +412,7 @@ import zio._
import zio.interop.catz._

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


Expand All @@ -403,7 +433,7 @@ See https://github.com/zio/interop-cats/issues/669 for more details about this i



### When will tranzactio work with <insert DB library here>?
### When will tranzactio work with ?

I want to add wrappers around more database access libraries.
Anorm was the second one I did, next should probably be Quill (based on the popularity of the project on GitHub),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,10 @@ package object anorm extends Wrapper {
override final def connectionFromJdbc(connection: => JdbcConnection)(implicit trace: Trace): ZIO[Any, Nothing, Connection] =
ZIO.succeed(connection)
}

override final type DatabaseT[M] = DatabaseTBase[M, Connection]

object DatabaseT extends DatabaseTBase.Companion[Connection, DbContext] {
def apply[M: Tag]: Module[M] = new Module[M](Database)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.gaelrenoux.tranzactio

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

/**
* This is a typed database, to use when you have multiple databases in your application. Simply provide a marker type,
* and ZIO will be able to differentiate between multiple DatabaseT[_] types in your environment.
* @tparam M Marker type, no instances. */
class DatabaseTBase[M: Tag, Connection](underlying: DatabaseOps.ServiceOps[Connection]) extends DatabaseOps.ServiceOps[Connection] {

override def transaction[R, E, A](task: => ZIO[Connection with R, E, A], commitOnFailure: => Boolean)
(implicit errorStrategies: ErrorStrategiesRef, trace: Trace): ZIO[R with Any, Either[DbException, E], A] =
underlying.transaction[R, E, A](task, commitOnFailure = commitOnFailure)


override def autoCommit[R, E, A](task: => ZIO[Connection with R, E, A])
(implicit errorStrategies: ErrorStrategiesRef, trace: Trace): ZIO[R with Any, Either[DbException, E], A] =
underlying.autoCommit[R, E, A](task)

}

object DatabaseTBase {
trait Companion[Connection, DbContext] {
type Module[M] = DatabaseTBase.Module[M, Connection, DbContext]

def apply[M: Tag]: Module[M]
}

class Module[M: Tag, Connection: Tag, DbContext](underlying: DatabaseModuleBase[Connection, DatabaseOps.ServiceOps[Connection], DbContext])
extends DatabaseModuleBase[Connection, DatabaseTBase[M, Connection], DbContext] {
override def fromConnectionSource(implicit dbContext: DbContext, trace: Trace): ZLayer[ConnectionSource, Nothing, DatabaseTBase[M, Connection]] =
underlying.fromConnectionSource.map(env => ZEnvironment(new DatabaseTBase[M, Connection](env.get)))
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.gaelrenoux.tranzactio

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


/** A specific wrapper package for one specific library (e.g. Doobie). */
Expand Down Expand Up @@ -31,4 +31,10 @@ trait Wrapper {
/** Wraps a library-specific query into a TranzactIO. */
def tzio[A](q: => Query[A])(implicit trace: Trace): TranzactIO[A]

/** Parameterized type for databases, allowing for multiple databases to be handled */
type DatabaseT[M]

val DatabaseT: DatabaseTBase.Companion[Connection, DbContext] // scalastyle:ignore field.name


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

import _root_.doobie.free.KleisliInterpreter
import _root_.doobie.LogHandler
import _root_.doobie.free.KleisliInterpreter
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 io.github.gaelrenoux.tranzactio.test.{DatabaseModuleTestOps, NoopJdbcConnection}
import zio.interop.catz._
import zio.stream.ZStream
import zio.stream.interop.fs2z._
Expand Down Expand Up @@ -76,4 +75,10 @@ package object doobie extends Wrapper {
implicit val Default: DbContext = DbContext(LogHandler.noop[Task])
}

override final type DatabaseT[M] = DatabaseTBase[M, Connection]

object DatabaseT extends DatabaseTBase.Companion[Connection, DbContext] {
def apply[M: Tag]: Module[M] = new Module[M](Database)
}

}
24 changes: 22 additions & 2 deletions examples/src/test/scala/samples/SamplesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ object SamplesSpec extends ZIOSpecDefault {
suite("SamplesSpec")(
testApp("Doobie", doobie.LayeredApp),
testApp("Doobie-Streaming", doobie.LayeredAppStreaming),
testApp("Anorm", anorm.LayeredApp)
testAppMultipleDatabases("Doobie", doobie.LayeredAppMultipleDatabases),
testApp("Anorm", anorm.LayeredApp),
testAppMultipleDatabases("Anorm", anorm.LayeredAppMultipleDatabases)
)

private def testApp(name: String, app: ZIOAppDefault): MySpec =

test(s"$name LayeredApp prints its progress then the trio") {
test(s"$name App prints its progress then the trio") {
for {
_ <- app.run.provide(ignoredAppArgs ++ Scope.default)
output <- TestConsole.output
Expand All @@ -31,4 +33,22 @@ object SamplesSpec extends ZIOSpecDefault {
))
}

private def testAppMultipleDatabases(name: String, app: ZIOAppDefault): MySpec =

test(s"$name App prints its progress for the trio, then its progress for the mentor, then prints the full team") {
for {
_ <- app.run.provide(ignoredAppArgs ++ Scope.default)
output <- TestConsole.output
} yield assertTrue(output == Vector(
"Starting the app\n",
"Creating the table\n",
"Inserting the trio\n",
"Reading the trio\n",
"Creating the table\n",
"Inserting the mentor\n",
"Reading the mentor\n",
"Buffy Summers, Willow Rosenberg, Alexander Harris, Rupert Giles\n"
))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package samples.anorm

import io.github.gaelrenoux.tranzactio.{DbException, ErrorStrategies}
import io.github.gaelrenoux.tranzactio.anorm._
import samples.{Conf, ConnectionPool, Person}
import zio._

import javax.sql.DataSource

/** A sample app where all modules are linked through ZLayer. Should run as is (make sure you have com.h2database:h2 in
* your dependencies). */
object LayeredAppMultipleDatabases extends zio.ZIOAppDefault {

/** Marker trait for the first DB */
trait Db1

/** Marker trait for the second DB */
trait Db2

private val database1: ZLayer[Any, Throwable, DatabaseT[Db1]] = {
// Fresh calls are required so that the confs and datasource aren't conflated with the other layer's
val conf = Conf.live("samble-anorm-app-1").fresh
val dbRecoveryConf: ZLayer[Any, Nothing, ErrorStrategies] = conf >>> ZLayer.fromFunction((_: Conf).dbRecovery).fresh
val datasource: ZLayer[Any, Throwable, DataSource] = conf >>> ConnectionPool.live.fresh
(datasource ++ dbRecoveryConf) >>> DatabaseT[Db1].fromDatasourceAndErrorStrategies
}

private val database2: ZLayer[Any, Throwable, DatabaseT[Db2]] = {
// Fresh calls are required so that the confs and datasource aren't conflated with the other layer's
val conf = Conf.live("samble-anorm-app-2").fresh
val dbRecoveryConf: ZLayer[Any, Nothing, ErrorStrategies] = conf >>> ZLayer.fromFunction((_: Conf).dbRecovery).fresh
val datasource: ZLayer[Any, Throwable, DataSource] = conf >>> ConnectionPool.live.fresh
(datasource ++ dbRecoveryConf) >>> DatabaseT[Db2].fromDatasourceAndErrorStrategies
}

private val personQueries = PersonQueries.live

type AppEnv = DatabaseT[Db1] with DatabaseT[Db2] with PersonQueries
private val appEnv = database1 ++ database2 ++ personQueries

override def run: ZIO[ZIOAppArgs with Scope, Any, Any] =
for {
_ <- Console.printLine("Starting the app")
team <- myApp().provideLayer(appEnv)
_ <- Console.printLine(team.mkString(", "))
} yield ExitCode(0)

/** Main code for the application. Results in a big ZIO depending on the AppEnv. */
def myApp(): ZIO[PersonQueries with DatabaseT[Db2] with DatabaseT[Db1], DbException, List[Person]] = {
val queries1: ZIO[PersonQueries with Connection, DbException, List[Person]] = for {
_ <- Console.printLine("Creating the table").orDie
_ <- PersonQueries.setup
_ <- Console.printLine("Inserting the trio").orDie
_ <- PersonQueries.insert(Person("Buffy", "Summers"))
_ <- PersonQueries.insert(Person("Willow", "Rosenberg"))
_ <- PersonQueries.insert(Person("Alexander", "Harris"))
_ <- Console.printLine("Reading the trio").orDie
trio <- PersonQueries.list
} yield trio

val queries2: ZIO[PersonQueries with Connection, DbException, List[Person]] = for {
_ <- Console.printLine("Creating the table").orDie
_ <- PersonQueries.setup
_ <- Console.printLine("Inserting the mentor").orDie
_ <- PersonQueries.insert(Person("Rupert", "Giles"))
_ <- Console.printLine("Reading the mentor").orDie
mentor <- PersonQueries.list
} yield mentor

val zTrio: ZIO[PersonQueries with DatabaseT[Db1], DbException, List[Person]] = DatabaseT[Db1].transactionOrWiden(queries1)
val zMentor: ZIO[PersonQueries with DatabaseT[Db2], DbException, List[Person]] = DatabaseT[Db2].transactionOrWiden(queries2)

for {
trio <- zTrio
mentor <- zMentor
} yield trio ++ mentor
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package samples.doobie

import io.github.gaelrenoux.tranzactio.{DbException, ErrorStrategies}
import io.github.gaelrenoux.tranzactio.doobie._
import samples.{Conf, ConnectionPool, Person}
import zio._

import javax.sql.DataSource

/** A sample app with two databases for different set of queries. */
object LayeredAppMultipleDatabases extends zio.ZIOAppDefault {

/** Marker trait for the first DB */
trait Db1

/** Marker trait for the second DB */
trait Db2

private val database1: ZLayer[Any, Throwable, DatabaseT[Db1]] = {
// Fresh calls are required so that the confs and datasource aren't conflated with the other layer's
val conf = Conf.live("samble-doobie-app-1").fresh
val dbRecoveryConf: ZLayer[Any, Nothing, ErrorStrategies] = conf >>> ZLayer.fromFunction((_: Conf).dbRecovery).fresh
val datasource: ZLayer[Any, Throwable, DataSource] = conf >>> ConnectionPool.live.fresh
(datasource ++ dbRecoveryConf) >>> DatabaseT[Db1].fromDatasourceAndErrorStrategies
}

private val database2: ZLayer[Any, Throwable, DatabaseT[Db2]] = {
// Fresh calls are required so that the confs and datasource aren't conflated with the other layer's
val conf = Conf.live("samble-doobie-app-2").fresh
val dbRecoveryConf: ZLayer[Any, Nothing, ErrorStrategies] = conf >>> ZLayer.fromFunction((_: Conf).dbRecovery).fresh
val datasource: ZLayer[Any, Throwable, DataSource] = conf >>> ConnectionPool.live.fresh
(datasource ++ dbRecoveryConf) >>> DatabaseT[Db2].fromDatasourceAndErrorStrategies
}

private val personQueries = PersonQueries.live

type AppEnv = DatabaseT[Db1] with DatabaseT[Db2] with PersonQueries
private val appEnv = database1 ++ database2 ++ personQueries

override def run: ZIO[ZIOAppArgs with Scope, Any, Any] =
for {
_ <- Console.printLine("Starting the app")
team <- myApp().provideLayer(appEnv)
_ <- Console.printLine(team.mkString(", "))
} yield ExitCode(0)

/** Main code for the application. Results in a big ZIO depending on the AppEnv. */
def myApp(): ZIO[PersonQueries with DatabaseT[Db2] with DatabaseT[Db1], DbException, List[Person]] = {
val queries1: ZIO[PersonQueries with Connection, DbException, List[Person]] = for {
_ <- Console.printLine("Creating the table").orDie
_ <- PersonQueries.setup
_ <- Console.printLine("Inserting the trio").orDie
_ <- PersonQueries.insert(Person("Buffy", "Summers"))
_ <- PersonQueries.insert(Person("Willow", "Rosenberg"))
_ <- PersonQueries.insert(Person("Alexander", "Harris"))
_ <- Console.printLine("Reading the trio").orDie
trio <- PersonQueries.list
} yield trio

val queries2: ZIO[PersonQueries with Connection, DbException, List[Person]] = for {
_ <- Console.printLine("Creating the table").orDie
_ <- PersonQueries.setup
_ <- Console.printLine("Inserting the mentor").orDie
_ <- PersonQueries.insert(Person("Rupert", "Giles"))
_ <- Console.printLine("Reading the mentor").orDie
mentor <- PersonQueries.list
} yield mentor

val zTrio: ZIO[PersonQueries with DatabaseT[Db1], DbException, List[Person]] = DatabaseT[Db1].transactionOrWiden(queries1)
val zMentor: ZIO[PersonQueries with DatabaseT[Db2], DbException, List[Person]] = DatabaseT[Db2].transactionOrWiden(queries2)

for {
trio <- zTrio
mentor <- zMentor
} yield trio ++ mentor
}

}

0 comments on commit cfc719f

Please sign in to comment.