Giter VIP home page Giter VIP logo

tranzactio's Introduction

TranzactIO

CI Releases

TranzactIO is a ZIO wrapper for some Scala database access libraries (Doobie and Anorm, for now).

If the library comes with an IO monad (like Doobie's ConnectionIO), it lifts it into a ZIO[Connection, E, A]. If the library doesn't have an IO monad to start with (like Anorm), it provides a ZIO[Connection, E, A] for the role.

Note that Connection is not Java's java.sql.Connection, it's a TranzactIO type.

When you're done chaining ZIO instances (containing either queries or whatever code you need), use TranzactIO's Database module to provide the Connection and execute the transaction. Database can also provide a Connection in auto-commit mode, without a transaction.

TranzactIO comes with a very small amount of dependencies: only ZIO and ZIO-interop-Cats are required.

Any constructive criticism, bug report or offer to help is welcome. Just open an issue or a PR.

Why ?

On my applications, I regularly have quite a bunch of business logic around my queries. If I want to run that logic within a transaction, I have to wrap it with Doobie's ConnectionIO. But I'm already using ZIO as my effect monad! I don't want another one... In addition, IO monads in DB libraries (like Doobie's ConnectionIO) miss quite a bit of the operations that ZIO has.

That's where TranzactIO comes from. I wanted a way to use ZIO everywhere, and run the transaction whenever I decided to.

Getting started

Sbt setup

TranzactIO is available on the Sonatype Central Repository (see the Nexus badge on top of this README to get the version number). In your build.sbt:

// Add this if you use doobie:
libraryDependencies += "io.github.gaelrenoux" %% "tranzactio-doobie" % TranzactIOVersion
// Or this if you use anorm:
libraryDependencies += "io.github.gaelrenoux" %% "tranzactio-anorm" % TranzactIOVersion

Imports

Most of the time, you will need to import two packages. The first is io.github.gaelrenoux.tranzactio._ and contains Tranzactio's generic classes, like DbException.

The second one is specific to the DB-library you are using. The names of most entities are the same for each DB-library: for instance, you'll always have the tzio function, or the Connection and Database classes. The package is always named after the DB-library it is used with, e.g.:

  • io.github.gaelrenoux.tranzactio.doobie._
  • io.github.gaelrenoux.tranzactio.anorm._

Wrapping a query

Just use tzio to wrap your usual query type!

Doobie

import zio._
import doobie.implicits._
import io.github.gaelrenoux.tranzactio._
import io.github.gaelrenoux.tranzactio.doobie._

val list: ZIO[Connection, DbException, List[String]] = tzio {
    sql"SELECT name FROM users".query[String].to[List]
}

Anorm

Since Anorm doesn't provide an IO monad (or even a specific query type), tzio will provide the JDBC connection you need to run a query. The operation will be wrapped in a ZIO (as a blocking effect).

import zio._
import anorm._
import io.github.gaelrenoux.tranzactio._
import io.github.gaelrenoux.tranzactio.anorm._

val list: ZIO[Connection, DbException, List[String]] = tzio { implicit c =>
    SQL"SELECT name FROM users".as(SqlParser.str(1).*)
}

Running the transaction (or using auto-commit)

The Database module (from the same package as tzio) contains the methods needed to provide the Connection and run the transactions.

Here are some examples with Doobie. The code for Anorm is identical, except it has a different import: io.github.gaelrenoux.tranzactio.anorm._ instead of io.github.gaelrenoux.tranzactio.doobie._.

import io.github.gaelrenoux.tranzactio._
import io.github.gaelrenoux.tranzactio.doobie._
import zio._
import zio.console.Console

// Let's start with a very simple one. Connection exceptions are transformed into defects.
val zio: ZIO[Connection, String, Long] = ???
val simple: ZIO[Database, String, Long] = Database.transactionOrDie(zio)

