fix #254 by adding active-tx?

This commit is contained in:
Sean Corfield 2023-06-10 11:24:07 -07:00
parent 24d55f3165
commit 06f9baea01
4 changed files with 40 additions and 1 deletions

View file

@ -2,6 +2,9 @@
Only accretive/fixative changes will be made from now on.
* 1.3.next in progress
* Address [#254](https://github.com/seancorfield/next-jdbc/issues/254) by adding `next.jdbc/active-tx?` and adding more explanation to [**Transactions**](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/CURRENT/doc/getting-started/transactions) about the conventions behind transactions and the limitations of thread-local tracking of active transactions in `next.jdbc`.
* 1.3.874 -- 2023-04-15
* Fix [#248](https://github.com/seancorfield/next-jdbc/issues/248) by allowing `:port` to be `:none`.
* Address [#247](https://github.com/seancorfield/next-jdbc/issues/247) by adding examples of using `next.jdbc.connection/jdbc-url` to build a connection string with additional parameters when creating connection pools.

View file

@ -10,6 +10,10 @@ By default, all connections that `next.jdbc` creates are automatically committab
It is possible to tell `next.jdbc` to create connections that do not automatically commit operations: pass `{:auto-commit false}` as part of the options map to anything that creates a connection (including `get-connection` itself). You can then decide when to commit or rollback by calling `.commit` or `.rollback` on the connection object itself. You can also create save points (`(.setSavepoint con)`, `(.setSavepoint con name)`) and rollback to them (`(.rollback con save-point)`). You can also change the auto-commit state of an open connection at any time (`(.setAutoCommit con on-off)`).
This is the machinery behind "transactions": one or more operations on a
`Connection` that are not automatically committed, and which can be rolled back
or committed explicitly at any point.
## Automatic Commit & Rollback
`next.jdbc`'s transaction handling provides a convenient baseline for either committing a group of operations if they all succeed or rolling them all back if any of them fails, by throwing an exception. You can either do this on an existing connection -- and `next.jdbc` will try to restore the state of the connection after the transaction completes -- or by providing a datasource and letting `with-transaction` create and manage its own connection:
@ -35,6 +39,14 @@ You can also provide an options map as the third element of the binding vector (
The latter can be particularly useful in tests, to run a series of SQL operations during a test and then roll them all back at the end.
If you use `next.jdbc/with-transaction` (or `next.jdbc/transact`), then
`next.jdbc` keeps track of whether a "transaction" is in progress or not, and
you can call `next.jdbc/active-tx?` to determine that, in your own code, in
case you want to write code that behaves differently inside or outside a
transaction.
> Note: `active-tx?` only knows about `next.jdbc` transactions -- it cannot track any transactions that you create yourself using the underlying JDBC `Connection`. In addition, this is a **global** state (per thread) and not related to just a single connection, so you can't use this predicate if you are working with multiple databases in the same context.
## Manual Rollback Inside a Transaction
Instead of throwing an exception (which will propagate through `with-transaction` and therefore provide no result), you can also explicitly rollback if you want to return a result in that case:

View file

@ -68,7 +68,7 @@
[next.jdbc.protocols :as p]
[next.jdbc.result-set :as rs]
[next.jdbc.sql-logging :as logger]
[next.jdbc.transaction])
[next.jdbc.transaction :as tx])
(:import (java.sql PreparedStatement)))
(set! *warn-on-reflection* true)
@ -399,6 +399,17 @@
(let [con (vary-meta sym assoc :tag 'java.sql.Connection)]
`(transact ~transactable (^{:once true} fn* [~con] ~@body) ~(or opts {}))))
(defn active-tx?
"Returns true if `next.jdbc` has a currently active transaction in the
current thread, else false.
Note: transactions are a convention of operations on a `Connection` so
this predicate only reflects `next.jdbc/transact` and `next.jdbc/with-transaction`
operations -- it does not reflect any other operations on a `Connection`,
performed via JDBC interop directly."
[]
@#'tx/*active-tx*)
(defn with-options
"Given a connectable/transactable object and a set of (default) options
that should be used on all operations on that object, return a new

View file

@ -217,17 +217,21 @@ VALUES ('Pear', 'green', 49, 47)
{:rollback-only true})))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))
(testing "with-transaction rollback-only"
(is (not (jdbc/active-tx?)) "should not be in a transaction")
(is (= [{:next.jdbc/update-count 1}]
(jdbc/with-transaction [t (ds) {:rollback-only true}]
(is (jdbc/active-tx?) "should be in a transaction")
(jdbc/execute! t ["
INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47)
"]))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
(is (not (jdbc/active-tx?)) "should not be in a transaction")
(with-open [con (jdbc/get-connection (ds))]
(let [ac (.getAutoCommit con)]
(is (= [{:next.jdbc/update-count 1}]
(jdbc/with-transaction [t con {:rollback-only true}]
(is (jdbc/active-tx?) "should be in a transaction")
(jdbc/execute! t ["
INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47)
@ -241,8 +245,10 @@ VALUES ('Pear', 'green', 49, 47)
INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47)
"])
(is (jdbc/active-tx?) "should be in a transaction")
(throw (ex-info "abort" {})))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
(is (not (jdbc/active-tx?)) "should not be in a transaction")
(with-open [con (jdbc/get-connection (ds))]
(let [ac (.getAutoCommit con)]
(is (thrown? Throwable
@ -251,6 +257,7 @@ VALUES ('Pear', 'green', 49, 47)
INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47)
"])
(is (jdbc/active-tx?) "should be in a transaction")
(throw (ex-info "abort" {})))))
(is (= 4 (count (jdbc/execute! con ["select * from fruit"]))))
(is (= ac (.getAutoCommit con))))))
@ -262,8 +269,11 @@ INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47)
"])]
(.rollback t)
;; still in a next.jdbc TX even tho' we rolled back!
(is (jdbc/active-tx?) "should be in a transaction")
result))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
(is (not (jdbc/active-tx?)) "should not be in a transaction")
(with-open [con (jdbc/get-connection (ds))]
(let [ac (.getAutoCommit con)]
(is (= [{:next.jdbc/update-count 1}]
@ -285,8 +295,11 @@ INSERT INTO fruit (name, appearance, cost, grade)
VALUES ('Pear', 'green', 49, 47)
"])]
(.rollback t save-point)
;; still in a next.jdbc TX even tho' we rolled back to a save point!
(is (jdbc/active-tx?) "should be in a transaction")
result))))
(is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))
(is (not (jdbc/active-tx?)) "should not be in a transaction")
(with-open [con (jdbc/get-connection (ds))]
(let [ac (.getAutoCommit con)]
(is (= [{:next.jdbc/update-count 1}]