Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple databases #52

Merged
merged 6 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}

}
Loading