// If you have an additional environment, it would end up on the resulting effect as well.
val zioEnv: ZIO[Connection with Console, String, Long] = ???
val withEnv: ZIO[Database with Console, String, Long] = Database.transactionOrDie(zioEnv)

// Do you want to handle connection errors yourself? They will appear on the Left side of the Either.
val withSeparateErrors: ZIO[Database, Either[DbException, String], Long] = Database.transaction(zio)

// Are you only expecting errors coming from the DB ? Let's handle all of them at the same time.
val zioDbEx: ZIO[Connection, DbException, Long] = ???
val withDbEx: ZIO[Database, DbException, Long] = Database.transactionOrWiden(zioDbEx)

// Or maybe you're just grouping all errors together as exceptions.
val zioEx: ZIO[Connection, java.io.IOException, Long] = ???
val withEx: ZIO[Database, Exception, Long] = Database.transactionOrWiden(zioEx)

// You can also commit even on a failure (only rollbacking on a defect). Useful if you're using the failure channel for short-circuiting!
val commitOnFailure: ZIO[Database, String, Long] = Database.transactionOrDie(zio, commitOnFailure = true)

// And if you're actually not interested in a transaction, you can just auto-commit all queries.
val zioAutoCommit: ZIO[Database, String, Long] = Database.autoCommitOrDie(zio)

Providing the Database

The Database methods return a ZIO instance which requires a Database as an environment. This module is provided as usual through a ZLayer.

The most common way to construct a Database is using a javax.sql.DataSource, which your connection pool implementation (like HikariCP) should provide. Alternatively (e.g. in a test environment), you can create a DataSource manually.

The layer to build a Database from a javax.sql.DataSource is on the Database object. Here's an example for Doobie. Again, the code for Anorm is identical, except it has a different import: io.github.gaelrenoux.tranzactio.anorm._ instead of io.github.gaelrenoux.tranzactio.doobie._.

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

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

More code samples

Find more in src/main/samples, or look below for some details.

Detailed documentation

Version compatibility

The table below indicates for each version of TranzactIO, the versions of ZIO or libraries it's been built with. Check the backward compatibility information on those libraries to check which versions TranzactIO can support.

TranzactIO Scala ZIO Doobie Anorm
0.1.0 2.13 1.0.0-RC17 0.8.6 -
0.2.0 2.13 1.0.0-RC18-2 0.8.6 -
0.3.0 2.13 1.0.0-RC18-2 0.8.6 2.6.5
0.4.0 2.13 1.0.0-RC19-2 0.9.0 2.6.5
0.5.0 2.13 1.0.0-RC20 0.9.0 2.6.5
0.6.0 2.13 1.0.0-RC21-1 0.9.0 2.6.5
1.0.0 2.13 1.0.0 0.9.0 2.6.7
1.0.1 2.13 1.0.0 0.9.0 2.6.7
1.1.0 2.13 1.0.3 0.9.2 2.6.7
1.2.0 2.13 1.0.3 0.9.2 2.6.7
1.3.0 2.13 1.0.5 0.9.4 2.6.10
2.0.0 2.13 1.0.5 0.12.1 2.6.10
2.1.0 2.12 2.13 1.0.9 0.13.4 2.6.10
3.0.0 2.12 2.13 1.0.11 1.0.0-RC2 2.6.10
4.0.0 2.12 2.13 2.0.0 1.0.0-RC2 2.6.10
4.1.0 2.12 2.13 3 2.0.2 1.0.0-RC2 2.7.0
4.2.0 2.12 2.13 3 2.0.13 1.0.0-RC2 2.7.0
5.0.1 2.12 2.13 3 2.0.15 1.0.0-RC4 2.7.0
5.1.0 2.12 2.13 3 2.0.21 1.0.0-RC5 2.7.0
5.2.0 2.12 2.13 3 2.0.21 1.0.0-RC5 2.7.0
master 2.12 2.13 3 2.0.21 1.0.0-RC5 2.7.0

