Small library, that provides interop between Slick and ZIO
Include zio-slick-interop in your build:
libraryDependencies += "io.scalac" %% "zio-slick-interop" % "0.5.0"
It gives you a couple of nice things:
Database is usually a dependency for your other services. Since ZLayer
is the best and recommended way to do DI with ZIO,
zio-slick-interop provides some convenience bindings for using slick with ZLayer
.
Specifically, it's slick.interop.zio.DatabaseProvider
, which is a service, defined by ZLayer
guidelines and provides access to underlying slick database.
You'd generally want to use it when defining your repository services, like the following.
Say we have a simple raw Slick table:
// import your specific DB driver here
import slick.jdbc.H2Profile.api._
final case class Item(id: Long, name: String)
// Raw Slick table
object ItemsTable {
class Items(tag: Tag) extends Table[Item](tag, "items") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def * = (id, name) <> ((Item.apply _).tupled, Item.unapply _)
}
val table = TableQuery[ItemsTable.Items]
}
And we want to have a repository service on top of it with the following contract:
import zio._
trait ItemRepository {
def getById(id: Long): IO[Throwable, Option[Item]]
}
Then we can implement it as a database-agnostic Slick repository using DatabaseProvider
:
import zio._
import slick.interop.zio.DatabaseProvider
// adds ZIO.fromDBIO extension
import slick.interop.zio.syntax._
object SlickItemRepository {
val live: ZLayer[DatabaseProvider, Throwable, ItemRepository] =
ZLayer {
for {
db <- ZIO.service[DatabaseProvider]
repo <- db.profile.flatMap { profile =>
import profile.api._
val dbLayer = ZLayer.succeed(dp)
new ItemRepository {
private val items = ItemsTable.table
def getById(id: Long): IO[Throwable, Option[Item]] = {
val query = items.filter(_.id === id).result
ZIO.fromDBIO(query)
.map(_.headOption)
.provide(dbLayer)
}
}
}
} yield repo
}
}
SlickItemRepository.live
is a repository layer that depends on raw underlying database.
You can notice ZIO.fromDBIO
which is provided by zio-slick-interop and is described below.
There's a syntax extension allowing to lift DBIO
actions into ZIO
:
import slick.interop.zio.syntax._
import slick.interop.zio.DatabaseProvider
import zio._
// import your specific DB driver here
import slick.jdbc.H2Profile.api._
val insert = ItemsTable.table += Item(0L, "name")
val z: ZIO[DatabaseProvider, Throwable, Int] = ZIO.fromDBIO(insert)
This is a ZIO, that can be run given a DatabaseProvider
is present in the environment.
slick.interop.zio.syntax._
imported.
There's also ZIO.fromStreamingDBIO
, which works with streaming slick actions.
When executing a sequence of actions (when "flatMap
-ing"), in the context of a transaction with transactionally
for example,
Slick requires an implicit ExecutionContext
in scope. For this use-case, another overload to ZIO.fromDBIO
takes a function with an
ExecutionContext
as argument:
import slick.interop.zio.syntax._
import slick.interop.zio.DatabaseProvider
import zio._
// import your specific DB driver here
import slick.jdbc.H2Profile.api._
import scala.concurrent.ExecutionContext
val id: Long = ???
val z: ZIO[DatabaseProvider, Throwable, Unit] =
ZIO.fromDBIO { implicit ec: ExecutionContext =>
(for {
_ <- ItemsTable.table += Item(0L, "name")
_ <- ItemsTable.table.filter(_.id === id).map(_.name).update("new name")
} yield ()).transactionally
}
DatabaseProvider
provides a live
layer that needs:
- a typesafe
Config
, that points to a standard Slick configuration block; - specific database backend you're using.
Here's an example of creating a DatabaseProvider
layer:
import zio._
import com.typesafe.config.Config
import slick.interop.zio.DatabaseProvider
val rootConfig: Config = ???
val dbConfigLayer = ZLayer(ZIO.attempt(rootConfig.getConfig("db")))
val dbBackendLayer = ZLayer.succeed(slick.jdbc.H2Profile)
(dbConfigLayer ++ dbBackendLayer) >>> DatabaseProvider.live
Developed by Scalac