Add the following dependencies to your build.sbt file.
"za.co.absa.db.fa-db" %% "doobie" % "<version>"
// if not already included as transitive dependency
"dev.zio" %% "zio-interop-cats" % "23.0.0.8"Create a class for your database function and ZLayer in its companion object.
class GetActorById(implicit schema: DBSchema, dbEngine: DoobieEngine[Task])
extends DoobieOptionalResultFunction[Int, Actor, Task] {
override def sql(values: Int)(implicit read: Read[Actor]): Fragment =
sql"SELECT actor_id, first_name, last_name FROM ${Fragment.const(functionName)}($values)"
}
object GetActorById {
val layer: ZLayer[PostgresDatabaseProvider, Nothing, GetActorById] = ZLayer {
for {
dbProvider <- ZIO.service[PostgresDatabaseProvider]
} yield new GetActorById()(Integration, dbProvider.dbEngine)
}
}Create a ZLayer for your doobie's Transactor. Please note that HikariTransactor is a managed resource, and it needs to be closed when the application is shutting down. Notice that the ZLayer requires a Scope to be provided. The Scope data type is the foundation of safe and composable resources handling in ZIO. More on the topic can be found here https://zio.dev/reference/resource/scope/.
import com.zaxxer.hikari.HikariConfig
import doobie.hikari.HikariTransactor
import zio.Runtime.defaultBlockingExecutor
import zio._
import zio.interop.catz._ // required for ZIO.toScopedZIO, provides also Async for zio.Task
object TransactorProvider {
val layer: ZLayer[Any with Scope, Throwable, HikariTransactor[Task]] = ZLayer {
for {
postgresConfig <- ZIO.config[PostgresConfig](PostgresConfig.config)
hikariConfig = {
val config = new HikariConfig()
config.setDriverClassName(postgresConfig.dataSourceClass)
config.setJdbcUrl(
s"jdbc:postgresql://${postgresConfig.serverName}:${postgresConfig.portNumber}/${postgresConfig.databaseName}"
)
config.setUsername(postgresConfig.user)
config.setPassword(postgresConfig.password)
// configurable pool size
config.setMaximumPoolSize(postgresConfig.maxPoolSize)
config
}
// notice we are using the default blocking executor from zio.Runtime
// .toScopedZIO is an extension method that converts a managed resource to a scoped ZIO
xa <- HikariTransactor.fromHikariConfig[Task](hikariConfig, defaultBlockingExecutor.asExecutionContext).toScopedZIO
} yield xa
}
}Transactor can then be provided as a dependency to other ZLayers.
class PostgresDatabaseProvider(val dbEngine: DoobieEngine[Task])
object PostgresDatabaseProvider {
val layer: RLayer[Transactor[Task], PostgresDatabaseProvider] = ZLayer {
for {
// access the transactor from the environment
transactor <- ZIO.service[Transactor[Task]]
doobieEngine <- ZIO.succeed(new DoobieEngine[Task](transactor))
} yield new PostgresDatabaseProvider(doobieEngine)
}
}Provide default Scope for your application.
object Main extends ZIOAppDefault with Server {
override def run: ZIO[Any, Throwable, Unit] =
server
.provide(
// provided layers ...
// ...
// provided default scope
zio.Scope.default
)
}This way we can establish a connection to a database with managed connection pooling and rely on the default ZIO's blocking execution context and also properly clean up resources when application shuts down.