Some definitions

Database operations

You will find reference through the documentation to Database operations. Those are the specific operations handled by Tranzactio, that are necessary to interact with a database:

  • openConnection
  • setAutoCommit
  • commitConnection
  • rollbackConnection
  • closeConnection

They correspond to specific methods in the ConnectionSource service. You would not usually address that service directly, going through Database instead.

Error kinds

In TranzactIO, we recognize two kinds of errors relating to the DB: query errors, and connection errors:

Query errors happen when you run a specific query. They can be timeouts, SQL syntax errors, constraint errors, etc. When you have a ZIO[Connection, E, A], E is the type for query errors.

Connection errors happen when you manage connections or transactions: opening connections, creating, commiting or rollbacking transactions, etc. They are not linked to a specific query. They are always reported as a DbException.

Running a query (detailed version)

There are two families of methods on the Database class: transaction and autoCommit. I'll only describe transaction here, keep in mind that there's an identical set of operations with autoCommit instead.

When providing the Connection with Database, you have three variants of the transaction method, which will handle unrecovered connection errors differently.

  • With transaction, the resulting error type is an Either: Right wraps a query error, and Left wraps a connection error. This is the most generic method, leaving you to handle all errors how you see fit.
  • With transactionOrDie, connection errors are converted into defects, and do not appear in the type signature.
  • With transactionOrWiden, the resulting error type will be the closest supertype of the query error type and DbException, and the error in the result may be a query error or a connection error. This is especially useful if your query error type is already DbException or directly Exception, as in the example below.
val zio: ZIO[Connection, E, A] = ???
val result1: ZIO[Database, Either[DbException, E], A] = Database.transaction(zio)
val result2: ZIO[Database, E, A] = Database.transactionOrDie(zio)
// assuming E extends Exception:
val result3: ZIO[Database, Exception, A] = Database.transactionOrWiden(zio)

A frequent case is to have an additional environment on your ZIO monad, e.g.: ZIO[ZEnv with Connection, E, A]. All methods mentioned above will carry over the additional environment:

val zio: ZIO[ZEnv with Connection, E, A] = ???
val result1: ZIO[Database with ZEnv, Either[DbException, E], A] = Database.transaction(zio)
val result2: ZIO[Database with ZEnv, E, A] = Database.transactionOrDie(zio)
// assuming E extends Exception:
val result3: ZIO[Database with ZEnv, Exception, A] = Database.transactionOrWiden(zio)

All the transaction methods take an optional argument commitOnFailure (which defaults to false). If true, the transaction will be committed on a failure (the E part in ZIO[R, E, A]), and will still be rollbacked on a defect. Obviously, this argument does not exist on the autoCommit methods.

Finally, all those methods take an optional implicit argument of type ErrorStrategies. See Handling connection errors below for details.

Handling connection errors (retries and timeouts)

TranzactIO provides no specific error handling for query errors. Since you, as the developer, have direct access to the ZIO instance representing the query (or aggregation of queries), it's up to you to add timeouts or retries, recover from errors, etc. However, you do not have access to the connection errors, which are hidden in the ConnectionSource and Database modules.

The error handling on connection errors is set up through an ErrorStrategies instance. An ErrorStrategies is a group of ErrorStrategy instances, one for each of the database operations (openConnection, setAutoCommit, etc.)

Passing ErrorStrategies

TranzactIO looks for an ErrorStrategies in three different places, in order:

  • You can pass an implicit ErrorStrategies parameter when calling the Database methods. If no implicit value is provided, it will defer to the next mechanism.
  • When declaring the Database or ConnectionSource layer, you can pass an ErrorStrategies as a parameter.
  • If no ErrorStrategies is defined either as an implicit parameter or in the layer definition, default is ErrorStrategies.Nothing: no retries and no timeouts.
