diff --git a/CHANGELOG.md b/CHANGELOG.md index fc7b1eb..0ba6861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Only accretive/fixative changes will be made from now on. * 1.2.next in progress + * Address [#204](https://github.com/seancorfield/next-jdbc/issues/204) by adding `next.jdbc/on-connection`. * Address [#203](https://github.com/seancorfield/next-jdbc/issues/203) by adding a note to the **PostgreSQL Tips & Tricks** section. * Update `build-clj` to v0.8.0. diff --git a/doc/getting-started.md b/doc/getting-started.md index a43f1e4..c0ac672 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -577,6 +577,21 @@ JDBC provides several features that let you introspect the database to obtain li Several methods on `DatabaseMetaData` return a `ResultSet` object, e.g., `.getCatalogs()`, `.getClientInfoProperties()`, `.getSchemas()`. All of those can be handled in a similar manner to the above. See the [Oracle documentation for `java.sql.DatabaseMetaData`](https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/DatabaseMetaData.html) (Java 11) for more details. +If you are working with a generalized datasource that may be a `Connection`, a `DataSource`, +or a wrapped connectable (via something like `with-options` or `with-transaction`), you can +write generic, `Connection`-based code using `on-connection` which will reuse a `Connection` +if one is passed or create a new one if needed (and automatically close it afterward): + +```clojure +(on-connection [con ds] + (-> (.getMetaData con) ; produces java.sql.DatabaseMetaData + ;; return a java.sql.ResultSet describing all tables and views: + (.getTables nil nil nil (into-array ["TABLE" "VIEW"])) + (rs/datafiable-result-set ds opts))) +``` + +> Note: to avoid confusion and/or incorrect usage, you cannot pass options to `on-connection` because they would be ignored in some cases (existing `Connection` or a wrapped `Connection`). + ## Logging Sometimes it is convenient to have database operations logged automatically. `next.jdbc/with-logging` diff --git a/src/next/jdbc.clj b/src/next/jdbc.clj index afe2105..5bb518f 100644 --- a/src/next/jdbc.clj +++ b/src/next/jdbc.clj @@ -339,6 +339,35 @@ (with-open [con (get-connection connectable)] (execute-batch! con sql param-groups opts))))) +(defmacro on-connection + "Given a connectable object, gets a connection and binds it to `sym`, + then executes the `body` in that context. + + This allows you to write generic, `Connection`-based code without + needing to know the exact type of an incoming datasource: + + (on-connection [conn datasource] + (let [metadata (.getMetadata conn) + catalog (.getCatalog conn)] + ...)) + + If passed a `Connection` or a `Connectable` that wraps a `Connection`, + then that `Connection` is used as-is. + + Otherwise, creates a new `Connection` object from the connectable, + executes the body, and automatically closes it for you." + [[sym connectable] & body] + (let [con-sym (vary-meta sym assoc :tag 'java.sql.Connection) + con-obj connectable] + `(cond (instance? java.sql.Connection ~con-obj) + ((^{:once true} fn* [~con-sym] ~@body) ~con-obj) + (and (satisfies? p/Connectable ~con-obj) + (instance? java.sql.Connection (:connectable ~con-obj))) + ((^{:once true} fn* [~con-sym] ~@body) (:connectable ~con-obj)) + :else + (with-open [con# (get-connection ~con-obj)] + ((^{:once true} fn* [~con-sym] ~@body) con#))))) + (defn transact "Given a transactable object and a function (taking a `Connection`), execute the function over the connection in a transactional manner. diff --git a/test/next/jdbc_test.clj b/test/next/jdbc_test.clj index 0bb69fc..e15094f 100644 --- a/test/next/jdbc_test.clj +++ b/test/next/jdbc_test.clj @@ -883,3 +883,15 @@ INSERT INTO fruit (name, appearance) VALUES (?,?) (default-options))))) (is (thrown? IllegalArgumentException (doall (take 3 (jdbc/plan (ds) ["select * from fruit"])))))) + +(deftest issue-204 + (testing "against a Connection" + (is (seq (with-open [con (jdbc/get-connection (ds))] + (jdbc/on-connection [x con] (jdbc/execute! x ["select * from fruit"])))))) + (testing "against a wrapped Connection" + (is (seq (with-open [con (jdbc/get-connection (ds))] + (jdbc/on-connection [x (jdbc/with-options con {})] (jdbc/execute! x ["select * from fruit"])))))) + (testing "against a wrapped Datasource" + (is (seq (jdbc/on-connection [x (jdbc/with-options (ds) {})] (jdbc/execute! x ["select * from fruit"]))))) + (testing "against a Datasource" + (is (seq (jdbc/on-connection [x (ds)] (jdbc/execute! x ["select * from fruit"]))))))