diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e1825..b350794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/doc/transactions.md b/doc/transactions.md index 8c1a59d..61bc285 100644 --- a/doc/transactions.md +++ b/doc/transactions.md @@ -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: diff --git a/src/next/jdbc.clj b/src/next/jdbc.clj index 9f002b5..f61b0e8 100644 --- a/src/next/jdbc.clj +++ b/src/next/jdbc.clj @@ -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 diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index bf888bb..721dcfe 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -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}]