implicit val es: ErrorStrategies = ErrorStrategies.retryForeverFixed(10.seconds)
Database.transaction(???) // es is passed implicitly to the method
val es: ErrorStrategies = ErrorStrategies.retryForeverFixed(10.seconds)
val dbLayerFromDatasource: ZLayer[DataSource, Nothing, Database] = Database.fromDatasource(es)

Defining an ErrorStrategies instance

To define an ErrorStrategies, start from the companion object, then add the retries and timeouts you want to apply. Note that the operations are applied in the order you gave them (a timeout defined after a retry will apply a maximum duration to the retrying effect).

val es: ErrorStrategies = ErrorStrategies.timeout(3.seconds).retryCountExponential(10, 1.second, maxDelay = 10.seconds)
val es2: ErrorStrategies = ErrorStrategies.timeout(3.seconds).retryForeverFixed(1.second).timeout(1.minute)

If you want a specific strategy for some operation, you can set the singular ErrorStrategy manually:

val es: ErrorStrategies =
  ErrorStrategies.timeout(3.seconds).retryCountExponential(10, 1.second, maxDelay = 10.seconds)
    .copy(closeConnection = ErrorStrategy.retryForeverFixed(1.second)) // no timeout and fixed delay for closeConnection

Important caveat regarding timeouts

I strongly recommend that for timeouts, you use the mechanisms on your data source (or database) as you primary mechanism, and only use Tranzactio's timeouts as a backup if needed.

This is especially important for the openConnection operation: you should never have a timeout over this in TranzactIO, as it could lead to connection leaks! For example, your app may encounter a timeout and abort the effect, but the data source is still going through and ends up providing a connection, which is lost. Therefore, timeouts defined at the top-level of error strategies (as the first examples above) will not apply to openConnection (note that this only applies to timeouts, retries will indeed be applied to openConnection).

If after everything I said you find yourself wanting a headache, you can still define a timeout on openConnection by defining the corresponding ErrorStrategy manually.

// THIS IS A BAD IDEA, DON'T DO THIS.
val es: ErrorStrategies =
  ErrorStrategies.timeout(3.seconds).retryForeverFixed(1.second)
    .copy(openConnection = ErrorStrategy.timeout(3.seconds).retryForeverFixed(1.second))

Streaming

When the wrapped framework handle streaming, you can convert the framework's stream to a ZStream using tzioStream. To provide a Connection to the ZIO stream, you can either consume the stream into a ZIO first (then use the same functions as above), or use the Database's streaming methods.

The methods transactionOrDieStream and autoCommitStream work in the same way as the similar, non-stream method. Note that for transactions, only the OrDie variant exists: this is because ZIO's acquire-release mechanism for stream does not allow to pass errors that occur during the acquire-release phase in the error channel. Defects in the stream due to connection errors can only be caught after the ZStream has been consumed into a ZIO.

import io.github.gaelrenoux.tranzactio.doobie._
import zio._
val queryStream: ZStream[Connection, Error, Person] = tzioStream { sql"""SELECT given_name, family_name FROM person""".query[Person].stream }.mapError(transform)
val zStream: ZStream[Database, DbException, Person] = Database.transactionOrDieStream(queryStream)

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

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.

import io.github.gaelrenoux.tranzactio.doobie._
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. This might be useful for testing, or if you want to manually manage that connection.

For that purpose, you can use the layer ConnectionSource.fromConnection. This layer requires a single JDBC Connection, and provides a ConnectionSource module. You must then use the Database.fromConnectionSource layer to get the Database module.

Note that this ConnectionSource does not allow for concurrent usage, as that would lead to undeterministic results. For example, some operation might close a transaction while a concurrent operation is between queries! The non-concurrent behavior is enforced through a ZIO semaphore.

Unit Testing

