diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5ef40..3faf0a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Only accretive/fixative changes will be made from now on. The following changes have been committed to the **master** branch since the 1.0.0 release: +* 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. ## Stable Builds diff --git a/README.md b/README.md index 2b48ef6..b011393 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The latest versions on Clojars and on cljdoc: [![Clojars Project](https://clojars.org/seancorfield/next.jdbc/latest-version.svg)](https://clojars.org/seancorfield/next.jdbc) [![cljdoc badge](https://cljdoc.org/badge/seancorfield/next.jdbc)](https://cljdoc.org/d/seancorfield/next.jdbc/CURRENT) -This documentation is for the 1.0.0 release -- [see the CHANGELOG](CHANGELOG.md). +This documentation is for **master**, beyond the 1.0.0 release -- [see the CHANGELOG](CHANGELOG.md). * [Getting Started](/doc/getting-started.md) * [Migrating from `clojure.java.jdbc`](/doc/migration-from-clojure-java-jdbc.md) diff --git a/doc/transactions.md b/doc/transactions.md index dbaaa14..d00826a 100644 --- a/doc/transactions.md +++ b/doc/transactions.md @@ -2,17 +2,26 @@ The `transact` function and `with-transaction` macro were briefly mentioned in the [Getting Started](/doc/getting-started.md) section but we'll go into more detail here. -Although `(transact transactable thunk)` is available, it is expected that you will mostly use `(with-transaction [tx transactable] body...)` when you want to execute multiple SQL operations in the context of a single transaction so that is what this section focuses on. +Although `(transact transactable f)` is available, it is expected that you will mostly use `(with-transaction [tx transactable] body...)` when you want to execute multiple SQL operations in the context of a single transaction so that is what this section focuses on. + +## Connection-level Control By default, all connections that `next.jdbc` creates are automatically committable, i.e., as each operation is performed, the effect is committed to the database directly before the next operation is performed. Any exceptions only cause the current operation to be aborted -- any prior operations have already been committed. 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)`). +## 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: ```clojure (jdbc/with-transaction [tx my-datasource] (jdbc/execute! tx ...) + (jdbc/execute! tx ...)) ; will commit, unless exception thrown + +(jdbc/with-transaction [tx my-datasource] + (jdbc/execute! tx ...) + (when ... (throw ...)) ; will rollback (jdbc/execute! tx ...)) ``` @@ -24,4 +33,18 @@ 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. +## 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: + +```clojure +(jdbc/with-transaction [tx my-datasource] + (let [result (jdbc/execute! tx ...)] + (if ... + (do + (.rollback tx) + result) + (jdbc/execute! tx ...)))) +``` + [<: Prepared Statements](/doc/prepared-statements.md) | [All The Options :>](/doc/all-the-options.md) diff --git a/src/next/jdbc.clj b/src/next/jdbc.clj index 5ab4876..cc4c436 100644 --- a/src/next/jdbc.clj +++ b/src/next/jdbc.clj @@ -203,4 +203,5 @@ * `:read-only` -- `true` / `false`, * `:rollback-only` -- `true` / `false`." [[sym transactable opts] & body] - `(transact ~transactable (^{:once true} fn* [~sym] ~@body) ~opts)) + (let [con (vary-meta sym assoc :tag 'java.sql.Connection)] + `(transact ~transactable (^{:once true} fn* [~con] ~@body) (or ~opts {})))) diff --git a/src/next/jdbc/specs.clj b/src/next/jdbc/specs.clj index 18b56a9..3c56068 100644 --- a/src/next/jdbc/specs.clj +++ b/src/next/jdbc/specs.clj @@ -90,7 +90,7 @@ :args (s/cat :binding (s/and vector? (s/cat :sym simple-symbol? :transactable ::transactable - :opts ::opts-map)) + :opts (s/? ::opts-map))) :body (s/* any?))) (s/fdef sql/insert! diff --git a/src/next/jdbc/transaction.clj b/src/next/jdbc/transaction.clj index 2e40e2e..0c14431 100644 --- a/src/next/jdbc/transaction.clj +++ b/src/next/jdbc/transaction.clj @@ -3,8 +3,7 @@ (ns next.jdbc.transaction "SQL Transaction logic." (:require [next.jdbc.protocols :as p]) - (:import (java.sql Connection - SQLException))) + (:import (java.sql Connection))) (set! *warn-on-reflection* true) diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index d00f6ef..1ca259c 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -110,13 +110,32 @@ VALUES ('Pear', 'green', 49, 47) "])) {:rollback-only true}))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) - (testing "with-transaction" + (testing "with-transaction rollback-only" (is (= [{:next.jdbc/update-count 1}] (jdbc/with-transaction [t (ds) {:rollback-only true}] (jdbc/execute! t [" INSERT INTO fruit (name, appearance, cost, grade) VALUES ('Pear', 'green', 49, 47) "])))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (testing "with-transaction exception" + (is (thrown? Throwable + (jdbc/with-transaction [t (ds)] + (jdbc/execute! t [" +INSERT INTO fruit (name, appearance, cost, grade) +VALUES ('Pear', 'green', 49, 47) +"]) + (throw (ex-info "abort" {}))))) + (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"]))))) + (testing "with-transaction call rollback" + (is (= [{:next.jdbc/update-count 1}] + (jdbc/with-transaction [t (ds)] + (let [result (jdbc/execute! t [" +INSERT INTO fruit (name, appearance, cost, grade) +VALUES ('Pear', 'green', 49, 47) +"])] + (.rollback t) + result)))) (is (= 4 (count (jdbc/execute! (ds) ["select * from fruit"])))))) (deftest plan-misuse