Fixes #75 by adding java.sql.Statement support

This commit is contained in:
Sean Corfield 2019-11-14 16:15:52 -08:00
parent b38afcb501
commit d561a7108e
6 changed files with 97 additions and 6 deletions

View file

@ -6,8 +6,9 @@ Only accretive/fixative changes will be made from now on.
The following changes have been committed to the **master** branch since the 1.0.9 release:
* Fix link to **All The Options** in **Migration from `clojure.java.jdbc`**. PR #71 (@laurio).
* Fix #75 by adding support for `java.sql.Statement` to `plan`, `execute!`, and `execute-one!`.
* Address #74 by making several small changes to satisfy Eastwood.
* Fix link to **All The Options** in **Migration from `clojure.java.jdbc`**. PR #71 (@laurio).
* Address #70 by adding **CLOB & BLOB SQL Types** to the **Tips & Tricks** section of **Friendly SQL Functions** and by adding `next.jdbc.result-set/clob-column-reader` and `next.jdbc.result-set/clob->string` helper to make it easier to deal with `CLOB` column data.
* Clarify what `execute!` and `execute-one!` produce when the result set is empty (`[]` and `nil` respectively, and there are now tests for this). Similarly for `find-by-keys` and `get-by-id`.
* Add **MS SQL Server** section to **Tips & Tricks** to note that returns an empty string for table names by default (so table-qualified column names are not available). Using the `:result-type` (scroll) and `:concurrency` options will cause table names to be returned.

View file

@ -149,7 +149,7 @@ Any operation that can perform key-based lookup can be used here without creatin
## Datasources, Connections & Transactions
In the examples above, we created a datasource and then passed it into each function call. When `next.jdbc` is given a datasource, it creates a `java.sql.Connection` from it, uses it for the SQL operation, and then closes it. If you're not using a connection pooling datasource (see below), that can be quite an overhead: setting up database connections to remote servers is not cheap!
In the examples above, we created a datasource and then passed it into each function call. When `next.jdbc` is given a datasource, it creates a `java.sql.Connection` from it, uses it for the SQL operation (by creating and populating a `java.sql.PreparedStatement` from the connection and the SQL string and parameters passed in), and then closes it. If you're not using a connection pooling datasource (see below), that can be quite an overhead: setting up database connections to remote servers is not cheap!
If you want to run multiple SQL operations without that overhead each time, you can create the connection yourself and reuse it across several operations using `with-open` and `next.jdbc/get-connection`:
@ -183,6 +183,17 @@ If `with-transaction` is given a datasource, it will create and close the connec
You can read more about [working with transactions](/doc/transactions.md) further on in the documentation.
### Prepared Statement Caveat
*Note: Not all databases support using a `PreparedStatement` for every type of SQL operation. You might have to create a `java.sql.Statement` yourself, directly from a `java.sql.Connection` and use that, without parameters, in `plan`, `execute!`, or `execute-one!`. See the following example:*
```clojure
(with-open [con (jdbc/get-connection ds)]
(jdbc/execute! (.createStatement con) ["...just a SQL string..."])
(jdbc/execute! con ["...some SQL..." "and" "parameters"]) ; uses PreparedStatement
(into [] (map :column) (jdbc/plan (.createStatement con) ["..."])))
```
## Connection Pooling
`next.jdbc` makes it easy to use either HikariCP or c3p0 for connection pooling.

View file