When unit testing, you typically use ZIO.succeed for your queries, instead of an actual SQL query. However, the type signature still requires a Database, which you need to provide. Database.none exists for this purpose: it satisfies the compiler, but does not provide a usable Database (so don't try to run any actual SQL queries against it).

import zio._
import doobie.implicits._
import io.github.gaelrenoux.tranzactio.DbException
import io.github.gaelrenoux.tranzactio.doobie._

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[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.

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[DataSource, Nothing, Database] = Database.fromDatasource

FAQ

I've got a compiler error: a type was inferred to be Any

Happens quite a lot when using ZIO, because Any is used to mark an 'empty' environment. The best thing to do is to drop this warning from your configuration. See zio/zio#6455.

I'm getting NoClassDefFoundError on cats/FlatMapArityFunctions

Check that your project has org.typelevel:cats-core as a dependency. See zio/interop-cats#669 for more details about this issue.

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), but I'm completely unfamiliar with it.

Slick, however, is a problem. I know it quite well, tried to implement a TranzactIO module for it, and couldn't. Transactions cannot be handled externally using Slick. I don't think it's doable until this ticket is done: slick/slick#1563

How do I migrate to 5.x?

Versions starting at 5.0.0 have split artifacts for Anorm and Doobie. If you were using a 4.x version or below, you need to update your project's dependencies.

// Before
// libraryDependencies += "io.github.gaelrenoux" %% "tranzactio" % TranzactIOVersion

// For Doobie
libraryDependencies += "io.github.gaelrenoux" %% "tranzactio-doobie" % TranzactIOVersion

// For Anorm
libraryDependencies += "io.github.gaelrenoux" %% "tranzactio-anorm" % TranzactIOVersion

tranzactio's People

Contributors

endertunc avatar fpatin avatar gaelrenoux avatar giabao avatar hmemcpy avatar pellekrogholt avatar skress avatar strokyl avatar thanhbv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tranzactio's Issues

Getting this error with ZIO 1.0.0

(I added tranzactio 0.6.0 in my sbt, upgraded zio to 1.0.0 and cats-interop to 2.1.4.0):

Exception in thread "main" java.lang.NoClassDefFoundError: zio.Has$AreHas$
	at io.github.gaelrenoux.tranzactio.DatabaseModuleBase.<init>(DatabaseModuleBase.scala:48)
	at io.github.gaelrenoux.tranzactio.doobie.package$Database$.<init>(package.scala:27)
	at io.github.gaelrenoux.tranzactio.doobie.package$Database$.<clinit>(package.scala:27)

I'll try to make a small repro...

Add support of multiple databases

I worked with the tranzactio a long time ago, and I remember an issue I could not overcome: I had more than one database and I wanted to combine two ZIOs that required two connections to two different databases and then do transactionOrDie for each of the connection separately. This is just a prototype, but I wanted something like this:


type ConnectionOne = Connection[DatabaseOne]
type ConnectionTwo = Connection[DatabaseTwo]

val list: ZIO[ConnectionOne, DbException, List[String]] = tzio {
  sql"SELECT name FROM users".query[String].to[List]
}

val list2: ZIO[ConnectionTwo, DbException, List[String]] = tzio {
  sql"SELECT name FROM users".query[String].to[List]
}

val combined:ZIO[ConnectionOne && ConnectionTwo, DbException, List[String]] = list *> list2

val foo: ZIO[DatabaseOne && DatabaseTwo, String, Long] = Database.transactionOrDie(combined)


I don't think something like this is doable in scala 2.13, but maybe in scala 3 is possible.

Testing with TranzactIO

TranzactIO looks really interesting library, great work! 🙂 I'm looking at solving exactly the kind of problem you've described where I can complete my transaction after performing multiple DB operations and various bits if business logic.

My question is around testing, is there a recommended approach when using TranzactIO? If my database classes return a RIO[Connection, A] and my core service logic completes that transaction, resulting in a RIO[Database, A] it would be great to be able to provide implementations of Connection and Database that don't require a running SQL database.

Handling multiple databases

Looking for direction on handling multiple databases. For example, we have an application that reads from Postgres and Snowflake.
Would the best approach be to make multiple package objects extending wrapper(one for each DB) or is there a more elegant way to do this?

question about transaction management?

Thanks for the tranzactio code, I experimented a bit on a fork against a local postgres, but wasn't sure if creating a PR for a question was pertinent?
I have a running REST-ZIO-Doobie-HikariTransactor-project, and I would like to make it more clear in terms of transaction management.
But with tranzactio, if I add a unique constraint and duplicate insert of alexander in an AppLayer, should each TranzactIO[_]/tzio be a transaction, or the whole thing.
_ <- PersonQueries.insert(Person("Alexander", "Harris")).orElse(Task.succeed(()))
Looks like everything is rolled back including the DDL, but then I don't know how have that happen per REST-service-request.
Nor how to have something like orElse catch an error and actually compensating with a DML (eg inserting on an error message log or status)
Anyway, any help is appreciated :)

