Open mbezjak opened 3 months ago
I got sidetracked a bit by other things. This is still on my mind. As soon as I have the time, I'll come back to this.
FYI: This is a variant (not merged yet) that we'll likely use in production. I'll report back after we've run it for a while and if we don't see any problems there.
(ns something
(:require
[next.jdbc.protocols :as p]
[next.jdbc.transaction :as tx])
(:import
(java.sql Connection)
(javax.sql DataSource)))
(defonce ^:private ^:dynamic *active-tx* #{})
(defn transact [con body-fn opts]
(@#'tx/transact* con body-fn opts))
(extend-protocol p/Transactable
java.sql.Connection
(-transact [this body-fn opts]
(let [raw (if (.isWrapperFor this Connection)
(.unwrap this Connection)
this)]
(if (contains? *active-tx* raw)
(body-fn this)
(binding [*active-tx* (conj *active-tx* raw)]
(transact this body-fn opts)))))
javax.sql.DataSource
(-transact [this body-fn opts]
(let [raw (if (.isWrapperFor this DataSource)
(.unwrap this DataSource)
this)]
(if (contains? *active-tx* raw)
(with-open [con (p/get-connection this opts)]
(body-fn con))
(binding [*active-tx* (conj *active-tx* raw)]
(with-open [con (p/get-connection this opts)]
(transact con body-fn opts)))))))
Note that the code above was simplified by assuming the :ignore
value of *nested-tx*
.
@mbezjak Seems strange to track DataSource
objects instead of Connection
objects in that second case -- each time you call get-connection
on a DS, you'll get a distinct, new Connection
object.
@seancorfield You are right, of course!
With the code above, the following tests would fail:
(try
(jdbc/with-transaction [tx1 data-source]
(insert-into-foo tx1)
(jdbc/with-transaction [tx2 tx1]
;; Running this SQL shouldn't commit anything! It's as if `with-transaction` block is not
;; there at all.
(is (= 1 (count-foo tx2))))
(throw (Exception. "rollback outer!")))
(catch Exception _))
(is (= 0 (count-foo data-source)))
and
(jdbc/with-transaction [tx1 data-source]
(is (= 0 (count-foo tx1)))
(try
(jdbc/with-transaction [tx2 data-source]
(insert-into-foo tx2)
(is (= 1 (count-foo tx2)))
;; tx2 is a transaction on its own that can be rollbacked. tx1 shouldn't see anything that
;; happened in tx2.
(throw (Exception. "rollback inner!")))
(catch Exception _)))
(is (= 0 (count-foo data-source)))
(I know about {:rollback-only true}
opt, but I went with exceptions anyway.)
With that in mind, the updated Transactables:
(ns something
(:require
[next.jdbc.protocols :as p]
[next.jdbc.transaction :as tx])
(:import
(java.sql Connection)))
(defonce ^:private ^:dynamic *active-tx* #{})
(defn transact [con body-fn opts]
(@#'tx/transact* con body-fn opts))
(extend-protocol p/Transactable
java.sql.Connection
(-transact [this body-fn opts]
(let [raw (if (.isWrapperFor this Connection)
(.unwrap this Connection)
this)]
(if (contains? *active-tx* raw)
(body-fn this)
(binding [*active-tx* (conj *active-tx* raw)]
(transact this body-fn opts)))))
javax.sql.DataSource
(-transact [this body-fn opts]
(with-open [con (p/get-connection this opts)]
(p/-transact con body-fn opts))))
:point_up: is really simple without *nested-tx*
.
As before, we haven't merged any of this yet. It's all waiting for me to play around with transactions some more. I'll report back after we've run it for a while and if we don't see any problems.
Is your feature request related to a problem? Please describe.
Should open two separate transactions because those are two separate data sources, with two separate SQL connections. They just happen to execute in the same thread.
tx2
should be rolled back due an exception.Right now, the above snippet is equivalent to
More context: https://clojurians.slack.com/archives/C1Q164V29/p1720188613556859?thread_ts=1720104756.702669&cid=C1Q164V29
Describe the solution you'd like
next.jdbc.transaction/*active-tx*
should probably be a hash-map java.sql.Connection -> boolean, or simply a hash-set ofjava.sql.Connection
objects.Implementing
java.sql.Connection
is usually done with proxies, so the key to the map above (element in a set) should probably be the result ofunwrap
call. Doing the same tojavax.sql.DataSource
might be needed.For backwards compability,
next.jdbc/active-tx?
should returntrue
if the hash-map/hash-set is not empty. 1-arity variant accepting atransactable
could be introduced.Describe alternatives you've considered
I haven't yet started thinking about how to solve the problem above.