@ -2,6 +2,8 @@
Under the hood, whenever you ask `next.jdbc` to execute some SQL it creates a `java.sql.PreparedStatement`, adds in the parameters you provide, and then calls `.execute` on it. Then it attempts to get a `ResultSet` from that and either return it or process it. If you asked for generated keys to be returned, that `ResultSet` will contain those generated keys if your database supports it, otherwise it will be whatever the `.execute` function produces. If no `ResultSet` is available at all, `next.jdbc` will ask for the count of updated rows and return that as if it were a result set.
*Note: Some databases do not support all SQL operations via `PreparedStatement`, in which case you may need to create a `java.sql.Statement` and pass that into `plan`, `execute!`, or `execute-one!`, along with the SQL you wish to execute. Note that such statement execution may not have parameters. See the [Prepared Statement Caveat in Getting Started](/doc/getting-started.md#prepared-statement-caveat) for an example.*
If you have a SQL operation that you intend to run multiple times on the same `java.sql.Connection`, it may be worth creating the prepared statement yourself and reusing it. `next.jdbc/prepare` accepts a connection and a vector of SQL and optional parameters and returns a `java.sql.PreparedStatement` which can be passed to `plan`, `execute!`, or `execute-one!` as the first argument. It is your responsibility to close the prepared statement after it has been used.
If you need to pass an option map to `plan`, `execute!`, or `execute-one!` when passing a prepared statement, you must pass `nil` or `[]` as the second argument:

View file

@ -19,6 +19,7 @@
(:import (java.sql Clob
PreparedStatement
ResultSet ResultSetMetaData
Statement
SQLException)
(java.util Locale)))
@ -532,6 +533,38 @@
init')))
(f init {:next.jdbc/update-count (.getUpdateCount stmt)})))
(defn- stmt-sql->result-set
"Given a `Statement`, a SQL command, and options, execute it and return a
`ResultSet` if possible."
^ResultSet
[^Statement stmt ^String sql opts]
(if (.execute stmt sql)
(.getResultSet stmt)
(when (:return-keys opts)
(try
(.getGeneratedKeys stmt)
(catch Exception _)))))
(defn- reduce-stmt-sql
"Execute the SQL command on the given `Statement`, attempt to get either
its `ResultSet` or its generated keys (as a `ResultSet`), and reduce
that using the supplied function and initial value.
If the statement yields neither a `ResultSet` nor generated keys, return
a hash map containing `:next.jdbc/update-count` and the number of rows
updated, with the supplied function and initial value applied."
[^Statement stmt sql f init opts]
(if-let [rs (stmt-sql->result-set stmt sql opts)]
(let [rs-map (mapify-result-set rs opts)]
(loop [init' init]
(if (.next rs)
(let [result (f init' rs-map)]
(if (reduced? result)
@result
(recur result)))
init')))
(f init {:next.jdbc/update-count (.getUpdateCount stmt)})))
(extend-protocol p/Executable
java.sql.Connection
(-execute [this sql-params opts]
@ -618,6 +651,34 @@
(datafiable-result-set rs (.getConnection this) opts)
[{:next.jdbc/update-count (.getUpdateCount this)}]))
java.sql.Statement
;; we can't tell if this PreparedStatement will return generated
;; keys so we pass a truthy value to at least attempt it if we
;; do not get a ResultSet back from the execute call
(-execute [this sql-params opts]
(assert (= 1 (count sql-params))
"Parameters cannot be provided when executing a non-prepared Statement")
(reify clojure.lang.IReduceInit
(reduce [_ f init]
(reduce-stmt-sql this (first sql-params) f init (assoc opts :return-keys true)))
(toString [_] "`IReduceInit` from `plan` -- missing reduction?")))
(-execute-one [this sql-params opts]
(assert (= 1 (count sql-params))
"Parameters cannot be provided when executing a non-prepared Statement")
(if-let [rs (stmt-sql->result-set this (first sql-params) (assoc opts :return-keys true))]
(let [builder-fn (get opts :builder-fn as-maps)
builder (builder-fn rs opts)]
(when (.next rs)
(datafiable-row (row-builder builder)
(.getConnection this) opts)))
{:next.jdbc/update-count (.getUpdateCount this)}))
(-execute-all [this sql-params opts]
(assert (= 1 (count sql-params))
"Parameters cannot be provided when executing a non-prepared Statement")
(if-let [rs (stmt-sql->result-set this (first sql-params) opts)]
(datafiable-result-set rs (.getConnection this) opts)
[{:next.jdbc/update-count (.getUpdateCount this)}]))
Object
(-execute [this sql-params opts]
(p/-execute (p/get-datasource this) sql-params opts))

View file

@ -93,19 +93,19 @@
(s/fdef jdbc/plan
:args (s/alt :prepared (s/cat :stmt ::prepared-statement)
:sql (s/cat :connectable ::connectable
:sql-params ::sql-params
:sql-params (s/nilable ::sql-params)
:opts (s/? ::opts-map))))
(s/fdef jdbc/execute!
:args (s/alt :prepared (s/cat :stmt ::prepared-statement)
:sql (s/cat :connectable ::connectable
:sql-params ::sql-params
:sql-params (s/nilable ::sql-params)
:opts (s/? ::opts-map))))
(s/fdef jdbc/execute-one!
:args (s/alt :prepared (s/cat :stmt ::prepared-statement)
:sql (s/cat :connectable ::connectable
:sql-params ::sql-params
:sql-params (s/nilable ::sql-params)
:opts (s/? ::opts-map))))
(s/fdef jdbc/transact

View file

@ -124,7 +124,23 @@
ps (jdbc/prepare
con
["select * from fruit where id = ?"])]
(jdbc/execute! (prep/set-parameters ps [4])))]
(jdbc/execute! (prep/set-parameters ps [4]) nil {}))]
(is (every? map? rs))
(is (every? meta rs))
(is (= 1 (count rs)))
(is (= 4 ((if (postgres?) :fruit/id :FRUIT/ID) (first rs))))))
(testing "statement"
(let [rs (with-open [con (jdbc/get-connection (ds))]
(jdbc/execute! (.createStatement con)
["select * from fruit order by id"]))]
(is (every? map? rs))
(is (every? meta rs))
(is (= 4 (count rs)))
(is (= 1 ((if (postgres?) :fruit/id :FRUIT/ID) (first rs))))
(is (= 4 ((if (postgres?) :fruit/id :FRUIT/ID) (last rs)))))
(let [rs (with-open [con (jdbc/get-connection (ds))]
(jdbc/execute! (.createStatement con)
["select * from fruit where id = 4"]))]
(is (every? map? rs))
(is (every? meta rs))
(is (= 1 (count rs)))