Upgrade zio-interop-cats version

The current zio-interop-cats version in the project has no support for ZIO 2, hence there are no implicit found errors when constructing the Transactor[Task] for Doobie.
`

Upgrade to ZIO 2.0

ZIO 2.0 is at Milestone 4, with an RC expected in the next few weeks.
https://github.com/zio/zio/releases/tag/v2.0.0-M4

The API is nearly stable at this point, so any early migration work against this version should pay off towards the official 2.0 release.

The progress is being tracked here:
zio/zio#5470

The Stream Encoding work in progress is the only area where the API might still change before the RC.

We are actively working on a ScalaFix rule that will cover the bulk of the simple API changes:
https://github.com/zio/zio/blob/series/2.x/scalafix/rules/src/main/scala/fix/Zio2Upgrade.scala
We highly recommend starting with that, and then working through any remaining compilation errors :)

To assist with the rest of the migration, we have created this guide:
https://zio.dev/howto/migrate/zio-2.x-migration-guide/

If you would like assistance with the migration from myself or other ZIO contributors, please let us know!

Hide Connection under opaque type alias of some sort

Hey, thanks for your work! I have hit the same issues as you did in my app and I will be interested in using tranzactio once it's released.

In the meantime, I wanted to suggest (or rather start a discussion about) hiding java.sql.Connection from the user. This type is completely unsafe and accessing it may cause undefined behavior (e.g. you can close a connection in the middle of a transaction or commit/rollback it.

So we could hide it by exposing something like TransactionToken without any methods on it and no way to create other than with tranzactio.

What do you think? Have you considered such a design?

Remove the somethingR methods

With ZIO 2.0.0, we can finally get rid of the somethingR (transactionR for example) methods, as the type inferer is now able to pick up the correct types when calling the generic method on a ZIO which only has a Connection as the environment.

We used to have:

  • def transaction[E, A](z: ZIO[Connection, E, A]): ZIO[Database, E, A]
  • def transactionR[R, E, A](z: ZIO[R with Connection], E, A]): ZIO[R with Database, E, A]

We can now have only the second one (renaming it simply transaction), and R will be infered correctly. However… When using it on a ZIO[Connection, E, A], it will infer R to be Any, which triggers a Scala compiler warning: a type was inferred to be Any; this may indicate a programming error..

I'm not sure which is the least annoying API. Keep the R methods, or do with the Scala warning ?

Remove *R methods

Hey, sorry to bump this, but I wanted to share something we use internally:

  def transaction[E, A](tx: ZIO[Connection, E, A]): ZIO[Transactor, E, A] =
    Database.transactionOrDie(tx)

  def transaction[R, E, A](tx: ZIO[Connection with R, E, A])(implicit ev: R <:< Has[_]): ZIO[Transactor with R, E, A] =
    Database.transactionOrDieR(tx)

A very thin wrapper, delegating to tranzactio. I was able to create two overloads called transaction. Normally this doesn't work, since [E, A] and [R, E, A] have the same erasure, but turns out that sticking an implicit causes them to be different as far as Scala is concerned!

So I can have my cake and eat it too - a single transaction function (from the user's perspective) that handles either Connection or Connection with R.

Not sure if that ship had sailed (now that you've published v2.0), but maybe you could use this trick and remove the R overloads completely! :)

Originally posted by @hmemcpy in #24 (comment)

[doobie][scala3] ZEnvironment missings layer

With the following code (scala-cli):

//> using scala "3.2.1"
//> using lib "dev.zio::zio:2.0.5"
//> using lib "io.github.gaelrenoux::tranzactio:4.1.0"
//> using lib "org.tpolecat::doobie-core:1.0.0-RC2"
//> using lib "org.tpolecat::doobie-postgres:1.0.0-RC2"

import zio._
import javax.sql.DataSource

import io.github.gaelrenoux.tranzactio.doobie._
import io.github.gaelrenoux.tranzactio.doobie.{ Database => TZDatabase }
import org.postgresql.ds.PGSimpleDataSource

object Database {
  val dsLayer: ULayer[DataSource] = ZLayer(ZIO.succeed(new PGSimpleDataSource()))
  val live: ULayer[TZDatabase]    = dsLayer >>> TZDatabase.fromDatasource
}

case class Service(database: TZDatabase) {
  def method: Task[Int] = ZIO.succeed(1)
}

object Service {
  def live: RLayer[TZDatabase, Service] = ZLayer.fromFunction(Service(_))
}

object Repro extends ZIOAppDefault {
  val run = ZIO.serviceWithZIO[Service](_.method.debug).provide(Service.live, Database.live)
}

And this triggers the following error:

timestamp=2022-12-07T08:44:57.396552Z level=ERROR thread=#zio-fiber-0 message="" cause="Exception in thread "zio-fiber-4" java.lang.Error: Defect in zio.ZEnvironment: Could not find DatabaseOps::ServiceOps[=transactor::Transactor[=λ %A → ZIO[-Any,+Throwable,+A]]] inside ZEnvironment(DatabaseServiceBase[=transactor$::Transactor[=λ %A → ZIO[-Any,+Throwable,+A]]] -> io.github.gaelrenoux.tranzactio.doobie.package$Database$$anon$4@6c558f81)
      at <empty>.Service.live(repro.scala:46)
      at <empty>.Repro.run(repro.scala:49)
      at <empty>.Repro.run(repro.scala:49)
      at <empty>.Repro.run(repro.scala:49)
      at <empty>.Repro.run(repro.scala:49)"

I'm pretty sure that TZDatabase is a DatabaseOps.ServiceOps[Connection] but there is an issue with tag, subtype and I really don't know how we could tackle that. This might be a ZIO bug as well (using 2.0.5).

I'm pretty confused with the types here, I don't know if there is something we could do to work around that. Also, when switching to scala 2.13 (easily changeable at the top of the file), everything works fine.

Scastie: https://scastie.scala-lang.org/rDWLHazKQqS2bulxpvP9Rw

Problem with restoring connection

Hi,

I observed a problem with restoring the connection when the database is unavailable for a while.
If I turn off the database I see messages Failed to validate connection org.postgresql.jdbc.PgConnection@60795222 (This connection has been closed.). Possibly consider using a shorter maxLifetime value. Then from the application level I will try to make some queries (probably as many as maximum pool size or more) and get io.github.gaelrenoux.tranzactio.DbException$Timeout: Timeout after PT1S exception (which is obvious, because the database is not available).
However, after turning on the database, I still see the same error when trying to make a connection. Interestingly, I see on the database that the application reconnected, so HikariCP reconnection works correctly, but the application does not see a new connection. So it looks like it is not a problem with reconnections (maybe something with execution context)

Option to commit transaction on a failure

Either through a parameter on the Database.transaction methods, or through another set of methods: have a method that would commit the transaction even on a Failure, but not on a Defect.

Reasoning: failures are sometimes used for early return. Right now, a workaround is possible by recovering errors before calling the transaction method, but that's a bit of boilerplate.

May be we can deprecate this repository

I also use this repo
But was unable with upgrade to codebase working with cats-effect 3
And finally I just did a small change and all library does not make sense to be used in my project
Very little amount of code is required

import doobie.Transactor
import doobie.implicits._
import zio.interop.catz._
import zio.{Has, Task, ZIO}

package object repository {
  type TranzactIO[T] = ZIO[Has[Transactor[Task]], Throwable, T]

  def tzio[T](c: doobie.ConnectionIO[T]): TranzactIO[T] = {
    for {
      tx <- ZIO.service[Transactor[Task]]
      res <- c.transact(tx)
               .foldM(
                 e => Task.fail(e),
                 r => Task.succeed(r)
               )
    } yield res
  }
}

Scala 3 artefacts?

Is there a reason why this is not published for Scala3?

any chance it could be?

Lack of support for doobie streams

Noticed this when trying to work with fs2.Streams returned by doobie.

Previously my code had something like this on order to try and convert an fs2.Stream[ConnectionIO, T]] into a ZStream

fragment
  .queryWithLogHandler[T](lh)
  .streamWithChunkSize(512)
  .translate(new (Query ~> TZIO) { def apply[A](a: Query[A]): TZIO[A] = tzio(a) })
  .toZStream(512)
  .mapError(DbException.Wrapped)

But what I found at runtime is that the stream was executing in internal streaming mode vs final streaming (it is trying to load the entire result into memory before it begins processing the stream).

If you check out the doobie docs, in order to get final streaming one must call the transactor on the stream itself before it is compiled. Looking at doobie.syntax.StreamOps we see that a different transformation is called: xa.transP.apply(fa)

I created this helper in my code that should resolve this.

final def tzioS[A](q: fs2.Stream[Query, A]): ZStream[Connection, DbException, T] =
    ZStream.accessStream[Connection] { c =>
      c.get.transP(zio.interop.catz.monadErrorInstance).apply(q).toZStream(512)
    }.mapError(DbException.Wrapped)

Is this something we might consider adding to this project? Do you see any other reason why final streaming may not be working properly?

Using Doobie version >= 1.0.0-RC3 causes java.lang.NoSuchMethodError

When using Doobie with version 1.0.0.-RC3 or RC4, running a query produces this error

[info] timestamp=2023-06-27T07:28:34.665976Z level=ERROR thread=#zio-fiber-1 message="" cause="Exception in thread "zio-fiber-4" java.lang.NoSuchMethodError: 'doobie.free.KleisliInterpreter doobie.free.KleisliInterpreter$.apply(doobie.WeakAsync)'
[info] 	at io.github.gaelrenoux.tranzactio.doobie.package$Database$.$anonfun$connectionFromJdbc$1(package.scala:50)

With 1.0.0-RC2 it works just fine

Using tzio with single connections

Do you have a use case where you can't get a DataSource? Notify me by creating an issue!

Ok, here we go! :)

In my application, I make use of Postgres advisory locks. They can either be tied to a session or a transaction, and will be freed automatically in both cases.

My use case is using advisory locks for a session (connection), in order to make sure that a background worker is only run on a single application instance in order to avoid race conditions.

When I was using doobie without tzio I could simply create a Transactor wrapping a single sql.Connection, and thus get excellent control over the scope of my locks. However, since tzio builds upon DataSource, doing this seems a bit tricker. What I'm currently thinking is to write a custom implementation ConnectionSource.Service that just no-ops e.g closeConnection and handle the setup/teardown solely in the ZLayer initializing the connection, but it would be handy if it was easier to go from a single sql.Connection to a tzio.Database in the (few) cases that's useful.

Another use case I can think if is LISTEN / NOTIFY in Postgres, which is also maintains per-connection state. Even if it doesn't work great with jdbc.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.