diff --git a/CHANGELOG.md b/CHANGELOG.md index 3faf0a6..a3c58c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The following changes have been committed to the **master** branch since the 1.0 * Fix #37 by adjusting the spec for `with-transaction` to "require less" of the `:binding` vector. * Fix #36 by adding type hint in `with-transaction` macro. * Fix #35 by explaining the database-specific options needed to ensure `insert-multi!` performs a single, batched operation. +* Fix #34 by explaining save points (in the Transactions documentation). ## Stable Builds diff --git a/doc/transactions.md b/doc/transactions.md index d00826a..d18ed51 100644 --- a/doc/transactions.md +++ b/doc/transactions.md @@ -47,4 +47,31 @@ Instead of throwing an exception (which will propagate through `with-transaction (jdbc/execute! tx ...)))) ``` +## Save Points Inside a Transaction + +In general, transactions are per-connection and do not nest in JDBC. If you nest calls to `with-transaction` using a `DataSource` argument (or a db-spec) then you will get separate connections inside each invocation and the transactions will be independent, as permitted by the isolation level. + +If you nest such calls passing a `Connection` instead, the inner call will commit (or rollback) all operations on that connection up to that point -- including any performed in the outer call, prior to entering the inner call. The outer call will then commit (or rollback) any additional operations within its scope. This will be confusing at best and most likely buggy behavior! + +If you want the ability to selectively roll back certain groups of operations inside a transaction, you can use named or unnamed save points: + +```clojure +(jdbc/with-transaction [tx my-datasource] + (let [result (jdbc/execute! tx ...) ; op A + sp1 (.setSavepoint)] ; unnamed save point + + (jdbc/execute! tx ...) ; op B + + (when ... (.rollback tx sp1)) ; just rolls back op B + + (let [sp2 (.setSavepoint "two")] ; named save point + + (jdbc/execute! tx ...) ; op C + + (when ... (.rollback tx sp2))) ; just rolls back op C + + result)) ; returns this and will commit op A + ;; (and ops B & C if they weren't rolled back above) +``` + [<: Prepared Statements](/doc/prepared-statements.md) | [All The Options :>](/doc/all-the-options.md) diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 1ca259c..4a2f7af 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -136,6 +136,28 @@ VALUES ('Pear', 'green', 49, 47) "])] (.rollback t) result)))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (testing "with-transaction with unnamed save point" + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t (ds)] + (let [save-point (.setSavepoint t) + result (jdbc/execute! t [" +INSERT INTO fruit (name, appearance, cost, grade) +VALUES ('Pear', 'green', 49, 47) +"])] + (.rollback t save-point) + result)))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (testing "with-transaction with named save point" + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t (ds)] + (let [save-point (.setSavepoint t (name (gensym))) + result (jdbc/execute! t [" +INSERT INTO fruit (name, appearance, cost, grade) +VALUES ('Pear', 'green', 49, 47) +"])] + (.rollback t save-point) + result)))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))) (deftest plan-misuse